66 Commits

Author SHA1 Message Date
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
51 changed files with 1044 additions and 506 deletions

View File

@@ -15,18 +15,27 @@ rustflags = ["-C", "target-feature=+crt-static"]
linker = "rust-lld"
rustflags = ["-C", "target-feature=+crt-static"]
[target.x86_64-apple-darwin]
linker = "rust-lld"
rustflags = ["-C", "target-feature=+crt-static"]
# Disable rust-lld for x86 macOS because the linker crashers when compiling
# the installer in release mode with debug info on.
# [target.x86_64-apple-darwin]
# linker = "rust-lld"
# rustflags = ["-C", "target-feature=+crt-static"]
[target.x86_64-unknown-linux-musl]
linker = "rust-lld"
rustflags = ["-C", "target-feature=+crt-static"]
# optimizations to reduce the binary size
# keep line numbers in stack traces for non-firmware binaries
[profile.release]
debug = "limited"
# optimizations to reduce the binary size of firmware binaries
[profile.firmware]
inherits = "release"
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
debug = false

View File

@@ -50,11 +50,11 @@ jobs:
with:
targets: armv7-unknown-linux-musleabihf
- name: Build rootshell (arm32)
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --release
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/release/rootshell
path: target/armv7-unknown-linux-musleabihf/firmware/rootshell
if-no-files-found: error
build_rayhunter:
strategy:
@@ -74,11 +74,19 @@ jobs:
npm install
npm run build
popd
cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --release --no-default-features --features ${{ matrix.device.name }}
# 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/release/rayhunter-daemon
path: target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon
if-no-files-found: error
build_rust_installer:
needs:
@@ -134,7 +142,7 @@ jobs:
run: |
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
mkdir "$VERSIONED_DIR"
mv rayhunter-daemon-* rootshell/rootshell installer-* "$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 }}"

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
/book
.DS_Store

97
Cargo.lock generated
View File

@@ -5,7 +5,7 @@ version = 4
[[package]]
name = "adb_client"
version = "2.1.11"
source = "git+https://github.com/gaykitty/adb_client.git?rev=1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81#1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
source = "git+https://github.com/gaykitty/adb_client.git?rev=e732fc178a0eb237138e4091059ff5ffa241385a#e732fc178a0eb237138e4091059ff5ffa241385a"
dependencies = [
"async-io",
"base64",
@@ -175,7 +175,7 @@ checksum = "b1d056f38f3e98ffe9808e6a482d1527e4ded46e72b3dbe77305f5508e76c5f6"
dependencies = [
"bitvec",
"clap",
"env_logger",
"env_logger 0.10.2",
"heck 0.4.1",
"lazy_static",
"log",
@@ -791,6 +791,16 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "env_logger"
version = "0.10.2"
@@ -804,6 +814,19 @@ dependencies = [
"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]]
name = "equivalent"
version = "1.0.2"
@@ -921,7 +944,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -944,17 +966,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@@ -1003,13 +1014,10 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@@ -1425,13 +1433,14 @@ dependencies = [
[[package]]
name = "installer"
version = "0.3.0"
version = "0.3.3"
dependencies = [
"adb_client",
"anyhow",
"axum",
"bytes",
"clap",
"env_logger 0.11.8",
"hyper",
"hyper-util",
"md5",
@@ -1503,6 +1512,30 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "jobserver"
version = "0.1.33"
@@ -2051,6 +2084,21 @@ dependencies = [
"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]]
name = "potential_utf"
version = "0.1.2"
@@ -2259,15 +2307,13 @@ dependencies = [
[[package]]
name = "rayhunter"
version = "0.3.0"
version = "0.3.3"
dependencies = [
"bytes",
"chrono",
"crc",
"deku",
"env_logger",
"futures",
"futures-core",
"libc",
"log",
"nix",
@@ -2280,14 +2326,13 @@ dependencies = [
[[package]]
name = "rayhunter-daemon"
version = "0.3.0"
version = "0.3.3"
dependencies = [
"axum",
"chrono",
"clap",
"env_logger",
"env_logger 0.11.8",
"futures",
"futures-core",
"futures-macro",
"image",
"include_dir",
@@ -2408,7 +2453,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
[[package]]
name = "rootshell"
version = "0.3.0"
version = "0.3.3"
dependencies = [
"nix",
]
@@ -2781,7 +2826,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "telcom-parser"
version = "0.3.0"
version = "0.3.3"
dependencies = [
"asn1-codecs",
"asn1-compiler",

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter-daemon"
version = "0.3.0"
version = "0.3.3"
edition = "2021"
[features]
@@ -22,20 +22,19 @@ path = "src/check.rs"
rayhunter = { path = "../lib" }
toml = "0.8.8"
serde = { version = "1.0.193", features = ["derive"] }
tokio = { version = "1.44.2", features = ["full"] }
axum = "0.8"
futures-core = "0.3.30"
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
thiserror = "1.0.52"
libc = "0.2.150"
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"] }
futures-macro = "0.3.30"
include_dir = "0.7.3"
mime_guess = "2.0.4"
chrono = { version = "0.4.31", features = ["serde"] }
tokio-stream = "0.1.14"
futures = "0.3.30"
tokio-stream = { version = "0.1.14", default-features = false }
futures = { version = "0.3.30", default-features = false }
clap = { version = "4.5.2", features = ["derive"] }
serde_json = "1.0.114"
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }

View File

@@ -11,6 +11,7 @@ pub struct Config {
pub ui_level: u8,
pub enable_dummy_analyzer: bool,
pub colorblind_mode: bool,
pub key_input_mode: u8,
}
impl Default for Config {
@@ -22,6 +23,7 @@ impl Default for Config {
ui_level: 1,
enable_dummy_analyzer: false,
colorblind_mode: false,
key_input_mode: 1,
}
}
}

View File

@@ -4,6 +4,7 @@ mod diag;
mod display;
mod dummy_analyzer;
mod error;
mod key_input;
mod pcap;
mod qmdl_store;
mod server;
@@ -175,7 +176,7 @@ async fn main() -> Result<(), RayhunterError> {
let store = init_qmdl_store(&config).await?;
let analysis_status = AnalysisStatus::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 (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
let mut maybe_ui_shutdown_tx = None;
@@ -193,13 +194,17 @@ async fn main() -> Result<(), RayhunterError> {
run_diag_read_thread(
&task_tracker,
dev,
rx,
diag_rx,
ui_update_tx.clone(),
qmdl_store_lock.clone(),
analysis_tx.clone(),
config.enable_dummy_analyzer,
);
info!("Starting UI");
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
info!("Starting Key Input service");
key_input::run_key_input_thread(&task_tracker, &config, diag_tx.clone());
}
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
info!("create shutdown thread");
@@ -213,7 +218,7 @@ async fn main() -> Result<(), RayhunterError> {
);
run_ctrl_c_thread(
&task_tracker,
tx.clone(),
diag_tx.clone(),
server_shutdown_tx,
maybe_ui_shutdown_tx,
qmdl_store_lock.clone(),
@@ -221,7 +226,7 @@ async fn main() -> Result<(), RayhunterError> {
);
let state = Arc::new(ServerState {
qmdl_store_lock: qmdl_store_lock.clone(),
diag_device_ctrl_sender: tx,
diag_device_ctrl_sender: diag_tx,
ui_update_sender: ui_update_tx,
debug_mode: config.debug_mode,
analysis_status_lock,

View File

@@ -7,7 +7,7 @@ use axum::http::header::CONTENT_TYPE;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use futures::{StreamExt, TryStreamExt};
use log::{debug, error, info};
use log::{debug, error, info, warn};
use rayhunter::diag::DataType;
use rayhunter::diag_device::DiagDevice;
use rayhunter::qmdl::QmdlWriter;
@@ -24,7 +24,7 @@ use crate::server::ServerState;
pub enum DiagDeviceCtrlMessage {
StopRecording,
StartRecording((QmdlWriter<File>, File)),
StartRecording,
Exit,
}
@@ -34,6 +34,7 @@ pub fn run_diag_read_thread(
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
ui_update_sender: Sender<display::DisplayState>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_sender: Sender<AnalysisCtrlMessage>,
enable_dummy_analyzer: bool,
) {
task_tracker.spawn(async move {
@@ -46,20 +47,56 @@ pub fn run_diag_read_thread(
tokio::select! {
msg = qmdl_file_rx.recv() => {
match msg {
Some(DiagDeviceCtrlMessage::StartRecording((new_writer, new_analysis_file))) => {
maybe_qmdl_writer = Some(new_writer);
Some(DiagDeviceCtrlMessage::StartRecording) => {
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 {
analysis_writer.close().await.expect("failed to close analysis writer");
}
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
.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) => {
let mut qmdl_store = qmdl_store_lock.write().await;
match qmdl_store.get_current_entry() {
Some((_, entry)) => {
if let Err(e) = analysis_sender
.send(AnalysisCtrlMessage::RecordingFinished(
entry.name.to_string(),
))
.await {
warn!("couldn't send analysis message: {}", e);
}
}
None => todo!(),
}
if let Err(e) = qmdl_store.close_current_entry().await {
error!("couldn't close current entry: {}", e);
}
maybe_qmdl_writer = None;
if let Some(analysis_writer) = maybe_analysis_writer {
analysis_writer.close().await.expect("failed to close analysis writer");
}
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
// time to go
@@ -125,37 +162,15 @@ pub async fn start_recording(
if state.debug_mode {
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
.diag_device_ctrl_sender
.send(DiagDeviceCtrlMessage::StartRecording((
qmdl_writer,
analysis_file,
)))
.send(DiagDeviceCtrlMessage::StartRecording)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send stop 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),
format!("couldn't send start recording message: {}", e),
)
})?;
@@ -168,30 +183,6 @@ pub async fn stop_recording(
if state.debug_mode {
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
.diag_device_ctrl_sender
.send(DiagDeviceCtrlMessage::StopRecording)
@@ -202,16 +193,6 @@ pub async fn stop_recording(
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()))
}

100
bin/src/key_input.rs Normal file
View File

@@ -0,0 +1,100 @@
use log::error;
use std::time::{Duration, Instant};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
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>,
) {
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;
loop {
if let Err(e) = file.read_exact(&mut buffer).await {
error!("failed to read key input: {}", e);
return;
}
let event = parse_event(buffer);
match event {
Event::KeyUp => {
if last_keyup.is_some()
&& last_keyup.unwrap().elapsed() < Duration::from_millis(500)
{
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;
} else {
last_keyup = Some(Instant::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));
}
}

View File

@@ -1,7 +1,9 @@
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
use chrono::{DateTime, Local};
use rayhunter::util::RuntimeMetadata;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thiserror::Error;
use tokio::{
fs::{self, try_exists, File, OpenOptions},
@@ -289,10 +291,10 @@ impl RecordingStore {
self.write_manifest().await?;
let qmdl_filepath = entry_to_delete.get_qmdl_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
.map_err(RecordingStoreError::DeleteFileError)?;
tokio::fs::remove_file(analysis_filepath)
remove_file_if_exists(&analysis_filepath)
.await
.map_err(RecordingStoreError::DeleteFileError)?;
Ok(entry_to_delete)
@@ -303,22 +305,41 @@ impl RecordingStore {
self.close_current_entry().await?;
}
let mut keep = Vec::new();
for entry in &self.manifest.entries {
let qmdl_filepath = entry.get_qmdl_filepath(&self.path);
let analysis_filepath = entry.get_analysis_filepath(&self.path);
tokio::fs::remove_file(qmdl_filepath)
.await
.map_err(RecordingStoreError::DeleteFileError)?;
tokio::fs::remove_file(analysis_filepath)
.await
.map_err(RecordingStoreError::DeleteFileError)?;
if let Err(e) = remove_file_if_exists(&qmdl_filepath).await {
log::warn!("failed to remove {qmdl_filepath:?}: {e:?}");
keep.push(true);
continue;
}
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?;
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)]
mod tests {
use super::*;

View File

@@ -7,6 +7,6 @@
%sveltekit.head%
</head>
<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>
</html>

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 => {
if (event_json === null) {
return null;
} else if (event_json.event_type === "Informational") {
} else if (event_json.event_type.type === "Informational") {
num_informational_logs += 1;
return {
type: EventType.Informational,
@@ -82,8 +82,8 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
num_warnings += 1;
return {
type: EventType.Warning,
severity: event_json.severity === "High" ? Severity.High :
event_json.severity === "Medium" ? Severity.Medium : Severity.Low,
severity: event_json.event_type.severity === "High" ? Severity.High :
event_json.event_type.severity === "Medium" ? Severity.Medium : Severity.Low,
message: event_json.message,
};
}

View File

@@ -2,9 +2,10 @@
import { AnalysisStatus } from "$lib/analysisManager.svelte";
import { EventType } from "$lib/analysis.svelte";
import type { ManifestEntry } from "$lib/manifest.svelte";
let { entry, onclick }: {
let { entry, onclick, analysis_visible}: {
entry: ManifestEntry,
onclick: () => void,
analysis_visible: boolean,
} = $props();
let summary = $derived.by(() => {
@@ -41,12 +42,11 @@
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>
<button class={button_class} disabled={!ready} {onclick}>
{summary}
</button>
<style>
</style>
<button class="flex flex-row gap-1 lg:gap-2" 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>
<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">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/>
</svg>
</button>

View File

@@ -25,60 +25,63 @@
return map;
});
</script>
<p class="text-lg underline">Warnings and Informational Logs</p>
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
<p>Nothing to show!</p>
{:else}
<table class="table-auto text-left border">
<thead class="p-2">
<tr class="bg-gray-300">
<th scope="col">Timestamp</th>
<th scope="col">Warning</th>
<th scope="col">Severity</th>
</tr>
</thead>
<tbody>
{#each report.rows as row, row_idx}
{#each row.analysis as analysis}
{@const parsed_date = new Date(analysis.timestamp)}
{#each analysis.events.filter(e => e !== null) as event}
<tr class="even:bg-gray-200 border-b">
{#if event.type === EventType.Warning}
<div>
<p class="text-lg underline">Warnings and Informational Logs</p>
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
<p>Nothing to show!</p>
{:else}
<table class="table-auto text-left">
<thead class="p-2">
<tr class="bg-gray-300">
<th class="p-2">Timestamp</th>
<th class="p-2">Warning</th>
<th class="p-2">Severity</th>
</tr>
</thead>
<tbody>
{#each report.rows as row, row_idx}
{#each row.analysis as analysis}
{@const parsed_date = new Date(analysis.timestamp)}
{#each analysis.events.filter(e => e !== null) as event}
<tr class="even:bg-gray-200 odd:bg-white">
{#if event.type === EventType.Warning}
{@const severity = ['Low', 'Medium', 'High'][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">{event.message}</td>
<td class="p-2 {severity_class}">{severity}</td>
{:else if event.type === EventType.Informational}
<th class="p-2">{date_formatter.format(parsed_date)}</th>
<td class="p-2">{event.message}</td>
<td class="p-2">Info</td>
{/if}
</tr>
{@const severity_class = ['bg-red-200', 'bg-red-400', 'bg-red-600'][event.severity]}
<td class="p-2">{date_formatter.format(parsed_date)}</td>
<td class="p-2">{event.message}</td>
<td class="p-2 {severity_class} text-center">{severity}</td>
{:else if event.type === EventType.Informational}
<td class="p-2">{date_formatter.format(parsed_date)}</td>
<td class="p-2">{event.message}</td>
<td class="p-2">Info</td>
{/if}
</tr>
{/each}
{/each}
{/each}
{/each}
</tbody>
</table>
{/if}
</tbody>
</table>
{/if}
</div>
{#if report.statistics.num_skipped_packets > 0}
<div>
<p class="text-lg underline">Unparsed Messages</p>
<p>These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a problem.</p>
<table class="table-auto text-left border">
<table class="table-auto text-left">
<thead class="p-2">
<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>
</tr>
</thead>
<tbody>
{#each skipped_messages.entries() as [message, count]}
<tr class="even:bg-gray-200 border-b">
<td>{count}</td>
<tr class="even:bg-gray-200 odd:bg-white">
<td class="text-center">{count}</td>
<td>{message}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}

View File

@@ -13,32 +13,34 @@
});
</script>
<div class="container max-h-96 overflow-auto">
<div class="container mt-2">
{#if entry.analysis_report === undefined}
<p>Report unavailable, try refreshing.</p>
{:else if typeof(entry.analysis_report) === 'string'}
<p>Error getting analysis report: {entry.analysis_report}</p>
{:else}
{@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}
<AnalysisTable report={entry.analysis_report} />
{:else}
<p>No warnings to display!</p>
{/if}
{#if metadata !== undefined && metadata.rayhunter !== undefined}
<div>
<p class="text-lg underline">Metadata</p>
{#if metadata !== undefined && metadata.rayhunter !== undefined}
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</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}
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
</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>
{/if}
</div>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,7 +1,8 @@
<script lang="ts">
let { url, text }: {
let { url, text, full_button=false }: {
url: string;
text: string;
full_button?: boolean;
} = $props();
function download() {
@@ -9,8 +10,9 @@
}
</script>
<button class="text-blue-600 flex flex-row 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">
<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">
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/>
</svg>
</button>

View File

@@ -0,0 +1,74 @@
<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: {date_formatter.format(entry.last_message_time)}</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 />
{#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>

View File

@@ -1,32 +1,37 @@
<script lang="ts">
import { Manifest, ManifestEntry } from "$lib/manifest.svelte";
import TableRow from "./ManifestTableRow.svelte";
import Card from "./ManifestCard.svelte"
interface Props {
entries: ManifestEntry[];
current_entry: ManifestEntry | undefined;
server_is_recording: boolean;
}
let { entries, current_entry }: Props = $props();
let { entries, server_is_recording }: Props = $props();
</script>
<table class="table-auto text-left border">
<thead class="p-2">
<tr class="bg-gray-300">
<th class='p-2' scope="col">Name</th>
<th class='p-2' scope="col">Date Started</th>
<th class='p-2' scope="col">Date of Last Message</th>
<th class='p-2' scope="col">Size (bytes)</th>
<!--For larger screens we use a table-->
<table class="hidden table-auto text-left lg:table">
<thead>
<tr class="bg-gray-100 drop-shadow">
<th class='p-2' scope="col">ID</th>
<th class='p-2' scope="col">Started</th>
<th class='p-2' scope="col">Last Message</th>
<th class='p-2' scope="col">Size</th>
<th class='p-2' scope="col">PCAP</th>
<th class='p-2' scope="col">QMDL</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>
</thead>
<tbody>
{#if current_entry !== undefined}
<TableRow entry={current_entry} current={true} i={0} />
{/if}
{#each entries as entry, i}
<TableRow {entry} current={false} {i} />
{/each}
</tbody>
</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} />
{/each}
</div>

View File

@@ -7,7 +7,7 @@
let { entry, current, i }: {
entry: ManifestEntry;
current: boolean;
i: number
i: number;
} = $props();
// passing `undefined` as the locale uses the browser default
@@ -29,14 +29,14 @@
}
</script>
<tr class="{status_row_color}">
<th class="font-bold p-2 bg-blue-100" scope='row'>{entry.name}</th>
<tr class="{status_row_color} drop-shadow">
<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.last_message_time)}</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_qmdl_url()} text="qmdl" /></td>
<td class="p-2"><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} /></td>
<td class="p-2"><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></td>
{#if current}
<td class="p-2"></td>
{:else}
@@ -49,8 +49,7 @@
{/if}
</tr>
<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="7">
<td class="border-t border-dashed p-2" colspan="8">
<AnalysisView {entry} />
</td>
</tr>

View File

@@ -17,8 +17,7 @@
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 start_recording_classes = "bg-blue-500 hover:bg-blue-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";
</script>
<div>
@@ -27,9 +26,19 @@
{server_is_recording ? "Stopping..." : "Starting..."}
</button>
{: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}
<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}
</div>

View File

@@ -4,31 +4,32 @@
stats: SystemStats;
} = $props();
const table_cell_classes = "border p-1 lg:p-2";
</script>
<div>
<p class="text-xl">System Stats</p>
<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 mb-2">System Information</p>
<table class="table-auto border">
<tbody>
<tr class="border">
<th class="border">
Rayhunter version
<th class={table_cell_classes}>
Rayhunter Version
</th>
<td class="border">{stats.runtime_metadata.rayhunter_version}</td>
<td class={table_cell_classes}>{stats.runtime_metadata.rayhunter_version}</td>
</tr>
<tr class="border">
<th class="border">
<th class={table_cell_classes}>
Storage
</th>
<td class="border">
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} / {stats.disk_stats.available_size})
<td class={table_cell_classes}>
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} used / {stats.disk_stats.available_size} available)
</td>
</tr>
<tr class="border-b">
<th class="border">
<th class={table_cell_classes}>
Memory (RAM)
</th>
<td class="border">
<td class={table_cell_classes}>
Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used}
</td>
</tr>

View File

@@ -69,6 +69,15 @@ export class ManifestEntry {
}
}
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 {
if (this.analysis_report === undefined || typeof(this.analysis_report) === 'string') {
return undefined;

View File

@@ -2,10 +2,12 @@
import { ManifestEntry } from "$lib/manifest.svelte";
import { get_manifest, get_system_stats } from "$lib/utils.svelte";
import ManifestTable from "$lib/components/ManifestTable.svelte";
import Card from "$lib/components/ManifestCard.svelte";
import type { SystemStats } from "$lib/systemStats";
import { AnalysisManager } from "$lib/analysisManager.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";
let manager: AnalysisManager = new AnalysisManager();
let loaded = $state(false);
@@ -30,13 +32,53 @@
})
</script>
<h1 class="ml-8 mt-8 text-4xl font-extrabold">Rayhunter Dashboard</h1>
<div class="p-8 flex flex-col gap-2">
<div class="p-4 xl:px-8 bg-rayhunter-blue drop-shadow flex flex-row justify-between items-center">
<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}
<ControlBar server_is_recording={recording} />
<SystemStatsTable stats={system_stats!} />
<ManifestTable entries={entries} current_entry={current_entry} />
<div class="flex flex-col lg:flex-row gap-4">
{#if recording}
<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/>
{: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}
</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

View File

@@ -14,5 +14,9 @@ colorblind_mode = false
#
# TP-Link with one-bit display:
# 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
# 0 = rayhunter does not read button presses
# 1 = double-tapping the power button starts/stops recordings
key_input_mode = 1

BIN
doc/Rayhunter_0.3.2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -1,139 +1,32 @@
# Installing from the latest release (Windows)
1. Install the [Zadig WinUSB driver](https://zadig.akeo.ie/).
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!`
Windows support in Rayhunter's installer is a work-in-progress. Depending on the device, the installation instructions differ.
## TP-Link
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).
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.
## Orbic
1. Connect the device to your computer using the provided USB cable.
1. Install the [Zadig WinUSB driver installer](https://zadig.akeo.ie/).
1. Open Zadig, click options->show all devices
![Zadig](./zadig2.png)
1. Select 'RNDIS (Interface 0)'
![Zadig](./zadig.png)
1. Click 'install driver' and wait for it to finish.
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
3. Unzip `rayhunter-vX.X.X` .
1. Open a powershell terminal by pressing Win+R and typing `powershell` and hitting enter.
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.
5. Run the install script: `.\installer.exe orbic` and hit enter.
- The device will restart multiple times over the next few minutes.
- You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
6. Rayhunter should now be running! You can verify this by following the instructions below to [view the web UI](#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`
```powershell
$global:adb = "./platform-tools-latest-windows/platform-tools/adb.exe"
$global:serial = "./serial-windows-x86_64/serial.exe"
function _adb_push {
$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 {
$proc = start-process -passthru -wait $global:adb -argumentlist "shell", $args[0]
if ($proc.exitcode -ne 0) {
write-host "shell exited with exit code $($proc.exitcode)"
}
return $proc.exitcode
}
function _wait_for_adb_shell {
do {
start-sleep -seconds 1
} until ((_adb_shell "cat /etc/ver.conf") -eq 0)
}
function _wait_for_atfwd_daemon {
do {
start-sleep -seconds 1
} until ((_adb_shell "pgrep atfwd_daemon") -eq 0)
}
function force_debug_mode {
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 {
_adb_push "rootshell" "/tmp"
write-host "cp..."
&$global:serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host
start-sleep -seconds 1
write-host "chown..."
&$global:serial "AT+SYSCMD=chown root /bin/rootshell" | Out-Host
start-sleep -seconds 1
write-host "chmod..."
&$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
```

View File

@@ -2,13 +2,12 @@
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)
2. Decompress the `release.tar` archive. Open the terminal and navigate to the folder
1. Download the latest `rayhunter-vX.X.X.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases)
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
mkdir ~/Downloads/release
tar -xvf ~/Downloads/release.tar -C ~/Downloads/release
cd ~/Downloads/release
unzip ~/Downloads/rayhunter-vX.X.X.zip
cd ~/Downloads/rayhunter-vX.X.X
```
3. Turn on your device by holding the power button on the front.
@@ -18,17 +17,32 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
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
./install orbic
# or: ./install tplink
# On MacOS, you must first remove the quarantine bit
xattr -d com.apple.quarantine install
```
Then run the installer:
```bash
./installer orbic
# or: ./installer tplink
```
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.
## 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

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:
```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
```

View File

@@ -1,8 +1,24 @@
# 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 from 0.3.0 release. 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
The TP-Link comes in many different *hardware versions*. Support for installation varies:
* `1.0`, `2.0`: **Not suported**, probably impossible to obtain anymore (even second-hand), however there is one report that installation is possible on `1.0` (but no reports if it is working or not)
* `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**
* `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 Ebay or equivalent, sometimes it's much cheaper there.
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350)
@@ -10,9 +26,9 @@ You can get it from:
## 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:
Unlike on Orbic, the installer will not enable ADB. Instead, you can obtain a root shell with the following command:
```sh
./installer util tplink-start-telnet
@@ -21,34 +37,40 @@ telnet 192.168.0.1
## Display states
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).
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).
If your device has a one-bit (black-and-white) display, Rayhunter will instead
show an emoji to indicate status:
If your device has a one-bit (black-and-white) display, Rayhunter will instead show an emoji to indicate status:
* `!` means "warning (potential IMSI catcher)"
* `:)` (smiling) means "recording"
* `:` (face with no mouth) means "paused"
## Hardware versions
## Configuration
The TP-Link comes in many different *hardware versions*. Support for installation varies:
Displaying status can be changed in the configuration (`config.toml`) file, where UI level (`ui_level` variable) could be changed to:
- `0`: invisible mode, no indicator that Rayhunter is running
- `1`: subtle mode, display a green line at the top of the screen when Rayhunter is running
- `2`: demo mode, display a fun Orca GIF
- `3`: display the EFF logo
* `1.0-2.0`: Not tested, probably impossible to obtain anymore (even second-hand)
* `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.
You can also change `colorblind_mode` (default is `false`) to `true`. In that case there will be blue line instead of green line.
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 change the `port` (default is `8080`) where Rayhunter is listening for incoming connections and more advanced users can change the variables `qmdl_store_path` and `debug_mode`. However, change those variables only if you know what you are doing.
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`.
## Power-saving mode/sleep
When filing bug reports, particularly with the installer, please always
specify the exact hardware version.
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.
## Port triggers
On hardware revisions starting with v4.0, the installer will modify settings to
add two port triggers. You can look at `Settings > NAT Settings > Port
Triggers` in TP-Link's admin UI to see them.
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`.
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.
## 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/)

View File

@@ -16,4 +16,9 @@ Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabl
## 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.

View File

@@ -2,19 +2,30 @@
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.
![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.
## The web UI
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)
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.
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 **Orbic**, you can find the WiFi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
* **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.
* 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`.
* **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.
* 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 (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.

BIN
doc/zadig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
doc/zadig2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
doc/zadig3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -1,10 +1,11 @@
#!/bin/bash -e
pushd bin/web
cd bin/web
npm run build
popd
#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"'
cd ..
docker build -t rayhunter-devenv -f tools/devenv.dockerfile .
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 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..."
adb shell '/bin/rootshell -c "reboot"'

View File

@@ -1,6 +1,6 @@
[package]
name = "installer"
version = "0.3.0"
version = "0.3.3"
edition = "2024"
[dependencies]
@@ -8,6 +8,7 @@ anyhow = "1.0.98"
axum = "0.8.3"
bytes = "1.10.1"
clap = { version = "4.5.37", features = ["derive"] }
env_logger = "0.11.8"
hyper = "1.6.0"
hyper-util = "0.1.11"
md5 = "0.7.0"
@@ -21,12 +22,12 @@ tokio-stream = "0.1.17"
[target.'cfg(target_os = "linux")'.dependencies.adb_client]
git = "https://github.com/gaykitty/adb_client.git"
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
rev = "e732fc178a0eb237138e4091059ff5ffa241385a"
default-features = false
features = ["trans-nusb"]
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client]
git = "https://github.com/gaykitty/adb_client.git"
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
rev = "e732fc178a0eb237138e4091059ff5ffa241385a"
default-features = false
features = ["trans-libusb"]

View File

@@ -6,7 +6,7 @@ fn main() {
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
let include_dir = Path::new(concat!(
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(

142
installer/install.ps1 Normal file
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

View File

@@ -1,5 +1,6 @@
use anyhow::{Context, Error, bail};
use clap::{Parser, Subcommand};
use env_logger::Env;
mod orbic;
mod tplink;
@@ -34,6 +35,17 @@ struct InstallTpLink {
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
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)]
@@ -49,6 +61,8 @@ struct Util {
enum UtilSubCommand {
/// Send a serial command to the Orbic.
Serial(Serial),
/// Start an ADB shell
Shell(Shell),
/// Root the tplink and launch telnetd.
TplinkStartTelnet(TplinkStartTelnet),
}
@@ -67,7 +81,11 @@ struct Serial {
command: Vec<String>,
}
#[derive(Parser, Debug)]
struct Shell {}
async fn run() -> Result<(), Error> {
env_logger::Builder::from_env(Env::default().default_filter_or("off")).init();
let Args { command } = Args::parse();
match command {
@@ -92,6 +110,7 @@ async fn run() -> Result<(), Error> {
}
}
}
UtilSubCommand::Shell(_) => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
UtilSubCommand::TplinkStartTelnet(options) => {
tplink::start_telnet(&options.admin_ip).await?;
}

View File

@@ -22,16 +22,24 @@ 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.
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.
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.
If you have adb installed you may need to kill the adb daemon"#;
const VENDOR_ID: u16 = 0x05c6;
const PRODUCT_ID: u16 = 0xf601;
const INTERFACE: u8 = 1;
#[cfg(target_os = "windows")]
const RNDIS_INTERFACE: u8 = 0;
#[cfg(not(target_os = "windows"))]
const RNDIS_INTERFACE: u8 = 1;
macro_rules! echo {
($($arg:tt)*) => {
print!($($arg)*);
@@ -41,12 +49,11 @@ macro_rules! echo {
pub async fn install() -> Result<()> {
let mut adb_device = force_debug_mode().await?;
let serial_interface = open_orbic()?.ok_or_else(|| anyhow!(ORBIC_NOT_FOUND))?;
echo!("Installing rootshell... ");
setup_rootshell(&serial_interface, &mut adb_device).await?;
setup_rootshell(&mut adb_device).await?;
println!("done");
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");
echo!("Testing rayhunter... ");
test_rayhunter(&mut adb_device).await?;
@@ -54,11 +61,19 @@ pub async fn install() -> Result<()> {
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> {
println!("Forcing a switch into the debug mode to enable ADB");
enable_command_mode()?;
echo!("ADB enabled, waiting for reboot... ");
let mut adb_device = get_adb().await?;
adb_setup_serial(&mut adb_device).await?;
println!("it's alive!");
echo!("Waiting for atfwd_daemon to startup... ");
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
@@ -67,22 +82,20 @@ async fn force_debug_mode() -> Result<ADBUSBDevice> {
}
async fn setup_rootshell(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice,
) -> Result<()> {
let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
install_file(
serial_interface,
adb_device,
"/bin/rootshell",
rootshell_bin,
)
.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;
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"])?;
if !output.contains("uid=0") {
bail!("rootshell is not giving us root.");
@@ -91,45 +104,40 @@ async fn setup_rootshell(
}
async fn setup_rayhunter(
serial_interface: &Interface,
mut adb_device: ADBUSBDevice,
) -> Result<ADBUSBDevice> {
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(
serial_interface,
&mut adb_device,
"/data/rayhunter/rayhunter-daemon",
rayhunter_daemon_bin,
)
.await?;
install_file(
serial_interface,
&mut adb_device,
"/data/rayhunter/config.toml",
CONFIG_TOML.as_bytes(),
)
.await?;
install_file(
serial_interface,
&mut adb_device,
"/etc/init.d/rayhunter_daemon",
RAYHUNTER_DAEMON_INIT.as_bytes(),
)
.await?;
install_file(
serial_interface,
&mut adb_device,
"/etc/init.d/misc-daemon",
include_bytes!("../../dist/scripts/misc-daemon"),
)
.await?;
at_syscmd(serial_interface, "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/rayhunter_daemon").await?;
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
println!("done");
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)
tokio::time::timeout(Duration::from_secs(30), async {
while let Ok(dev) = adb_echo_test(adb_device).await {
@@ -162,7 +170,6 @@ async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
}
async fn install_file(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice,
dest: &str,
payload: &[u8],
@@ -170,7 +177,7 @@ async fn install_file(
const MAX_FAILURES: u32 = 5;
let mut failures = 0;
loop {
match install_file_impl(serial_interface, adb_device, dest, payload).await {
match install_file_impl(adb_device, dest, payload).await {
Ok(()) => return Ok(()),
Err(e) => {
if failures > MAX_FAILURES {
@@ -185,7 +192,6 @@ async fn install_file(
}
async fn install_file_impl(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice,
dest: &str,
mut payload: &[u8],
@@ -202,7 +208,7 @@ async fn install_file_impl(
let file_hash_bytes = hasher.finalize();
let file_hash = format!("{file_hash_bytes:x}");
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
.stat(dest)
.context("Failed to stat transfered file")?;
@@ -244,7 +250,7 @@ async fn get_adb() -> Result<ADBUSBDevice> {
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => {
bail!(ORBIC_BUSY);
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os="windows"))]
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => {
bail!(ORBIC_BUSY_MAC);
}
@@ -321,9 +327,58 @@ async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
}
}
async fn at_syscmd(interface: &Interface, command: &str) -> Result<()> {
send_serial_cmd(interface, &format!("AT+SYSCMD={command}")).await
async fn adb_setup_serial(adb_device: &mut ADBUSBDevice) -> Result<()> {
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
///
/// First establish a USB handle and context by calling `open_orbic(<T>)
@@ -400,7 +455,7 @@ pub fn enable_command_mode() -> Result<()> {
index: 0,
};
let interface = device
.detach_and_claim_interface(1)
.detach_and_claim_interface(RNDIS_INTERFACE)
.context("detach_and_claim_interface(1) failed")?;
if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) {
// If the device reboots while the command is still executing we
@@ -421,7 +476,7 @@ pub fn open_orbic() -> Result<Option<Interface>> {
// Device after initial mode switch
if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? {
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")?;
return Ok(Some(interface));
}
@@ -429,7 +484,7 @@ pub fn open_orbic() -> Result<Option<Interface>> {
// Device with rndis enabled as well
if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? {
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")?;
return Ok(Some(interface));
}

View File

@@ -27,10 +27,11 @@ pub async fn main_tplink(
InstallTpLink {
skip_sdcard,
admin_ip,
sdcard_path,
}: InstallTpLink,
) -> Result<(), Error> {
start_telnet(&admin_ip).await?;
tplink_run_install(skip_sdcard, admin_ip).await
let is_v3 = start_telnet(&admin_ip).await?;
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
}
#[derive(Deserialize)]
@@ -38,7 +39,7 @@ struct V3RootResponse {
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();
@@ -51,7 +52,9 @@ pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
.send()
.await?;
if response.status() == 404 {
let is_v3 = response.status() != 404;
if !is_v3 {
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
tplink_launch_telnet_v5(admin_ip).await?;
} else {
@@ -82,20 +85,49 @@ pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
println!(
"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."
);
Ok(())
Ok(is_v3)
}
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}");
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
if !skip_sdcard {
println!("Mounting sdcard");
if telnet_send_command(addr, "mount | grep -q /media/card", "exit code 0")
.await
.is_err()
if sdcard_path.is_empty() {
if telnet_send_command(addr, "ls /media/card", "exit code 0")
.await
.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 {
println!("sdcard already mounted");
}
@@ -105,28 +137,38 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
// expects things to be at this location
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, "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(
addr,
"/media/card/config.toml",
&format!("{sdcard_path}/config.toml"),
crate::CONFIG_TOML.as_bytes(),
)
.await?;
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(
addr,
"/etc/init.d/rayhunter_daemon",
get_rayhunter_daemon().as_bytes(),
get_rayhunter_daemon(&sdcard_path).as_bytes(),
)
.await?;
telnet_send_command(
addr,
"chmod ugo+x /media/card/rayhunter-daemon",
&format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"),
"exit code 0",
)
.await?;
@@ -136,7 +178,13 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
"exit code 0",
)
.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!(
"Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080"
@@ -278,6 +326,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.
data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"});
alert("Success! You can go back to the rayhunter installer.");
window.clearInterval(window.rayhunterPoll);
}, 1000);"#);
@@ -324,7 +373,7 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
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
// changing the order in which daemons are started up seems to not work reliably.
//
@@ -332,12 +381,12 @@ fn get_rayhunter_daemon() -> String {
// specific to a particular hardware revision here.
crate::RAYHUNTER_DAEMON_INIT.replace(
"#RAYHUNTER-PRESTART",
"mount /dev/mmcblk0p1 /media/card || true",
&format!("mount /dev/mmcblk0p1 {sdcard_path} || true"),
)
}
#[test]
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"));
}

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter"
version = "0.3.0"
version = "0.3.3"
edition = "2021"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
@@ -19,14 +19,12 @@ bytes = "1.5.0"
chrono = "0.4.31"
crc = "3.0.1"
deku = { version = "0.18.0", features = ["logging"] }
env_logger = "0.10.1"
libc = "0.2.150"
log = "0.4.20"
nix = { version = "0.29.0", features = ["feature"] }
pcap-file-tokio = "0.1.0"
thiserror = "1.0.50"
telcom-parser = { path = "../telcom-parser" }
tokio = { version = "1.44.2", features = ["full"] }
futures-core = "0.3.30"
futures = "0.3.30"
tokio = { version = "1.44.2", default-features = false }
futures = { version = "0.3.30", default-features = false }
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -6,7 +6,7 @@ use crate::hdlc::hdlc_encapsulate;
use crate::log_codes;
use deku::prelude::*;
use futures_core::TryStream;
use futures::TryStream;
use log::{error, info};
use std::io::ErrorKind;
use std::os::fd::AsRawFd;
@@ -251,6 +251,7 @@ impl DiagDevice {
//
// TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct diag_logging_mode_param_t {
req_mode: u32,
peripheral_mask: u32,
@@ -261,30 +262,41 @@ struct diag_logging_mode_param_t {
fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> {
unsafe {
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 {
req_mode: mode,
peripheral_mask: 0,
mode_param: 1,
}
} else {
},
// tplink M7350 HW revision v9 requires the same parameters as orbic
diag_logging_mode_param_t {
req_mode: mode,
peripheral_mask: u32::MAX,
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 {
let msg = format!(
"DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}",

View File

@@ -2,8 +2,8 @@
pushd bin/web
npm run build
popd
cargo build --release --target="armv7-unknown-linux-musleabihf" #--features debug
cargo build --profile firmware --target="armv7-unknown-linux-musleabihf" #--features debug
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
adb push target/armv7-unknown-linux-musleabihf/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..."
adb shell '/bin/rootshell -c "reboot"'

View File

@@ -1,6 +1,6 @@
[package]
name = "rootshell"
version = "0.3.0"
version = "0.3.3"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,6 +1,6 @@
[package]
name = "telcom-parser"
version = "0.3.0"
version = "0.3.3"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html