Compare commits

...

44 Commits

Author SHA1 Message Date
Will Greenberg 6bd36921d8 consider early IMSI request medium sev 2025-01-08 15:23:59 -08:00
Will Greenberg c83ae30be8 fix language 2025-01-08 15:23:59 -08:00
Will Greenberg fa612241a5 lib: add IMSI requested heuristic 2025-01-08 15:23:59 -08:00
Will Greenberg 10592bbd9d lib: add inbound/outbound field to NAS 2025-01-06 16:24:11 -08:00
Will Greenberg 327eaddcd7 rayhunter-check: pcapify qmdl 2025-01-06 16:24:11 -08:00
Will Greenberg 32149c3b37 Update tools/nasparse.py 2024-12-17 14:46:31 -08:00
Cooper Quintin e47d4dacc4 raise error on non nas message 2024-12-17 14:46:31 -08:00
Cooper Quintin 4009e3d1ed fix nits 2024-12-17 14:46:31 -08:00
Cooper Quintin b2cd735a07 proof of concept pcap reader for nas heuristic 2024-12-17 14:46:31 -08:00
Cooper Quintin 94e9a88a91 PoC of python nas heuristic 2024-12-17 14:46:31 -08:00
Cooper Quintin f4a6c834d2 remove false positive IMSI heuristic until we get a NAS parser 2024-12-09 10:53:58 -08:00
Cooper Quintin 95e8f846d3 propegate colorblind mode beyond start/stop 2024-11-26 11:05:13 -08:00
Cooper Quintin 15f128add1 remove unneeded import 2024-11-26 11:05:13 -08:00
Cooper Quintin 87f9cc403b add colorblind mode. Fixes #77 2024-11-26 11:05:13 -08:00
Cooper Quintin 7addf3a67f fix reboot timeout 2024-11-18 17:10:16 -08:00
Cooper Quintin 4d8cc9b738 Revert "name binary rayhunter-daemon"
This reverts commit 9cd5ce3394.
2024-11-18 16:16:43 -08:00
Cooper Quintin b0d797d206 name binary rayhunter-daemon 2024-11-18 16:16:43 -08:00
Will Greenberg 1ae3b5020b fix installer script
With the odd permissions issues we've been seeing, we should use
AT_SYSCMD for all mv operations into /data
2024-11-18 16:16:43 -08:00
Cooper Quintin a23df84848 workaround for root not being root permissions issue (#72)
* workaround for root not being root permissions issue

* update setup_rootshell()
2024-10-24 12:02:47 -07:00
Will Greenberg 4e862841b3 serial: use vendored rusb 2024-10-22 14:46:40 -07:00
Will Greenberg 2cc8404b13 rm unused windows build 2024-10-22 14:35:48 -07:00
Will Greenberg 35ae2962f2 fix build script 2024-10-22 14:35:48 -07:00
Will Greenberg 1134361cca fix build-release 2024-10-22 13:30:34 -07:00
Cooper Quintin bec680f93d fix release script 2024-10-22 12:51:40 -07:00
Cooper Quintin 968af93b69 fix nits (#68) 2024-10-22 12:43:11 -07:00
Cooper Quintin ee75326912 Fix macos install (#67)
* update shell path and some docs

* download ADB if not present

* big O not little o

* bugfix

* bugfix

* silence errors for macos developers

* Update dist/install-common.sh

Co-authored-by: Will Greenberg <willg@eff.org>

---------

Co-authored-by: Will Greenberg <willg@eff.org>
2024-10-22 12:21:27 -07:00
will 3b9a001e88 Update readme to ensure adb is in PATH 2024-10-18 11:49:47 -07:00
will 78d33b2cff serial: claim USB interface
Without doing this, it seems macOS can't use any interface endpoints.
Also disconnect any kernel extensions that're using the interface,
since that can cause issues on macOS as well.
2024-10-18 09:43:12 -07:00
Will Greenberg 6c237e884c lib: rm duplicate TestAnalyzer 2024-10-10 12:41:25 -07:00
Will Greenberg f3e4091e1d daemon: fix some bugs in the frontend js 2024-10-08 14:58:46 -07:00
Will Greenberg 16f705f29c Add the test analyzer entirely via daemon flags
Also consolidate the duplicate AnalysisWriter implementation
2024-10-08 14:58:46 -07:00
Cooper Quintin a6fce6d568 fix bug where warnings not displayed 2024-10-08 14:58:46 -07:00
Will Greenberg fcac6fdf16 Fix broken yaml 2024-10-08 14:58:46 -07:00
Will Greenberg df84faa1f9 On-demand analysis of past recordings
* rayhunter-daemon: API for triggering and reading analysis
* rayhunter-daemon: rename readonly mode to debug mode
* rayhunter-daemon: debug mode allows live-loading frontend files
* rayhunter-check: rework to handle directories
* rayhunter-check: better output
* CI: build rayhunter-check
2024-10-08 14:58:46 -07:00
Cooper Quintin c59fb7c013 Fix UI and add more logging (#61)
* add some more logging

* WIP attempt to fix async on update_ui

* fix async for update_ui
2024-10-03 13:32:59 -07:00
Cooper Quintin ca4f49b15f Framebuffer update (#60)
* first pass at changing the UI color based on state

* adding flag to qmdl metadata for when hueristic is triggered

* update style for web page to match UI and have color alert on heuristic trigger

* add test analyzer

* rename example_analyzer to test_analyzer

* refactor ui update to not depend on server

* refactor to pass around color instead of display state for framebuffer channel

* add debug feature flag for test analyzer

* remove warning status from qmdl manifest

* dont keep has warning around
2024-10-03 10:41:59 -07:00
Will Greenberg 861aaedd47 rayhunter-check improvements 2024-08-19 16:49:01 -07:00
Will Greenberg f6681a3703 Merge pull request #54 from EFForg/mac-bugfix
bugfix for mac install process
2024-08-05 10:11:59 -07:00
Cooper Quintin d6bc307a81 bugfix for mac install process 2024-08-05 10:10:41 -07:00
Will Greenberg 7cbb3369d8 serial: when rooting, don't panic if device is already rooted 2024-08-05 09:57:09 -07:00
Will Greenberg cb3dbff54a install-common: wait for atfwd_daemon to startup
We can't successfully run any AT commands until it has.
2024-08-05 09:57:09 -07:00
Will Greenberg 65e1cd4967 serial: split out --root process, don't wait for reboot, update usage text
We need to wait for atfwd_daemon to startup before sending any AT
commands, and I can't think of a way to reliably do that within rust.
So, instead of trying to switch the device to command mode before
executing a command, require the user to run the "--root" step
beforehand, and then start executing AT commands.
2024-08-05 09:57:09 -07:00
Will Greenberg d6fb54afb3 lib: rm unused imports 2024-08-05 09:57:09 -07:00
Will Greenberg bc93c01890 bin: rm deprecated tempdir crate 2024-08-05 09:57:09 -07:00
38 changed files with 1320 additions and 389 deletions
+16 -8
View File
@@ -8,16 +8,16 @@ env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
build_serial: build_serial_and_check:
strategy: strategy:
matrix: matrix:
platform: platform:
- os: ubuntu-latest - os: ubuntu-latest
build_name: serial serial_build_name: serial
- os: windows-latest check_build_name: rayhunter-check
build_name: serial.exe
- os: macos-latest - os: macos-latest
build_name: serial serial_build_name: serial
check_build_name: rayhunter-check
runs-on: ${{ matrix.platform.os }} runs-on: ${{ matrix.platform.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -26,7 +26,15 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: serial-${{ matrix.platform.os }} name: serial-${{ matrix.platform.os }}
path: ./target/release/${{ matrix.platform.build_name }} path: ./target/release/${{ matrix.platform.serial_build_name }}
if-no-files-found: error
- uses: actions/checkout@v4
- name: Build check
run: cargo build --bin rayhunter-check --release
- uses: actions/upload-artifact@v4
with:
name: rayhunter-check-${{ matrix.platform.os }}
path: ./target/release/${{ matrix.platform.check_build_name }}
if-no-files-found: error if-no-files-found: error
build_rootshell_and_rayhunter: build_rootshell_and_rayhunter:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -56,14 +64,14 @@ jobs:
if-no-files-found: error if-no-files-found: error
build_release_zip: build_release_zip:
needs: needs:
- build_serial - build_serial_and_check
- build_rootshell_and_rayhunter - build_rootshell_and_rayhunter
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
- name: Fix executable permissions on binaries - name: Fix executable permissions on binaries
run: chmod +x serial-*/serial rayhunter-daemon/rayhunter-daemon run: chmod +x serial-*/serial rayhunter-check-*/rayhunter-check rayhunter-daemon/rayhunter-daemon
- name: Setup release directory - name: Setup release directory
run: mv rayhunter-daemon/rayhunter-daemon rootshell/rootshell serial-* dist run: mv rayhunter-daemon/rayhunter-daemon rootshell/rootshell serial-* dist
- name: Archive release directory - name: Archive release directory
Generated
+46 -61
View File
@@ -638,6 +638,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "exr" name = "exr"
version = "1.72.0" version = "1.72.0"
@@ -654,6 +664,12 @@ dependencies = [
"zune-inflate", "zune-inflate",
] ]
[[package]]
name = "fastrand"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]] [[package]]
name = "fdeflate" name = "fdeflate"
version = "0.3.4" version = "0.3.4"
@@ -697,12 +713,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]] [[package]]
name = "funty" name = "funty"
version = "2.0.0" version = "2.0.0"
@@ -1160,6 +1170,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.11" version = "0.4.11"
@@ -1545,19 +1561,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@@ -1566,7 +1569,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha",
"rand_core 0.6.4", "rand_core",
] ]
[[package]] [[package]]
@@ -1576,24 +1579,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core 0.6.4", "rand_core",
] ]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.6.4" version = "0.6.4"
@@ -1629,7 +1617,7 @@ dependencies = [
"once_cell", "once_cell",
"paste", "paste",
"profiling", "profiling",
"rand 0.8.5", "rand",
"rand_chacha", "rand_chacha",
"simd_helpers", "simd_helpers",
"system-deps", "system-deps",
@@ -1691,7 +1679,7 @@ dependencies = [
"rayhunter", "rayhunter",
"serde", "serde",
"serde_json", "serde_json",
"tempdir", "tempfile",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@@ -1719,15 +1707,6 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@@ -1766,15 +1745,6 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.37" version = "0.8.37"
@@ -1807,6 +1777,19 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.14" version = "1.0.14"
@@ -2031,13 +2014,15 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tempdir" name = "tempfile"
version = "0.3.7" version = "3.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [ dependencies = [
"rand 0.4.6", "cfg-if",
"remove_dir_all", "fastrand",
"rustix",
"windows-sys 0.52.0",
] ]
[[package]] [[package]]
+11 -6
View File
@@ -35,10 +35,11 @@ linux/qualcom devices but this is the only one we have tested on. Buy the orbic
## Setup ## Setup
1. Install the Android Debug Bridge (ADB) on your computer (don't worry about instructions for installing it on a phone/device yet). You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer). *NOTE: We don't currently support automated installs on windows, you will have to follow the manual install instructions below*
2. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it (on Windows use 7zip).
3. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`). 1. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it.
4. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below. 2. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`).
3. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
## Usage ## Usage
@@ -46,10 +47,14 @@ Once installed, rayhunter will run automatically whenever your Orbic device is r
1. Over wifi: Connect your phone/laptop to the Orbic's wifi network and visit `http://192.168.1.1:8080` (click past your browser warning you about the connection not being secure, rayhunter doesn't have HTTPS yet!) 1. Over wifi: Connect your phone/laptop to the Orbic's wifi network and visit `http://192.168.1.1:8080` (click past your browser warning you about the connection not being secure, rayhunter doesn't have HTTPS yet!)
* Note that you'll need the Orbic's wifi password for this, which can be retrieved by pressing the "MENU" button on the device and opening the 2.4 GHz menu. * Note that you'll need the Orbic's wifi password for this, which can be retrieved by pressing the "MENU" button on the device and opening the 2.4 GHz menu.
2. Over usb: Connect the Orbic device to your laptop via usb. Run `adb forward tcp:8080 tcp:8080`, then visit `http://localhost:8080`. 2. Over usb: Connect the Orbic device to your laptop via usb. Run `adb forward tcp:8080 tcp:8080`, then visit `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).
## Development ## Development
* Install ADB on your computer using the instructions above. * Install ADB on your computer using the instructions above, and make sure it's in your terminal's PATH
* You can verify if ADB is in your PATH by running `which adb` in a terminal. If it prints the filepath to where ADB is installed, you're set! Otherwise, try following one of these guides:
* [linux](https://askubuntu.com/questions/652936/adding-android-sdk-platform-tools-to-path-downloaded-from-umake)
* [macOS](https://www.repeato.app/setting-up-adb-on-macos-a-step-by-step-guide/)
* [Windows](https://medium.com/@yadav-ajay/a-step-by-step-guide-to-setting-up-adb-path-on-windows-0b833faebf18)
### If your are on x86 linux ### If your are on x86 linux
* on your linux laptop install rust the usual way and then install cross compiling dependences. * on your linux laptop install rust the usual way and then install cross compiling dependences.
+1 -1
View File
@@ -25,10 +25,10 @@ tokio-util = { version = "0.7.10", features = ["rt"] }
futures-macro = "0.3.30" futures-macro = "0.3.30"
include_dir = "0.7.3" include_dir = "0.7.3"
mime_guess = "2.0.4" mime_guess = "2.0.4"
tempdir = "0.3.7"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
tokio-stream = "0.1.14" tokio-stream = "0.1.14"
futures = "0.3.30" futures = "0.3.30"
clap = { version = "4.5.2", features = ["derive"] } clap = { version = "4.5.2", features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.114"
image = "0.25.1" image = "0.25.1"
tempfile = "3.10.1"
+248
View File
@@ -0,0 +1,248 @@
use std::sync::Arc;
use std::{future, pin};
use axum::Json;
use axum::{
extract::{Path, State},
http::StatusCode,
};
use futures::TryStreamExt;
use log::{debug, error, info};
use rayhunter::analysis::analyzer::Harness;
use rayhunter::diag::{DataType, MessagesContainer};
use rayhunter::qmdl::QmdlReader;
use serde::Serialize;
use tokio::fs::File;
use tokio::io::{AsyncWriteExt, BufWriter};
use tokio::sync::mpsc::Receiver;
use tokio::sync::{RwLock, RwLockWriteGuard};
use tokio_util::task::TaskTracker;
use crate::qmdl_store::RecordingStore;
use crate::server::ServerState;
use crate::dummy_analyzer::TestAnalyzer;
pub struct AnalysisWriter {
writer: BufWriter<File>,
harness: Harness,
bytes_written: usize,
}
// We write our analysis results to a file immediately to minimize the amount of
// state Rayhunter has to keep track of in memory. The analysis file's format is
// Newline Delimited JSON
// (https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson), which
// lets us simply append new rows to the end without parsing the entire JSON
// object beforehand.
impl AnalysisWriter {
pub async fn new(file: File, enable_dummy_analyzer: bool) -> Result<Self, std::io::Error> {
let mut harness = Harness::new_with_all_analyzers();
if enable_dummy_analyzer {
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
}
let mut result = Self {
writer: BufWriter::new(file),
bytes_written: 0,
harness,
};
let metadata = result.harness.get_metadata();
result.write(&metadata).await?;
Ok(result)
}
// Runs the analysis harness on the given container, serializing the results
// to the analysis file and returning the file's new length.
pub async fn analyze(&mut self, container: MessagesContainer) -> Result<(usize, bool), std::io::Error> {
let row = self.harness.analyze_qmdl_messages(container);
if !row.is_empty() {
self.write(&row).await?;
}
Ok((self.bytes_written, row.contains_warnings()))
}
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
let mut value_str = serde_json::to_string(value).unwrap();
value_str.push('\n');
self.bytes_written += value_str.len();
self.writer.write_all(value_str.as_bytes()).await?;
self.writer.flush().await?;
Ok(())
}
// Flushes any pending I/O to disk before dropping the writer
pub async fn close(mut self) -> Result<(), std::io::Error> {
self.writer.flush().await?;
Ok(())
}
}
#[derive(Debug, Serialize, Clone, Default)]
pub struct AnalysisStatus {
queued: Vec<String>,
running: Option<String>,
}
pub enum AnalysisCtrlMessage {
NewFilesQueued,
Exit,
}
async fn queued_len(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -> usize {
analysis_status_lock.read().await.queued.len()
}
async fn dequeue_to_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -> String {
let mut analysis_status = analysis_status_lock.write().await;
let name = analysis_status.queued.remove(0);
assert!(analysis_status.running.is_none());
analysis_status.running = Some(name.clone());
name
}
async fn clear_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) {
let mut analysis_status = analysis_status_lock.write().await;
analysis_status.running = None;
}
async fn perform_analysis(
name: &str,
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
enable_dummy_analyzer: bool,
) -> Result<(), String> {
info!("Opening QMDL and analysis file for {}...", name);
let (analysis_file, qmdl_file, entry_index) = {
let mut qmdl_store = qmdl_store_lock.write().await;
let (entry_index, _) = qmdl_store
.entry_for_name(&name)
.ok_or(format!("failed to find QMDL store entry for {}", name))?;
let analysis_file = qmdl_store
.clear_and_open_entry_analysis(entry_index)
.await
.map_err(|e| format!("{:?}", e))?;
let qmdl_file = qmdl_store
.open_entry_qmdl(entry_index)
.await
.map_err(|e| format!("{:?}", e))?;
(analysis_file, qmdl_file, entry_index)
};
let mut analysis_writer = AnalysisWriter::new(analysis_file, enable_dummy_analyzer)
.await
.map_err(|e| format!("{:?}", e))?;
let file_size = qmdl_file
.metadata()
.await
.expect("failed to get QMDL file metadata")
.len();
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
let mut qmdl_stream = pin::pin!(qmdl_reader
.as_stream()
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
info!("Starting analysis for {}...", name);
while let Some(container) = qmdl_stream
.try_next()
.await
.expect("failed getting QMDL container")
{
let (size_bytes, _) = analysis_writer
.analyze(container)
.await
.map_err(|e| format!("{:?}", e))?;
debug!("{} analysis: {} bytes written", name, size_bytes);
let mut qmdl_store = qmdl_store_lock.write().await;
qmdl_store
.update_entry_analysis_size(entry_index, size_bytes)
.await
.map_err(|e| format!("{:?}", e))?;
}
analysis_writer
.close()
.await
.map_err(|e| format!("{:?}", e))?;
info!("Analysis for {} complete!", name);
Ok(())
}
pub fn run_analysis_thread(
task_tracker: &TaskTracker,
mut analysis_rx: Receiver<AnalysisCtrlMessage>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
enable_dummy_analyzer: bool,
) {
task_tracker.spawn(async move {
loop {
match analysis_rx.recv().await {
Some(AnalysisCtrlMessage::NewFilesQueued) => {
let count = queued_len(analysis_status_lock.clone()).await;
for _ in 0..count {
let name = dequeue_to_running(analysis_status_lock.clone()).await;
if let Err(err) = perform_analysis(&name, qmdl_store_lock.clone(), enable_dummy_analyzer).await {
error!("failed to analyze {}: {}", name, err);
}
clear_running(analysis_status_lock.clone()).await;
}
}
Some(AnalysisCtrlMessage::Exit) | None => return,
}
}
});
}
pub async fn get_analysis_status(
State(state): State<Arc<ServerState>>,
) -> Result<Json<AnalysisStatus>, (StatusCode, String)> {
Ok(Json(state.analysis_status_lock.read().await.clone()))
}
fn queue_qmdl(name: &str, analysis_status: &mut RwLockWriteGuard<AnalysisStatus>) -> bool {
if analysis_status.queued.iter().any(|n| n == name)
|| analysis_status.running.iter().any(|n| n == name)
{
return false;
}
analysis_status.queued.push(name.to_string());
true
}
pub async fn start_analysis(
State(state): State<Arc<ServerState>>,
Path(qmdl_name): Path<String>,
) -> Result<(StatusCode, Json<AnalysisStatus>), (StatusCode, String)> {
let mut analysis_status = state.analysis_status_lock.write().await;
let store = state.qmdl_store_lock.read().await;
let queued = if qmdl_name.is_empty() {
let mut entry_names: Vec<&str> = store
.manifest
.entries
.iter()
.map(|e| e.name.as_str())
.collect();
if let Some(current_entry) = store.current_entry {
entry_names.remove(current_entry);
}
entry_names
.iter()
.any(|name| queue_qmdl(name, &mut analysis_status))
} else {
queue_qmdl(&qmdl_name, &mut analysis_status)
};
if queued {
state
.analysis_sender
.send(AnalysisCtrlMessage::NewFilesQueued)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to queue new analysis files: {:?}", e),
)
})?;
}
Ok((StatusCode::ACCEPTED, Json(analysis_status.clone())))
}
+98 -12
View File
@@ -1,14 +1,81 @@
use std::{future, path::PathBuf, pin::pin}; use std::{collections::HashMap, future, path::PathBuf, pin::pin};
use rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader}; use rayhunter::{analysis::analyzer::Harness, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlReader};
use tokio::fs::File; use tokio::fs::{metadata, read_dir, File};
use clap::Parser; use clap::Parser;
use futures::TryStreamExt; use futures::TryStreamExt;
mod dummy_analyzer;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about)] #[command(version, about)]
struct Args { struct Args {
#[arg(short, long)] #[arg(short, long)]
qmdl_path: PathBuf, qmdl_path: PathBuf,
#[arg(short, long)]
pcapify: bool,
#[arg(long)]
show_skipped: bool,
#[arg(long)]
enable_dummy_analyzer: bool,
}
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
let file_size = qmdl_file.metadata().await.expect("failed to get QMDL file metadata").len();
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
let mut skipped_reasons: HashMap<String, i32> = HashMap::new();
let mut total_messages = 0;
let mut warnings = 0;
let mut skipped = 0;
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") {
let row = harness.analyze_qmdl_messages(container);
total_messages += 1;
for reason in row.skipped_message_reasons {
*skipped_reasons.entry(reason).or_insert(0) += 1;
skipped += 1;
}
for analysis in row.analysis {
for maybe_event in analysis.events {
if let Some(event) = maybe_event {
warnings += 1;
println!("{}: {:?}", analysis.timestamp, event);
}
}
}
}
if show_skipped && skipped > 0 {
println!("{}: messages skipped:", qmdl_path);
for (reason, count) in skipped_reasons.iter() {
println!(" - {}: \"{}\"", count, reason);
}
}
println!("{}: {} messages analyzed, {} warnings, {} messages skipped", qmdl_path, total_messages, warnings, skipped);
}
async fn pcapify(qmdl_path: &PathBuf) {
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open qmdl file");
let qmdl_file_size = qmdl_file.metadata().await.unwrap().len();
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize));
let mut pcap_path = qmdl_path.clone();
pcap_path.set_extension("pcap");
let pcap_file = &mut File::create(&pcap_path).await.expect("failed to open pcap file");
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
pcap_writer.write_iface_header().await.unwrap();
while let Some(container) = qmdl_reader.get_next_messages_container().await.expect("failed to get container") {
for maybe_msg in container.into_messages() {
if let Ok(msg) = maybe_msg {
if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) {
pcap_writer.write_gsmtap_message(parsed, timestamp).await.expect("failed to write");
}
}
}
}
println!("wrote pcap to {:?}", &pcap_path);
} }
#[tokio::main] #[tokio::main]
@@ -17,15 +84,34 @@ async fn main() {
let args = Args::parse(); let args = Args::parse();
let mut harness = Harness::new_with_all_analyzers(); let mut harness = Harness::new_with_all_analyzers();
if args.enable_dummy_analyzer {
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
}
println!("Analyzers:");
for analyzer in harness.get_metadata().analyzers {
println!(" - {}: {}", analyzer.name, analyzer.description);
}
let qmdl_file = File::open(args.qmdl_path).await.expect("failed to open QMDL file"); let metadata = metadata(&args.qmdl_path).await.expect("failed to get metadata");
let file_size = qmdl_file.metadata().await.expect("failed to get QMDL file metadata").len(); if metadata.is_dir() {
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize)); let mut dir = read_dir(&args.qmdl_path).await.expect("failed to read dir");
let mut qmdl_stream = pin!(qmdl_reader.as_stream() while let Some(entry) = dir.next_entry().await.expect("failed to get entry") {
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace))); let name = entry.file_name();
println!("{}\n", serde_json::to_string(&harness.get_metadata()).expect("failed to serialize report metadata")); let name_str = name.to_str().unwrap();
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") { if name_str.ends_with(".qmdl") {
let row = harness.analyze_qmdl_messages(container); let path = entry.path();
println!("{}\n", serde_json::to_string(&row).expect("failed to serialize row")); let path_str = path.to_str().unwrap();
analyze_file(&mut harness, path_str, args.show_skipped).await;
if args.pcapify {
pcapify(&path).await;
}
}
}
} else {
let path = args.qmdl_path.to_str().unwrap();
analyze_file(&mut harness, path, args.show_skipped).await;
if args.pcapify {
pcapify(&args.qmdl_path).await;
}
} }
} }
+15 -7
View File
@@ -6,16 +6,20 @@ use serde::Deserialize;
struct ConfigFile { struct ConfigFile {
qmdl_store_path: Option<String>, qmdl_store_path: Option<String>,
port: Option<u16>, port: Option<u16>,
readonly_mode: Option<bool>, debug_mode: Option<bool>,
ui_level: Option<u8>, ui_level: Option<u8>,
enable_dummy_analyzer: Option<bool>,
colorblind_mode: Option<bool>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub qmdl_store_path: String, pub qmdl_store_path: String,
pub port: u16, pub port: u16,
pub readonly_mode: bool, pub debug_mode: bool,
pub ui_level: u8, pub ui_level: u8,
pub enable_dummy_analyzer: bool,
pub colorblind_mode: bool,
} }
impl Default for Config { impl Default for Config {
@@ -23,8 +27,10 @@ impl Default for Config {
Config { Config {
qmdl_store_path: "/data/rayhunter/qmdl".to_string(), qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
port: 8080, port: 8080,
readonly_mode: false, debug_mode: false,
ui_level: 1, ui_level: 1,
enable_dummy_analyzer: false,
colorblind_mode: false,
} }
} }
} }
@@ -34,10 +40,12 @@ pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef
if let Ok(config_file) = std::fs::read_to_string(&path) { if let Ok(config_file) = std::fs::read_to_string(&path) {
let parsed_config: ConfigFile = toml::from_str(&config_file) let parsed_config: ConfigFile = toml::from_str(&config_file)
.map_err(RayhunterError::ConfigFileParsingError)?; .map_err(RayhunterError::ConfigFileParsingError)?;
if let Some(path) = parsed_config.qmdl_store_path { config.qmdl_store_path = path } parsed_config.qmdl_store_path.map(|v| config.qmdl_store_path = v);
if let Some(port) = parsed_config.port { config.port = port } parsed_config.port.map(|v| config.port = v);
if let Some(readonly_mode) = parsed_config.readonly_mode { config.readonly_mode = readonly_mode } parsed_config.debug_mode.map(|v| config.debug_mode = v);
if let Some(ui_level) = parsed_config.ui_level { config.ui_level = ui_level } parsed_config.ui_level.map(|v| config.ui_level = v);
parsed_config.enable_dummy_analyzer.map(|v| config.enable_dummy_analyzer = v);
parsed_config.colorblind_mode.map(|v| config.colorblind_mode = v);
} }
Ok(config) Ok(config)
} }
+65 -23
View File
@@ -1,3 +1,4 @@
mod analysis;
mod config; mod config;
mod error; mod error;
mod pcap; mod pcap;
@@ -6,6 +7,7 @@ mod stats;
mod qmdl_store; mod qmdl_store;
mod diag; mod diag;
mod framebuffer; mod framebuffer;
mod dummy_analyzer;
use crate::config::{parse_config, parse_args}; use crate::config::{parse_config, parse_args};
use crate::diag::run_diag_read_thread; use crate::diag::run_diag_read_thread;
@@ -16,6 +18,7 @@ use crate::stats::get_system_stats;
use crate::error::RayhunterError; use crate::error::RayhunterError;
use crate::framebuffer::Framebuffer; use crate::framebuffer::Framebuffer;
use analysis::{get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus};
use axum::response::Redirect; use axum::response::Redirect;
use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage}; use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage};
use log::{info, error}; use log::{info, error};
@@ -23,7 +26,7 @@ use rayhunter::diag_device::DiagDevice;
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::Router; use axum::Router;
use stats::get_qmdl_manifest; use stats::get_qmdl_manifest;
use tokio::sync::mpsc::{self, Sender}; use tokio::sync::mpsc::{self, Sender, Receiver};
use tokio::sync::oneshot::error::TryRecvError; use tokio::sync::oneshot::error::TryRecvError;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
@@ -43,12 +46,20 @@ async fn run_server(
config: &config::Config, config: &config::Config,
qmdl_store_lock: Arc<RwLock<RecordingStore>>, qmdl_store_lock: Arc<RwLock<RecordingStore>>,
server_shutdown_rx: oneshot::Receiver<()>, server_shutdown_rx: oneshot::Receiver<()>,
diag_device_sender: Sender<DiagDeviceCtrlMessage> ui_update_tx: Sender<framebuffer::DisplayState>,
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
analysis_sender: Sender<AnalysisCtrlMessage>,
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
) -> JoinHandle<()> { ) -> JoinHandle<()> {
info!("spinning up server");
let state = Arc::new(ServerState { let state = Arc::new(ServerState {
qmdl_store_lock, qmdl_store_lock,
diag_device_ctrl_sender: diag_device_sender, diag_device_ctrl_sender: diag_device_sender,
readonly_mode: config.readonly_mode ui_update_sender: ui_update_tx,
debug_mode: config.debug_mode,
analysis_status_lock,
analysis_sender,
colorblind_mode: config.colorblind_mode,
}); });
let app = Router::new() let app = Router::new()
@@ -58,7 +69,9 @@ async fn run_server(
.route("/api/qmdl-manifest", get(get_qmdl_manifest)) .route("/api/qmdl-manifest", get(get_qmdl_manifest))
.route("/api/start-recording", post(start_recording)) .route("/api/start-recording", post(start_recording))
.route("/api/stop-recording", post(stop_recording)) .route("/api/stop-recording", post(stop_recording))
.route("/api/analysis-report", get(get_analysis_report)) .route("/api/analysis-report/*name", get(get_analysis_report))
.route("/api/analysis", get(get_analysis_status))
.route("/api/analysis/*name", post(start_analysis))
.route("/", get(|| async { Redirect::permanent("/index.html") })) .route("/", get(|| async { Redirect::permanent("/index.html") }))
.route("/*path", get(serve_static)) .route("/*path", get(serve_static))
.with_state(state); .with_state(state);
@@ -78,12 +91,12 @@ async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
} }
// Loads a QmdlStore if one exists, and if not, only create one if we're not in // Loads a QmdlStore if one exists, and if not, only create one if we're not in
// readonly mode. // debug mode.
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> { async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
match (RecordingStore::exists(&config.qmdl_store_path).await?, config.readonly_mode) { match (RecordingStore::exists(&config.qmdl_store_path).await?, config.debug_mode) {
(true, _) => Ok(RecordingStore::load(&config.qmdl_store_path).await?), (true, _) => Ok(RecordingStore::load(&config.qmdl_store_path).await?),
(false, false) => Ok(RecordingStore::create(&config.qmdl_store_path).await?), (false, false) => Ok(RecordingStore::create(&config.qmdl_store_path).await?),
(false, true) => Err(RayhunterError::NoStoreReadonlyMode(config.qmdl_store_path.clone())), (false, true) => Err(RayhunterError::NoStoreDebugMode(config.qmdl_store_path.clone())),
} }
} }
@@ -94,8 +107,9 @@ fn run_ctrl_c_thread(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
diag_device_sender: Sender<DiagDeviceCtrlMessage>, diag_device_sender: Sender<DiagDeviceCtrlMessage>,
server_shutdown_tx: oneshot::Sender<()>, server_shutdown_tx: oneshot::Sender<()>,
ui_shutdown_tx: oneshot::Sender<()>, maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>,
qmdl_store_lock: Arc<RwLock<RecordingStore>> qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_tx: Sender<AnalysisCtrlMessage>,
) -> JoinHandle<Result<(), RayhunterError>> { ) -> JoinHandle<Result<(), RayhunterError>> {
task_tracker.spawn(async move { task_tracker.spawn(async move {
match tokio::signal::ctrl_c().await { match tokio::signal::ctrl_c().await {
@@ -110,10 +124,14 @@ fn run_ctrl_c_thread(
server_shutdown_tx.send(()) server_shutdown_tx.send(())
.expect("couldn't send server shutdown signal"); .expect("couldn't send server shutdown signal");
info!("sending UI shutdown"); info!("sending UI shutdown");
ui_shutdown_tx.send(()) if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
.expect("couldn't send ui shutdown signal"); ui_shutdown_tx.send(())
.expect("couldn't send ui shutdown signal");
}
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
.expect("couldn't send Exit message to diag thread"); .expect("couldn't send Exit message to diag thread");
analysis_tx.send(AnalysisCtrlMessage::Exit).await
.expect("couldn't send Exit message to analysis thread");
}, },
Err(err) => { Err(err) => {
error!("Unable to listen for shutdown signal: {}", err); error!("Unable to listen for shutdown signal: {}", err);
@@ -123,13 +141,20 @@ fn run_ctrl_c_thread(
}) })
} }
async fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>){ fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>, mut ui_update_rx: Receiver<framebuffer::DisplayState>) -> JoinHandle<()> {
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/"); static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
let mut display_color: framebuffer::Color565;
let display_level = config.ui_level; let display_level = config.ui_level;
if display_level == 0 { if display_level == 0 {
info!("Invisible mode, not spawning UI."); info!("Invisible mode, not spawning UI.");
} }
if config.colorblind_mode {
display_color = framebuffer::Color565::Blue;
} else {
display_color = framebuffer::Color565::Green;
}
task_tracker.spawn_blocking(move || { task_tracker.spawn_blocking(move || {
let mut fb: Framebuffer = Framebuffer::new(); let mut fb: Framebuffer = Framebuffer::new();
// this feels wrong, is there a more rusty way to do this? // this feels wrong, is there a more rusty way to do this?
@@ -147,8 +172,15 @@ async fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_
}, },
Err(TryRecvError::Empty) => {}, Err(TryRecvError::Empty) => {},
Err(e) => panic!("error receiving shutdown message: {e}") Err(e) => panic!("error receiving shutdown message: {e}")
} }
match ui_update_rx.try_recv() {
Ok(state) => {
display_color = state.into();
},
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {},
Err(e) => error!("error receiving framebuffer update message: {e}")
}
match display_level { match display_level {
2 => { 2 => {
fb.draw_gif(img.unwrap()); fb.draw_gif(img.unwrap());
@@ -164,13 +196,12 @@ async fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_
fb.draw_line(framebuffer::Color565::Cyan, 25); fb.draw_line(framebuffer::Color565::Cyan, 25);
}, },
1 | _ => { 1 | _ => {
fb.draw_line(framebuffer::Color565::Green, 2); fb.draw_line(display_color, 2);
}, },
}; };
sleep(Duration::from_millis(100)); sleep(Duration::from_millis(1000));
} }
}).await.unwrap(); })
} }
#[tokio::main] #[tokio::main]
@@ -183,25 +214,36 @@ async fn main() -> Result<(), RayhunterError> {
// TaskTrackers give us an interface to spawn tokio threads, and then // TaskTrackers give us an interface to spawn tokio threads, and then
// eventually await all of them ending // eventually await all of them ending
let task_tracker = TaskTracker::new(); let task_tracker = TaskTracker::new();
println!("R A Y H U N T E R 🐳");
let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?)); let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1); let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
if !config.readonly_mode { let (ui_update_tx, ui_update_rx) = mpsc::channel::<framebuffer::DisplayState>(1);
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
let mut maybe_ui_shutdown_tx = None;
if !config.debug_mode {
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
let mut dev = DiagDevice::new().await let mut dev = DiagDevice::new().await
.map_err(RayhunterError::DiagInitError)?; .map_err(RayhunterError::DiagInitError)?;
dev.config_logs().await dev.config_logs().await
.map_err(RayhunterError::DiagInitError)?; .map_err(RayhunterError::DiagInitError)?;
run_diag_read_thread(&task_tracker, dev, rx, qmdl_store_lock.clone()); info!("Starting Diag Thread");
run_diag_read_thread(&task_tracker, dev, rx, ui_update_tx.clone(), qmdl_store_lock.clone(), config.enable_dummy_analyzer);
info!("Starting UI");
update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
} }
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>(); let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, ui_shutdown_tx, qmdl_store_lock.clone()); info!("create shutdown thread");
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx).await; let analysis_status_lock = Arc::new(RwLock::new(AnalysisStatus::default()));
update_ui(&task_tracker, &config, ui_shutdown_rx).await; run_analysis_thread(&task_tracker, analysis_rx, qmdl_store_lock.clone(), analysis_status_lock.clone(), config.enable_dummy_analyzer);
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, maybe_ui_shutdown_tx, qmdl_store_lock.clone(), analysis_tx.clone());
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, ui_update_tx, tx, analysis_tx, analysis_status_lock).await;
task_tracker.close(); task_tracker.close();
task_tracker.wait().await; task_tracker.wait().await;
info!("see you space cowboy...");
Ok(()) Ok(())
} }
+43 -69
View File
@@ -2,26 +2,25 @@ use std::pin::pin;
use std::sync::Arc; use std::sync::Arc;
use axum::body::Body; use axum::body::Body;
use axum::extract::State; use axum::extract::{Path, State};
use axum::http::header::CONTENT_TYPE; use axum::http::header::CONTENT_TYPE;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use rayhunter::analysis::analyzer::Harness; use rayhunter::diag::DataType;
use rayhunter::diag::{DataType, MessagesContainer};
use rayhunter::diag_device::DiagDevice; use rayhunter::diag_device::DiagDevice;
use serde::Serialize;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio::sync::mpsc::Receiver; use tokio::sync::mpsc::{Receiver, Sender};
use rayhunter::qmdl::QmdlWriter; use rayhunter::qmdl::QmdlWriter;
use log::{debug, error, info}; use log::{debug, error, info};
use tokio::fs::File; use tokio::fs::File;
use tokio::io::{BufWriter, AsyncWriteExt};
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use crate::framebuffer;
use crate::qmdl_store::RecordingStore; use crate::qmdl_store::RecordingStore;
use crate::server::ServerState; use crate::server::ServerState;
use crate::analysis::AnalysisWriter;
pub enum DiagDeviceCtrlMessage { pub enum DiagDeviceCtrlMessage {
StopRecording, StopRecording,
@@ -29,67 +28,19 @@ pub enum DiagDeviceCtrlMessage {
Exit, Exit,
} }
struct AnalysisWriter {
writer: BufWriter<File>,
harness: Harness,
bytes_written: usize,
}
// We write our analysis results to a file immediately to minimize the amount of
// state Rayhunter has to keep track of in memory. The analysis file's format is
// Newline Delimited JSON
// (https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson), which
// lets us simply append new rows to the end without parsing the entire JSON
// object beforehand.
impl AnalysisWriter {
pub async fn new(file: File) -> Result<Self, std::io::Error> {
let mut result = Self {
writer: BufWriter::new(file),
harness: Harness::new_with_all_analyzers(),
bytes_written: 0,
};
let metadata = result.harness.get_metadata();
result.write(&metadata).await?;
Ok(result)
}
// Runs the analysis harness on the given container, serializing the results
// to the analysis file and returning the file's new length.
pub async fn analyze(&mut self, container: MessagesContainer) -> Result<usize, std::io::Error> {
let row = self.harness.analyze_qmdl_messages(container);
if !row.is_empty() {
self.write(&row).await?;
}
Ok(self.bytes_written)
}
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
let mut value_str = serde_json::to_string(value).unwrap();
value_str.push('\n');
self.bytes_written += value_str.len();
self.writer.write_all(value_str.as_bytes()).await?;
self.writer.flush().await?;
Ok(())
}
// Flushes any pending I/O to disk before dropping the writer
pub async fn close(mut self) -> Result<(), std::io::Error> {
self.writer.flush().await?;
Ok(())
}
}
pub fn run_diag_read_thread( pub fn run_diag_read_thread(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
mut dev: DiagDevice, mut dev: DiagDevice,
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>, mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
qmdl_store_lock: Arc<RwLock<RecordingStore>> ui_update_sender: Sender<framebuffer::DisplayState>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
enable_dummy_analyzer: bool,
) { ) {
task_tracker.spawn(async move { task_tracker.spawn(async move {
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry"); let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file)); let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
let mut diag_stream = pin!(dev.as_stream().into_stream()); let mut diag_stream = pin!(dev.as_stream().into_stream());
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file).await let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer).await
.expect("failed to create analysis writer")); .expect("failed to create analysis writer"));
loop { loop {
tokio::select! { tokio::select! {
@@ -100,7 +51,7 @@ pub fn run_diag_read_thread(
if let Some(analysis_writer) = maybe_analysis_writer { if let Some(analysis_writer) = maybe_analysis_writer {
analysis_writer.close().await.expect("failed to close analysis writer"); analysis_writer.close().await.expect("failed to close analysis writer");
} }
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file).await maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
.expect("failed to write to analysis file")); .expect("failed to write to analysis file"));
}, },
Some(DiagDeviceCtrlMessage::StopRecording) => { Some(DiagDeviceCtrlMessage::StopRecording) => {
@@ -143,8 +94,14 @@ pub fn run_diag_read_thread(
} }
if let Some(analysis_writer) = maybe_analysis_writer.as_mut() { if let Some(analysis_writer) = maybe_analysis_writer.as_mut() {
let analysis_file_len = analysis_writer.analyze(container).await let analysis_output = analysis_writer.analyze(container).await
.expect("failed to analyze container"); .expect("failed to analyze container");
let (analysis_file_len, heuristic_warning) = analysis_output;
if heuristic_warning {
info!("a heuristic triggered on this run!");
ui_update_sender.send(framebuffer::DisplayState::WarningDetected).await
.expect("couldn't send ui update message: {}");
}
let mut qmdl_store = qmdl_store_lock.write().await; let mut qmdl_store = qmdl_store_lock.write().await;
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???"); let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
qmdl_store.update_entry_analysis_size(index, analysis_file_len as usize).await qmdl_store.update_entry_analysis_size(index, analysis_file_len as usize).await
@@ -163,8 +120,8 @@ pub fn run_diag_read_thread(
} }
pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> { pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.readonly_mode { if state.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
let mut qmdl_store = state.qmdl_store_lock.write().await; let mut qmdl_store = state.qmdl_store_lock.write().await;
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await let (qmdl_file, analysis_file) = qmdl_store.new_entry().await
@@ -172,30 +129,47 @@ pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(S
let qmdl_writer = QmdlWriter::new(qmdl_file); let qmdl_writer = QmdlWriter::new(qmdl_file);
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
let display_state: framebuffer::DisplayState;
if state.colorblind_mode {
display_state = framebuffer::DisplayState::RecordingCBM;
} else {
display_state = framebuffer::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)))?;
Ok((StatusCode::ACCEPTED, "ok".to_string())) Ok((StatusCode::ACCEPTED, "ok".to_string()))
} }
pub async fn stop_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> { pub async fn stop_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.readonly_mode { if state.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_string())); return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
} }
let mut qmdl_store = state.qmdl_store_lock.write().await; let mut qmdl_store = state.qmdl_store_lock.write().await;
qmdl_store.close_current_entry().await qmdl_store.close_current_entry().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't close current qmdl entry: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't close current qmdl entry: {}", e)))?;
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StopRecording).await state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StopRecording).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
state.ui_update_sender.send(framebuffer::DisplayState::Paused).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send ui update message: {}", e)))?;
Ok((StatusCode::ACCEPTED, "ok".to_string())) Ok((StatusCode::ACCEPTED, "ok".to_string()))
} }
pub async fn get_analysis_report(State(state): State<Arc<ServerState>>) -> Result<Response, (StatusCode, String)> { pub async fn get_analysis_report(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await; let qmdl_store = state.qmdl_store_lock.read().await;
let Some(entry) = qmdl_store.get_current_entry() else { let (entry_index, _) = if qmdl_name == "live" {
return Err(( qmdl_store.get_current_entry().ok_or((
StatusCode::SERVICE_UNAVAILABLE, StatusCode::SERVICE_UNAVAILABLE,
"No QMDL data's being recorded to analyze, try starting a new recording!".to_string() "No QMDL data's being recorded to analyze, try starting a new recording!".to_string()
)); ))?
} else {
qmdl_store.entry_for_name(&qmdl_name).ok_or((
StatusCode::NOT_FOUND,
format!("Couldn't find QMDL entry with name \"{}\"", qmdl_name)
))?
}; };
let analysis_file = qmdl_store.open_entry_analysis(entry).await let analysis_file = qmdl_store.open_entry_analysis(entry_index).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
let analysis_stream = ReaderStream::new(analysis_file); let analysis_stream = ReaderStream::new(analysis_file);
+45
View File
@@ -0,0 +1,45 @@
use std::borrow::Cow;
use rayhunter::telcom_parser::lte_rrc::{PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Identity};
use rayhunter::analysis::analyzer::{Analyzer, Event, EventType, Severity};
use rayhunter::analysis::information_element::{InformationElement, LteInformationElement};
pub struct TestAnalyzer{
pub count: i32,
}
impl Analyzer for TestAnalyzer{
fn get_name(&self) -> Cow<str> {
Cow::from("Example Analyzer")
}
fn get_description(&self) -> Cow<str> {
Cow::from("Always returns true, if you are seeing this you are either a developer or you are about to have problems.")
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
self.count += 1;
if self.count % 100 == 0 {
return Some(Event {
event_type: EventType::Informational ,
message: "multiple of 100 events processed".to_string(),
})
}
let InformationElement::LTE(LteInformationElement::PCCH(pcch_msg)) = ie else {
return None;
};
let PCCH_MessageType::C1(PCCH_MessageType_c1::Paging(paging)) = &pcch_msg.message else {
return None;
};
for record in &paging.paging_record_list.as_ref()?.0 {
if let PagingUE_Identity::S_TMSI(_) = record.ue_identity {
return Some(Event {
event_type: EventType::QualitativeWarning { severity: Severity::Low },
message: "TMSI was provided to cell".to_string(),
})
}
}
None
}
}
+2 -2
View File
@@ -13,6 +13,6 @@ pub enum RayhunterError{
TokioError(#[from] tokio::io::Error), TokioError(#[from] tokio::io::Error),
#[error("QmdlStore error: {0}")] #[error("QmdlStore error: {0}")]
QmdlStoreError(#[from] RecordingStoreError), QmdlStoreError(#[from] RecordingStoreError),
#[error("No QMDL store found at path {0}, but can't create a new one due to readonly mode")] #[error("No QMDL store found at path {0}, but can't create a new one due to debug mode")]
NoStoreReadonlyMode(String), NoStoreDebugMode(String),
} }
+19
View File
@@ -11,6 +11,7 @@ struct Dimensions {
} }
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Copy, Clone)]
pub enum Color565 { pub enum Color565 {
Red = 0b1111100000000000, Red = 0b1111100000000000,
Green = 0b0000011111100000, Green = 0b0000011111100000,
@@ -22,6 +23,24 @@ pub enum Color565 {
Pink = 0b1111010010011111, Pink = 0b1111010010011111,
} }
pub enum DisplayState {
Recording,
Paused,
WarningDetected,
RecordingCBM,
}
impl From<DisplayState> for Color565 {
fn from(state: DisplayState) -> Self {
match state {
DisplayState::Paused => Color565::White,
DisplayState::Recording => Color565::Green,
DisplayState::RecordingCBM => Color565::Blue,
DisplayState::WarningDetected => Color565::Red,
}
}
}
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Framebuffer<'a> { pub struct Framebuffer<'a> {
dimensions: Dimensions, dimensions: Dimensions,
+4 -4
View File
@@ -21,7 +21,7 @@ use futures::TryStreamExt;
// pcap data to a channel that's piped to the client. // pcap data to a channel that's piped to the client.
pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> { pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await; let qmdl_store = state.qmdl_store_lock.read().await;
let entry = qmdl_store.entry_for_name(&qmdl_name) let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name)
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?; .ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
if entry.qmdl_size_bytes == 0 { if entry.qmdl_size_bytes == 0 {
return Err(( return Err((
@@ -29,8 +29,8 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
"QMDL file is empty, try again in a bit!".to_string() "QMDL file is empty, try again in a bit!".to_string()
)); ));
} }
let qmdl_size_bytes = entry.qmdl_size_bytes;
let qmdl_file = qmdl_store.open_entry_qmdl(&entry).await let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
// the QMDL reader should stop at the last successfully written data chunk // the QMDL reader should stop at the last successfully written data chunk
// (entry.size_bytes) // (entry.size_bytes)
@@ -39,7 +39,7 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
pcap_writer.write_iface_header().await.unwrap(); pcap_writer.write_iface_header().await.unwrap();
tokio::spawn(async move { tokio::spawn(async move {
let mut reader = QmdlReader::new(qmdl_file, Some(entry.qmdl_size_bytes)); let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
let mut messages_stream = pin!(reader.as_stream() let mut messages_stream = pin!(reader.as_stream()
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace))); .try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
+136 -50
View File
@@ -1,8 +1,11 @@
use std::path::{PathBuf, Path};
use thiserror::Error;
use tokio::{fs::{self, File, try_exists}, io::AsyncWriteExt};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thiserror::Error;
use tokio::{
fs::{self, try_exists, File, OpenOptions},
io::AsyncWriteExt,
};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum RecordingStoreError { pub enum RecordingStoreError {
@@ -19,7 +22,7 @@ pub enum RecordingStoreError {
#[error("Couldn't write manifest file: {0}")] #[error("Couldn't write manifest file: {0}")]
WriteManifestError(tokio::io::Error), WriteManifestError(tokio::io::Error),
#[error("Couldn't parse QMDL store manifest file: {0}")] #[error("Couldn't parse QMDL store manifest file: {0}")]
ParseManifestError(toml::de::Error) ParseManifestError(toml::de::Error),
} }
pub struct RecordingStore { pub struct RecordingStore {
@@ -70,16 +73,26 @@ impl ManifestEntry {
impl RecordingStore { impl RecordingStore {
// Returns whether a directory with a "manifest.toml" exists at the given // Returns whether a directory with a "manifest.toml" exists at the given
// path (though doesn't check if that manifest is valid) // path (though doesn't check if that manifest is valid)
pub async fn exists<P>(path: P) -> Result<bool, RecordingStoreError> where P: AsRef<Path> { pub async fn exists<P>(path: P) -> Result<bool, RecordingStoreError>
where
P: AsRef<Path>,
{
let manifest_path = path.as_ref().join("manifest.toml"); let manifest_path = path.as_ref().join("manifest.toml");
let dir_exists = try_exists(path).await.map_err(RecordingStoreError::OpenDirError)?; let dir_exists = try_exists(path)
let manifest_exists = try_exists(manifest_path).await.map_err(RecordingStoreError::ReadManifestError)?; .await
.map_err(RecordingStoreError::OpenDirError)?;
let manifest_exists = try_exists(manifest_path)
.await
.map_err(RecordingStoreError::ReadManifestError)?;
Ok(dir_exists && manifest_exists) Ok(dir_exists && manifest_exists)
} }
// Loads an existing RecordingStore at the given path. Errors if no store exists, // Loads an existing RecordingStore at the given path. Errors if no store exists,
// or if it's malformed. // or if it's malformed.
pub async fn load<P>(path: P) -> Result<Self, RecordingStoreError> where P: AsRef<Path> { pub async fn load<P>(path: P) -> Result<Self, RecordingStoreError>
where
P: AsRef<Path>,
{
let path: PathBuf = path.as_ref().to_path_buf(); let path: PathBuf = path.as_ref().to_path_buf();
let manifest = RecordingStore::read_manifest(&path).await?; let manifest = RecordingStore::read_manifest(&path).await?;
Ok(RecordingStore { Ok(RecordingStore {
@@ -91,26 +104,38 @@ impl RecordingStore {
// Creates a new RecordingStore at the given path. This involves creating a dir // Creates a new RecordingStore at the given path. This involves creating a dir
// and writing an empty manifest. // and writing an empty manifest.
pub async fn create<P>(path: P) -> Result<Self, RecordingStoreError> where P: AsRef<Path> { pub async fn create<P>(path: P) -> Result<Self, RecordingStoreError>
where
P: AsRef<Path>,
{
let manifest_path = path.as_ref().join("manifest.toml"); let manifest_path = path.as_ref().join("manifest.toml");
fs::create_dir_all(&path).await fs::create_dir_all(&path)
.await
.map_err(RecordingStoreError::OpenDirError)?; .map_err(RecordingStoreError::OpenDirError)?;
let mut manifest_file = File::create(&manifest_path).await let mut manifest_file = File::create(&manifest_path)
.await
.map_err(RecordingStoreError::WriteManifestError)?; .map_err(RecordingStoreError::WriteManifestError)?;
let empty_manifest = Manifest { entries: Vec::new() }; let empty_manifest = Manifest {
let empty_manifest_contents = toml::to_string_pretty(&empty_manifest) entries: Vec::new(),
.expect("failed to serialize manifest"); };
manifest_file.write_all(empty_manifest_contents.as_bytes()).await let empty_manifest_contents =
toml::to_string_pretty(&empty_manifest).expect("failed to serialize manifest");
manifest_file
.write_all(empty_manifest_contents.as_bytes())
.await
.map_err(RecordingStoreError::WriteManifestError)?; .map_err(RecordingStoreError::WriteManifestError)?;
RecordingStore::load(path).await RecordingStore::load(path).await
} }
async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError> where P: AsRef<Path> { async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError>
where
P: AsRef<Path>,
{
let manifest_path = path.as_ref().join("manifest.toml"); let manifest_path = path.as_ref().join("manifest.toml");
let file_contents = fs::read_to_string(&manifest_path).await let file_contents = fs::read_to_string(&manifest_path)
.await
.map_err(RecordingStoreError::ReadManifestError)?; .map_err(RecordingStoreError::ReadManifestError)?;
toml::from_str(&file_contents) toml::from_str(&file_contents).map_err(RecordingStoreError::ParseManifestError)
.map_err(RecordingStoreError::ParseManifestError)
} }
// Closes the current entry (if needed), creates a new entry based on the // Closes the current entry (if needed), creates a new entry based on the
@@ -126,13 +151,15 @@ impl RecordingStore {
let qmdl_file = File::options() let qmdl_file = File::options()
.create(true) .create(true)
.write(true) .write(true)
.open(&qmdl_filepath).await .open(&qmdl_filepath)
.await
.map_err(RecordingStoreError::CreateFileError)?; .map_err(RecordingStoreError::CreateFileError)?;
let analysis_filepath = new_entry.get_analysis_filepath(&self.path); let analysis_filepath = new_entry.get_analysis_filepath(&self.path);
let analysis_file = File::options() let analysis_file = File::options()
.create(true) .create(true)
.write(true) .write(true)
.open(&analysis_filepath).await .open(&analysis_filepath)
.await
.map_err(RecordingStoreError::CreateFileError)?; .map_err(RecordingStoreError::CreateFileError)?;
self.manifest.entries.push(new_entry); self.manifest.entries.push(new_entry);
self.current_entry = Some(self.manifest.entries.len() - 1); self.current_entry = Some(self.manifest.entries.len() - 1);
@@ -141,37 +168,71 @@ impl RecordingStore {
} }
// Returns the corresponding QMDL file for a given entry // Returns the corresponding QMDL file for a given entry
pub async fn open_entry_qmdl(&self, entry: &ManifestEntry) -> Result<File, RecordingStoreError> { pub async fn open_entry_qmdl(
File::open(entry.get_qmdl_filepath(&self.path)).await &self,
entry_index: usize,
) -> Result<File, RecordingStoreError> {
let entry = &self.manifest.entries[entry_index];
File::open(entry.get_qmdl_filepath(&self.path))
.await
.map_err(RecordingStoreError::ReadFileError) .map_err(RecordingStoreError::ReadFileError)
} }
// Returns the corresponding QMDL file for a given entry // Returns the corresponding QMDL file for a given entry
pub async fn open_entry_analysis(&self, entry: &ManifestEntry) -> Result<File, RecordingStoreError> { pub async fn open_entry_analysis(
File::open(entry.get_analysis_filepath(&self.path)).await &self,
entry_index: usize,
) -> Result<File, RecordingStoreError> {
let entry = &self.manifest.entries[entry_index];
File::open(entry.get_analysis_filepath(&self.path))
.await
.map_err(RecordingStoreError::ReadFileError) .map_err(RecordingStoreError::ReadFileError)
} }
pub async fn clear_and_open_entry_analysis(
&mut self,
entry_index: usize,
) -> Result<File, RecordingStoreError> {
let entry = &self.manifest.entries[entry_index];
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(entry.get_analysis_filepath(&self.path))
.await
.map_err(RecordingStoreError::ReadFileError)?;
self.update_entry_analysis_size(entry_index, 0)
.await?;
Ok(file)
}
// Unsets the current entry // Unsets the current entry
pub async fn close_current_entry(&mut self) -> Result<(), RecordingStoreError> { pub async fn close_current_entry(&mut self) -> Result<(), RecordingStoreError> {
match self.current_entry { match self.current_entry {
Some(_) => { Some(_) => {
self.current_entry = None; self.current_entry = None;
Ok(()) Ok(())
}, }
None => Err(RecordingStoreError::NoCurrentEntry) None => Err(RecordingStoreError::NoCurrentEntry),
} }
} }
// Sets the given entry's size and updates the last_message_time to now, updating the manifest // Sets the given entry's size and updates the last_message_time to now, updating the manifest
pub async fn update_entry_qmdl_size(&mut self, entry_index: usize, size_bytes: usize) -> Result<(), RecordingStoreError> { pub async fn update_entry_qmdl_size(
&mut self,
entry_index: usize,
size_bytes: usize,
) -> Result<(), RecordingStoreError> {
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes; self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
self.manifest.entries[entry_index].last_message_time = Some(Local::now()); self.manifest.entries[entry_index].last_message_time = Some(Local::now());
self.write_manifest().await self.write_manifest().await
} }
// Sets the given entry's analysis file size // Sets the given entry's analysis file size
pub async fn update_entry_analysis_size(&mut self, entry_index: usize, size_bytes: usize) -> Result<(), RecordingStoreError> { pub async fn update_entry_analysis_size(
&mut self,
entry_index: usize,
size_bytes: usize,
) -> Result<(), RecordingStoreError> {
self.manifest.entries[entry_index].analysis_size_bytes = size_bytes; self.manifest.entries[entry_index].analysis_size_bytes = size_bytes;
self.write_manifest().await self.write_manifest().await
} }
@@ -179,36 +240,45 @@ impl RecordingStore {
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> { async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
let mut manifest_file = File::options() let mut manifest_file = File::options()
.write(true) .write(true)
.open(self.path.join("manifest.toml")).await .open(self.path.join("manifest.toml"))
.await
.map_err(RecordingStoreError::WriteManifestError)?; .map_err(RecordingStoreError::WriteManifestError)?;
let manifest_contents = toml::to_string_pretty(&self.manifest) let manifest_contents =
.expect("failed to serialize manifest"); toml::to_string_pretty(&self.manifest).expect("failed to serialize manifest");
manifest_file.write_all(manifest_contents.as_bytes()).await manifest_file
.write_all(manifest_contents.as_bytes())
.await
.map_err(RecordingStoreError::WriteManifestError)?; .map_err(RecordingStoreError::WriteManifestError)?;
Ok(()) Ok(())
} }
// Finds an entry by filename // Finds an entry by filename
pub fn entry_for_name(&self, name: &str) -> Option<ManifestEntry> { pub fn entry_for_name(&self, name: &str) -> Option<(usize, &ManifestEntry)> {
self.manifest.entries.iter() let entry_index = self.manifest
.find(|entry| entry.name == name) .entries
.cloned() .iter()
.position(|entry| entry.name == name)?;
Some((entry_index, &self.manifest.entries[entry_index]))
} }
pub fn get_current_entry(&self) -> Option<&ManifestEntry> { pub fn get_current_entry(&self) -> Option<(usize, &ManifestEntry)> {
let entry_index = self.current_entry?; let entry_index = self.current_entry?;
self.manifest.entries.get(entry_index) Some((entry_index, &self.manifest.entries[entry_index]))
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use tempdir::TempDir;
use super::*; use super::*;
use tempfile::{Builder, TempDir};
fn make_temp_dir() -> TempDir {
Builder::new().prefix("qmdl_store_test").tempdir().unwrap()
}
#[tokio::test] #[tokio::test]
async fn test_load_from_empty_dir() { async fn test_load_from_empty_dir() {
let dir = TempDir::new("qmdl_store_test").unwrap(); let dir = make_temp_dir();
assert!(!RecordingStore::exists(dir.path()).await.unwrap()); assert!(!RecordingStore::exists(dir.path()).await.unwrap());
let _created_store = RecordingStore::create(dir.path()).await.unwrap(); let _created_store = RecordingStore::create(dir.path()).await.unwrap();
assert!(RecordingStore::exists(dir.path()).await.unwrap()); assert!(RecordingStore::exists(dir.path()).await.unwrap());
@@ -218,26 +288,42 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_creating_updating_and_closing_entries() { async fn test_creating_updating_and_closing_entries() {
let dir = TempDir::new("qmdl_store_test").unwrap(); let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap(); let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap(); let _ = store.new_entry().await.unwrap();
let entry_index = store.current_entry.unwrap(); let entry_index = store.current_entry.unwrap();
assert_eq!(RecordingStore::read_manifest(dir.path()).await.unwrap(), store.manifest); assert_eq!(
assert!(store.manifest.entries[entry_index].last_message_time.is_none()); RecordingStore::read_manifest(dir.path()).await.unwrap(),
store.manifest
);
assert!(store.manifest.entries[entry_index]
.last_message_time
.is_none());
store.update_entry_qmdl_size(entry_index, 1000).await.unwrap(); store
let entry = store.entry_for_name(&store.manifest.entries[entry_index].name).unwrap(); .update_entry_qmdl_size(entry_index, 1000)
.await
.unwrap();
let (entry_index, entry) = store
.entry_for_name(&store.manifest.entries[entry_index].name)
.unwrap();
assert!(entry.last_message_time.is_some()); assert!(entry.last_message_time.is_some());
assert_eq!(store.manifest.entries[entry_index].qmdl_size_bytes, 1000); assert_eq!(store.manifest.entries[entry_index].qmdl_size_bytes, 1000);
assert_eq!(RecordingStore::read_manifest(dir.path()).await.unwrap(), store.manifest); assert_eq!(
RecordingStore::read_manifest(dir.path()).await.unwrap(),
store.manifest
);
store.close_current_entry().await.unwrap(); store.close_current_entry().await.unwrap();
assert!(matches!(store.close_current_entry().await, Err(RecordingStoreError::NoCurrentEntry))); assert!(matches!(
store.close_current_entry().await,
Err(RecordingStoreError::NoCurrentEntry)
));
} }
#[tokio::test] #[tokio::test]
async fn test_repeated_new_entries() { async fn test_repeated_new_entries() {
let dir = TempDir::new("qmdl_store_test").unwrap(); let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap(); let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap(); let _ = store.new_entry().await.unwrap();
let entry_index = store.current_entry.unwrap(); let entry_index = store.current_entry.unwrap();
+40 -5
View File
@@ -4,6 +4,7 @@ use axum::extract::State;
use axum::http::{StatusCode, HeaderValue}; use axum::http::{StatusCode, HeaderValue};
use axum::response::{Response, IntoResponse}; use axum::response::{Response, IntoResponse};
use axum::extract::Path; use axum::extract::Path;
use tokio::fs::File;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use std::sync::Arc; use std::sync::Arc;
@@ -11,20 +12,25 @@ use tokio::sync::RwLock;
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use include_dir::{include_dir, Dir}; use include_dir::{include_dir, Dir};
use crate::DiagDeviceCtrlMessage; use crate::{framebuffer, DiagDeviceCtrlMessage};
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
use crate::qmdl_store::RecordingStore; use crate::qmdl_store::RecordingStore;
pub struct ServerState { pub struct ServerState {
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>, pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>, pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
pub readonly_mode: bool pub ui_update_sender: Sender<framebuffer::DisplayState>,
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
pub analysis_sender: Sender<AnalysisCtrlMessage>,
pub debug_mode: bool,
pub colorblind_mode: bool,
} }
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> { pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await; let qmdl_store = state.qmdl_store_lock.read().await;
let entry = qmdl_store.entry_for_name(&qmdl_name) let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name)
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?; .ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
let qmdl_file = qmdl_store.open_entry_qmdl(&entry).await let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?; .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?;
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64); let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
let qmdl_stream = ReaderStream::new(limited_qmdl_file); let qmdl_stream = ReaderStream::new(limited_qmdl_file);
@@ -37,10 +43,39 @@ pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
// Bundles the server's static files (html/css/js) into the binary for easy distribution // Bundles the server's static files (html/css/js) into the binary for easy distribution
static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static"); static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static");
pub async fn serve_static(Path(path): Path<String>) -> impl IntoResponse { pub async fn serve_static(State(state): State<Arc<ServerState>>, Path(path): Path<String>) -> impl IntoResponse {
let path = path.trim_start_matches('/'); let path = path.trim_start_matches('/');
let mime_type = mime_guess::from_path(path).first_or_text_plain(); let mime_type = mime_guess::from_path(path).first_or_text_plain();
// if we're in debug mode, return the files from the build directory so we
// don't have to rebuild every time the JS/HTML change
if state.debug_mode {
let mut build_path = std::path::PathBuf::new();
build_path.push("bin");
build_path.push("static");
for part in path.split("/") {
build_path.push(part);
}
return match File::open(build_path).await {
Ok(mut file) => {
let mut body = String::new();
file.read_to_string(&mut body).await.expect("failed to read file");
Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
HeaderValue::from_str(mime_type.as_ref()).unwrap(),
)
.body(Body::from(body))
.unwrap()
},
Err(_) => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())
.unwrap()
};
}
match STATIC_DIR.get_file(path) { match STATIC_DIR.get_file(path) {
None => Response::builder() None => Response::builder()
.status(StatusCode::NOT_FOUND) .status(StatusCode::NOT_FOUND)
+5
View File
@@ -22,6 +22,11 @@ th[scope='row'] {
} }
tr.current { tr.current {
background-color: #53fe7b;
font-weight: bold;
}
tr.warning {
background-color: #fe537b; background-color: #fe537b;
font-weight: bold; font-weight: bold;
} }
+3 -2
View File
@@ -27,15 +27,16 @@
<th scope="col">Size (bytes)</th> <th scope="col">Size (bytes)</th>
<th scope="col">PCAP</th> <th scope="col">PCAP</th>
<th scope="col">QMDL</th> <th scope="col">QMDL</th>
<th scope="col">Analysis Result</th>
</tr> </tr>
</thead> </thead>
</table> </table>
<div> <div>
<h3>System stats</h3> <h3>Live System stats</h3>
<pre id="system-stats">Loading...</pre> <pre id="system-stats">Loading...</pre>
</div> </div>
<div> <div>
<h3>Analysis Report</h3> <h3>Analysis Report of Current Capture</h3>
<pre id="analysis-report">Loading...</pre> <pre id="analysis-report">Loading...</pre>
</div> </div>
</body> </body>
+119 -19
View File
@@ -1,16 +1,100 @@
const STATUS_RUNNING = 'running';
const STATUS_QUEUED = 'queued';
const STATUS_NEEDS_UPDATE = 'needs-update';
const STATUS_COMPLETE = 'complete';
async function populateDivs() { async function populateDivs() {
const systemStats = await getSystemStats(); const systemStats = await getSystemStats();
const systemStatsDiv = document.getElementById('system-stats'); const systemStatsDiv = document.getElementById('system-stats');
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2); systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
const analysisReport = await getAnalysisReport();
const analysisReportDiv = document.getElementById('analysis-report'); const analysisReportDiv = document.getElementById('analysis-report');
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2); try {
const analysisReport = await getAnalysisReport('live');
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2);
} catch (e) {
analysisReportDiv.innerHTML = e.toString();
}
const qmdlManifest = await getQmdlManifest(); const qmdlManifest = await getQmdlManifest();
await updateAnalysisStatus(qmdlManifest);
await updateAnalysisResults(qmdlManifest);
updateQmdlManifestTable(qmdlManifest); updateQmdlManifestTable(qmdlManifest);
} }
function setStatus(qmdlManifest, name, status) {
// ignore qmdlManifest.current_entry, it's always running
for (const entry of qmdlManifest.entries) {
if (entry.name === name) {
entry['status'] = status;
return;
}
}
}
async function updateAnalysisStatus(qmdlManifest) {
const status = JSON.parse(await req('GET', '/api/analysis'));
if (status.running) {
setStatus(qmdlManifest, status.running, STATUS_RUNNING);
}
for (const queued in status.queued) {
setStatus(qmdlManifest, queued, STATUS_QUEUED);
}
}
function parseNewlineDelimitedJSON(inputStr) {
const lines = inputStr.split('\n');
const result = [];
let currentLine = '';
while (lines.length > 0) {
currentLine += lines.shift();
try {
const entry = JSON.parse(currentLine);
result.push(entry);
currentLine = '';
// if this chunk wasn't valid JSON, there was an escaped newline in the
// JSON line, so simply continue to the next one
} catch (e) {}
}
return result;
}
async function updateEntryAnalysisResult(entry) {
entry.analysis = {
warnings: [],
};
const report = parseNewlineDelimitedJSON(await req('GET', `/api/analysis-report/${entry.name}`));
for (const row of report) {
if (row["analysis"]) {
const timestamp = new Date(row["timestamp"]);
const analysis = row["analysis"];
for (const warning of analysis) {
entry.analysis.warnings.push({
timestamp,
warning,
})
}
}
}
if (entry.analysis.warnings.length === 0) {
entry.analysis_result = `0 warnings!`;
} else {
entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`;
}
}
async function updateAnalysisResults(qmdlManifest) {
if (qmdlManifest.current_entry) {
await updateEntryAnalysisResult(qmdlManifest.current_entry);
}
for (const entry of qmdlManifest.entries) {
if (entry.status === STATUS_NEEDS_UPDATE) {
await updateEntryAnalysisResult(entry);
entry.status = STATUS_COMPLETE;
}
}
}
function updateQmdlManifestTable(manifest) { function updateQmdlManifestTable(manifest) {
const table = document.getElementById('qmdl-manifest-table'); const table = document.getElementById('qmdl-manifest-table');
const numRows = table.rows.length; const numRows = table.rows.length;
@@ -18,43 +102,55 @@ function updateQmdlManifestTable(manifest) {
table.deleteRow(1); table.deleteRow(1);
} }
if (manifest.current_entry) { if (manifest.current_entry) {
const row = createEntryRow(manifest.current_entry); const row = createEntryRow(manifest.current_entry, true);
row.classList.add('current'); row.classList.add('current');
table.appendChild(row) table.appendChild(row)
} }
for (let entry of manifest.entries) { for (let entry of manifest.entries) {
table.appendChild(createEntryRow(entry)); table.appendChild(createEntryRow(entry), false);
} }
} }
function createEntryRow(entry) { function createLink(uri, text) {
const link = document.createElement('a');
link.href = uri;
link.innerText = text;
return link;
}
function createEntryRow(entry, isCurrent) {
const row = document.createElement('tr'); const row = document.createElement('tr');
const name = document.createElement('th'); const name = document.createElement('th');
name.scope = 'row'; name.scope = 'row';
name.innerText = entry.name; name.innerText = entry.name;
row.appendChild(name); row.appendChild(name);
for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) { for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) {
const td = document.createElement('td'); const td = document.createElement('td');
td.innerText = entry[key]; td.innerText = entry[key];
row.appendChild(td); row.appendChild(td);
} }
const pcap_td = document.createElement('td');
const pcap_link = document.createElement('a'); const pcapTd = document.createElement('td');
pcap_link.href = `/api/pcap/${entry.name}`; pcapTd.appendChild(createLink(`/api/pcap/${entry.name}`, 'pcap'));
pcap_link.innerText = 'pcap'; row.appendChild(pcapTd);
pcap_td.appendChild(pcap_link);
row.appendChild(pcap_td); const qmdlTd = document.createElement('td');
const qmdl_td = document.createElement('td'); qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}`, 'qmdl'));
const qmdl_link = document.createElement('a'); row.appendChild(qmdlTd);
qmdl_link.href = `/api/qmdl/${entry.name}`;
qmdl_link.innerText = 'qmdl'; const analysisResult = document.createElement('td');
qmdl_td.appendChild(qmdl_link); analysisResult.innerText = entry.analysis_result;
row.appendChild(qmdl_td); if (entry.analysis.warnings.length > 0) {
row.classList.add("warning");
}
row.appendChild(analysisResult);
return row; return row;
} }
async function getAnalysisReport() { async function getAnalysisReport(name) {
const rows = await req('GET', '/api/analysis-report'); const rows = await req('GET', `/api/analysis-report/${name}`);
return rows.split('\n') return rows.split('\n')
.filter(row => row.length > 0) .filter(row => row.length > 0)
.map(row => JSON.parse(row)); .map(row => JSON.parse(row));
@@ -67,6 +163,8 @@ async function getSystemStats() {
async function getQmdlManifest() { async function getQmdlManifest() {
const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest')); const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest'));
if (manifest.current_entry) { if (manifest.current_entry) {
manifest.current_entry.status = STATUS_NEEDS_UPDATE;
manifest.current_entry.analysis_result = 'Waiting...';
manifest.current_entry.start_time = new Date(manifest.current_entry.start_time); manifest.current_entry.start_time = new Date(manifest.current_entry.start_time);
if (manifest.current_entry.last_message_time === undefined) { if (manifest.current_entry.last_message_time === undefined) {
manifest.current_entry.last_message_time = "N/A"; manifest.current_entry.last_message_time = "N/A";
@@ -75,6 +173,8 @@ async function getQmdlManifest() {
} }
} }
for (entry of manifest.entries) { for (entry of manifest.entries) {
entry.status = STATUS_NEEDS_UPDATE;
entry.analysis_result = 'Waiting...';
entry.start_time = new Date(entry.start_time); entry.start_time = new Date(entry.start_time);
entry.last_message_time = new Date(entry.last_message_time); entry.last_message_time = new Date(entry.last_message_time);
} }
+3 -1
View File
@@ -1,7 +1,9 @@
# cat config.toml # cat config.toml
qmdl_store_path = "/data/rayhunter/qmdl" qmdl_store_path = "/data/rayhunter/qmdl"
port = 8080 port = 8080
readonly_mode = false debug_mode = false
enable_dummy_analyzer = false
colorblind_mode = false
# UI Levels: # UI Levels:
# 0 = invisible mode, no indicator that rayhunter is running # 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 # 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
+53 -39
View File
@@ -1,96 +1,110 @@
#!/bin/env bash #!/usr/bin/env bash
install() { install() {
if [[ -z "${SERIAL_PATH}" ]]; then if [[ -z "${SERIAL_PATH}" ]]; then
echo "SERIAL_PATH not set, did you run this from install-linux.sh or install-mac.sh?" echo "\$SERIAL_PATH not set, did you run this from install-linux.sh or install-mac.sh?"
exit 1
fi
if [[ -z "${ADB}" ]]; then
echo "\$ADB not set, did you run this from install-linux.sh or install-mac.sh?"
exit 1 exit 1
fi fi
check_adb
force_debug_mode force_debug_mode
setup_rootshell setup_rootshell
setup_rayhunter setup_rayhunter
test_rayhunter test_rayhunter
} }
check_adb() { force_debug_mode() {
if ! command -v adb &> /dev/null echo "Using adb at $ADB"
then echo "Force a switch into the debug mode to enable ADB"
echo "adb not found, please ensure it's installed or check the README.md" "$SERIAL_PATH" --root
exit 1 echo -n "adb enabled, waiting for reboot..."
fi wait_for_adb_shell
echo " it's alive!"
echo -n "waiting for atfwd_daemon to startup..."
wait_for_atfwd_daemon
echo " done!"
} }
force_debug_mode() { wait_for_atfwd_daemon() {
echo " Force a switch into the debug mode to enable ADB" until [ -n "$(_adb_shell 'pgrep atfwd_daemon')" ]
"$SERIAL_PATH" --root do
echo -n "adb enabled, waiting for reboot" sleep 1
wait_for_adb_shell done
echo "it's alive!"
} }
wait_for_adb_shell() { wait_for_adb_shell() {
until adb shell true 2> /dev/null until _adb_shell true 2> /dev/null
do do
echo -n .
sleep 1 sleep 1
done done
echo
} }
setup_rootshell() { setup_rootshell() {
_adb_push rootshell /tmp/ _adb_push rootshell /tmp/
"$SERIAL_PATH" "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" _at_syscmd "cp /tmp/rootshell /bin/rootshell"
sleep 1 sleep 1
"$SERIAL_PATH" "AT+SYSCMD=chown root /bin/rootshell" _at_syscmd "chown root /bin/rootshell"
sleep 1 sleep 1
"$SERIAL_PATH" "AT+SYSCMD=chmod 4755 /bin/rootshell" _at_syscmd "chmod 4755 /bin/rootshell"
_adb_shell '/bin/rootshell -c id'
echo "we have root!" echo "we have root!"
adb shell /bin/rootshell -c id
} }
_adb_push() { _adb_push() {
adb push "$(dirname "$0")/$1" "$2" "$ADB" push "$(dirname "$0")/$1" "$2"
}
_adb_shell() {
"$ADB" shell "$1"
}
_at_syscmd() {
"$SERIAL_PATH" "AT+SYSCMD=$1"
} }
setup_rayhunter() { setup_rayhunter() {
adb shell '/bin/rootshell -c "mkdir -p /data/rayhunter"' _at_syscmd "mkdir -p /data/rayhunter"
_adb_push config.toml.example /data/rayhunter/config.toml _adb_push config.toml.example /tmp/config.toml
_adb_push rayhunter-daemon /data/rayhunter/ _at_syscmd "mv /tmp/config.toml /data/rayhunter"
_adb_push rayhunter-daemon /tmp/rayhunter-daemon
_at_syscmd "mv /tmp/rayhunter-daemon /data/rayhunter"
_adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon _adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon
_at_syscmd "mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"
_adb_push scripts/misc-daemon /tmp/misc-daemon _adb_push scripts/misc-daemon /tmp/misc-daemon
adb shell '/bin/rootshell -c "cp /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"' _at_syscmd "mv /tmp/misc-daemon /etc/init.d/misc-daemon"
adb shell '/bin/rootshell -c "cp /tmp/misc-daemon /etc/init.d/misc-daemon"'
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/rayhunter_daemon"' _at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon"
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/misc-daemon"' _at_syscmd "chmod 755 /etc/init.d/misc-daemon"
echo -n "rebooting, this may take a sec..."
adb shell '/bin/rootshell -c reboot' echo -n "waiting for reboot..."
_at_syscmd "shutdown -r -t 1 now"
# first wait for shutdown (it can take ~10s) # first wait for shutdown (it can take ~10s)
until ! adb shell true 2> /dev/null until ! _adb_shell true 2> /dev/null
do do
echo -n '.'
sleep 1 sleep 1
done done
# now wait for boot to finish # now wait for boot to finish
wait_for_adb_shell wait_for_adb_shell
echo "rebooted successfully!" echo " done!"
} }
test_rayhunter() { test_rayhunter() {
URL="http://localhost:8080" URL="http://localhost:8080"
adb forward tcp:8080 tcp:8080 "$ADB" forward tcp:8080 tcp:8080 > /dev/null
echo -n "checking for rayhunter server..." echo -n "checking for rayhunter server..."
SECONDS=0 SECONDS=0
while (( SECONDS < 30 )); do while (( SECONDS < 30 )); do
if curl -L --fail-with-body "$URL" -o /dev/null -s; then if curl -L --fail-with-body "$URL" -o /dev/null -s; then
echo echo "success!"
echo "success! you can access rayhunter at $URL" echo "you can access rayhunter at $URL"
return return
fi fi
sleep 1 sleep 1
echo -n "."
done done
echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :(" echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :("
} }
+11
View File
@@ -1,6 +1,17 @@
#!/bin/env bash #!/bin/env bash
set -e set -e
if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ] ; then
echo "adb not found, downloading local copy"
curl -O "https://dl.google.com/android/repository/platform-tools-latest-linux.zip"
unzip platform-tools-latest-linux.zip
fi
export ADB="./platform-tools/adb"
else
export ADB=`which adb`
fi
export SERIAL_PATH="./serial-ubuntu-latest/serial" export SERIAL_PATH="./serial-ubuntu-latest/serial"
. "$(dirname "$0")"/install-common.sh . "$(dirname "$0")"/install-common.sh
install install
+13 -2
View File
@@ -1,6 +1,17 @@
#!/bin/env bash #!/usr/bin/env bash
set -e set -e
export SERIAL_PATH="./serial-mac-latest/serial" if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ]; then
echo "adb not found, downloading local copy"
curl -O "https://dl.google.com/android/repository/platform-tools-latest-darwin.zip"
unzip platform-tools-latest-darwin.zip
fi
export ADB="./platform-tools/adb"
else
export ADB=`which adb`
fi
export SERIAL_PATH="./serial-macos-latest/serial"
. "$(dirname "$0")"/install-common.sh . "$(dirname "$0")"/install-common.sh
install install
+28 -9
View File
@@ -4,7 +4,12 @@ use serde::Serialize;
use crate::{diag::MessagesContainer, gsmtap_parser}; use crate::{diag::MessagesContainer, gsmtap_parser};
use super::{imsi_provided::ImsiProvidedAnalyzer, information_element::InformationElement, lte_downgrade::LteSib6And7DowngradeAnalyzer, null_cipher::NullCipherAnalyzer}; use super::{
imsi_requested::ImsiRequestedAnalyzer,
information_element::InformationElement,
lte_downgrade::LteSib6And7DowngradeAnalyzer,
null_cipher::NullCipherAnalyzer,
};
/// Qualitative measure of how severe a Warning event type is. /// Qualitative measure of how severe a Warning event type is.
/// The levels should break down like this: /// The levels should break down like this:
@@ -18,7 +23,7 @@ pub enum Severity {
High, High,
} }
/// [QualitativeWarning] events will always be shown to the user in some manner, /// `QualitativeWarning` events will always be shown to the user in some manner,
/// while `Informational` ones may be hidden based on user settings. /// while `Informational` ones may be hidden based on user settings.
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
#[serde(tag = "type")] #[serde(tag = "type")]
@@ -60,19 +65,19 @@ pub trait Analyzer {
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct AnalyzerMetadata { pub struct AnalyzerMetadata {
name: String, pub name: String,
description: String, pub description: String,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct ReportMetadata { pub struct ReportMetadata {
analyzers: Vec<AnalyzerMetadata>, pub analyzers: Vec<AnalyzerMetadata>,
} }
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct PacketAnalysis { pub struct PacketAnalysis {
timestamp: DateTime<FixedOffset>, pub timestamp: DateTime<FixedOffset>,
events: Vec<Option<Event>>, pub events: Vec<Option<Event>>,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@@ -86,6 +91,19 @@ impl AnalysisRow {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.skipped_message_reasons.is_empty() && self.analysis.is_empty() self.skipped_message_reasons.is_empty() && self.analysis.is_empty()
} }
pub fn contains_warnings(&self) -> bool {
for analysis in &self.analysis {
for maybe_event in &analysis.events {
if let Some(event) = maybe_event {
if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
return true;
}
}
}
}
false
}
} }
pub struct Harness { pub struct Harness {
@@ -100,8 +118,9 @@ impl Harness {
pub fn new_with_all_analyzers() -> Self { pub fn new_with_all_analyzers() -> Self {
let mut harness = Harness::new(); let mut harness = Harness::new();
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{})); harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{})); harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
harness.add_analyzer(Box::new(NullCipherAnalyzer{})); harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
harness harness
} }
@@ -175,7 +194,7 @@ impl Harness {
pub fn get_metadata(&self) -> ReportMetadata { pub fn get_metadata(&self) -> ReportMetadata {
let names = self.get_names(); let names = self.get_names();
let descriptions = self.get_names(); let descriptions = self.get_descriptions();
let mut analyzers = Vec::new(); let mut analyzers = Vec::new();
for (name, description) in names.iter().zip(descriptions.iter()) { for (name, description) in names.iter().zip(descriptions.iter()) {
analyzers.push(AnalyzerMetadata { analyzers.push(AnalyzerMetadata {
+59
View File
@@ -0,0 +1,59 @@
use std::borrow::Cow;
use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement};
const PACKET_THRESHHOLD: usize = 150;
pub struct ImsiRequestedAnalyzer {
packet_num: usize,
}
impl ImsiRequestedAnalyzer {
pub fn new() -> Self {
Self { packet_num: 0 }
}
}
impl Analyzer for ImsiRequestedAnalyzer {
fn get_name(&self) -> Cow<str> {
Cow::from("IMSI Requested")
}
fn get_description(&self) -> Cow<str> {
Cow::from("Tests whether the ME sends an IMSI Identity Request NAS message")
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
self.packet_num += 1;
let InformationElement::LTE(LteInformationElement::NAS(payload)) = ie else {
return None;
};
// NAS identity request
if payload == &[0x07, 0x55, 0x01] {
if self.packet_num < PACKET_THRESHHOLD {
return Some(Event {
event_type: EventType::QualitativeWarning {
severity: Severity::Medium
},
message: format!(
"NAS IMSI request detected, however it was within \
the first {} packets of this analysis. If you just \
turned your device on, this is likely a \
false-positive.",
PACKET_THRESHHOLD
)
})
} else {
return Some(Event {
event_type: EventType::QualitativeWarning {
severity: Severity::High
},
message: format!("NAS IMSI request detected"),
})
}
}
None
}
}
+7 -1
View File
@@ -5,7 +5,7 @@
use telcom_parser::{decode, lte_rrc}; use telcom_parser::{decode, lte_rrc};
use thiserror::Error; use thiserror::Error;
use crate::gsmtap::{GsmtapType, LteRrcSubtype, GsmtapMessage}; use crate::gsmtap::{GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum InformationElementError { pub enum InformationElementError {
@@ -40,6 +40,9 @@ pub enum LteInformationElement {
SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message), SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message),
SbcchSlBchV2x(lte_rrc::SBCCH_SL_BCH_Message_V2X_r14), SbcchSlBchV2x(lte_rrc::SBCCH_SL_BCH_Message_V2X_r14),
// FIXME: actually parse NAS messages
NAS(Vec<u8>),
// FIXME: unclear which message these "NB" types map to // FIXME: unclear which message these "NB" types map to
//DlCcchNb(), //DlCcchNb(),
//DlDcchNb(), //DlDcchNb(),
@@ -79,6 +82,9 @@ impl TryFrom<&GsmtapMessage> for InformationElement {
}; };
Ok(InformationElement::LTE(lte)) Ok(InformationElement::LTE(lte))
}, },
GsmtapType::LteNas(LteNasSubtype::Plain) => {
Ok(InformationElement::LTE(LteInformationElement::NAS(gsmtap_msg.payload.clone())))
},
_ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)), _ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)),
} }
} }
+1
View File
@@ -2,4 +2,5 @@ pub mod analyzer;
pub mod information_element; pub mod information_element;
pub mod lte_downgrade; pub mod lte_downgrade;
pub mod imsi_provided; pub mod imsi_provided;
pub mod imsi_requested;
pub mod null_cipher; pub mod null_cipher;
+1 -1
View File
@@ -1,6 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use telcom_parser::lte_rrc::{CipheringAlgorithm_r12, DL_CCCH_MessageType, DL_CCCH_MessageType_c1, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Identity, RRCConnectionReconfiguration, RRCConnectionReconfigurationCriticalExtensions, RRCConnectionReconfigurationCriticalExtensions_c1, RRCConnectionReconfiguration_r8_IEs, RRCConnectionRelease_v890_IEs, SCG_Configuration_r12, SecurityConfigHO_v1530HandoverType_v1530, SecurityModeCommand, SecurityModeCommandCriticalExtensions, SecurityModeCommandCriticalExtensions_c1}; use telcom_parser::lte_rrc::{CipheringAlgorithm_r12, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReconfiguration, RRCConnectionReconfigurationCriticalExtensions, RRCConnectionReconfigurationCriticalExtensions_c1, SCG_Configuration_r12, SecurityConfigHO_v1530HandoverType_v1530, SecurityModeCommand, SecurityModeCommandCriticalExtensions, SecurityModeCommandCriticalExtensions_c1};
use super::analyzer::{Analyzer, Event, EventType, Severity}; use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement}; use super::information_element::{InformationElement, LteInformationElement};
+15
View File
@@ -183,6 +183,8 @@ pub enum LogBody {
// * 0xb0ed: plain EMM NAS message (outgoing) // * 0xb0ed: plain EMM NAS message (outgoing)
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")] #[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
Nas4GMessage { Nas4GMessage {
#[deku(ctx = "log_type")]
direction: Nas4GMessageDirection,
ext_header_version: u8, ext_header_version: u8,
rrc_rel: u8, rrc_rel: u8,
rrc_version_minor: u8, rrc_version_minor: u8,
@@ -211,6 +213,19 @@ pub enum LogBody {
} }
} }
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "log_type: u16", id = "log_type")]
pub enum Nas4GMessageDirection {
// * 0xb0e2: plain ESM NAS message (incoming)
// * 0xb0e3: plain ESM NAS message (outgoing)
// * 0xb0ec: plain EMM NAS message (incoming)
// * 0xb0ed: plain EMM NAS message (outgoing)
#[deku(id_pat = "0xb0e2 | 0xb0ec")]
Inbound,
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
Outbound,
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)] #[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")] #[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
pub enum LteRrcOtaPacket { pub enum LteRrcOtaPacket {
+4
View File
@@ -63,11 +63,15 @@ const MEMORY_DEVICE_MODE: i32 = 2;
const DIAG_IOCTL_REMOTE_DEV: u32 = 32; const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
const DIAG_IOCTL_REMOTE_DEV: u64 = 32; const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
#[cfg(target_arch = "aarch64")]
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
#[cfg(target_arch = "arm")] #[cfg(target_arch = "arm")]
const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7; const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7;
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7; const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
#[cfg(target_arch = "aarch64")]
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
pub struct DiagDevice { pub struct DiagDevice {
file: File, file: File,
+3
View File
@@ -7,3 +7,6 @@ pub mod gsmtap;
pub mod gsmtap_parser; pub mod gsmtap_parser;
pub mod pcap; pub mod pcap;
pub mod analysis; pub mod analysis;
// re-export telcom_parser, since we use its types in our API
pub use telcom_parser;
+1 -1
View File
@@ -60,7 +60,7 @@ impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
}) })
} }
async fn get_next_messages_container(&mut self) -> Result<Option<MessagesContainer>, std::io::Error> { pub async fn get_next_messages_container(&mut self) -> Result<Option<MessagesContainer>, std::io::Error> {
if let Some(max_bytes) = self.max_bytes { if let Some(max_bytes) = self.max_bytes {
if self.bytes_read >= max_bytes { if self.bytes_read >= max_bytes {
if self.bytes_read > max_bytes { if self.bytes_read > max_bytes {
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
cargo build --release --target="armv7-unknown-linux-gnueabihf" cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"' adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"'
+8 -5
View File
@@ -6,6 +6,7 @@ use std::process::Command;
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::env; use std::env;
#[cfg(target_arch = "arm")]
use nix::unistd::Gid; use nix::unistd::Gid;
fn main() { fn main() {
@@ -14,11 +15,13 @@ fn main() {
// Android's "paranoid network" feature restricts network access to // Android's "paranoid network" feature restricts network access to
// processes in specific groups. More info here: // processes in specific groups. More info here:
// https://www.elinux.org/Android_Security#Paranoid_network-ing // https://www.elinux.org/Android_Security#Paranoid_network-ing
let gids = &[ #[cfg(target_arch = "arm")] {
Gid::from_raw(3003), // AID_INET let gids = &[
Gid::from_raw(3004), // AID_NET_RAW Gid::from_raw(3003), // AID_INET
]; Gid::from_raw(3004), // AID_NET_RAW
nix::unistd::setgroups(gids).expect("setgroups failed"); ];
nix::unistd::setgroups(gids).expect("setgroups failed");
}
// discard argv[0] // discard argv[0]
let _ = args.next(); let _ = args.next();
+1 -2
View File
@@ -6,5 +6,4 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rusb = "0.9.3" rusb = { version = "0.9.3", features = ["vendored"] }
+38 -37
View File
@@ -20,7 +20,6 @@
//! Err(e) => panic!("Failed to initialize libusb: {0}", e), //! Err(e) => panic!("Failed to initialize libusb: {0}", e),
//! ```` //! ````
use std::str; use std::str;
use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
use rusb::{Context, DeviceHandle, UsbContext}; use rusb::{Context, DeviceHandle, UsbContext};
@@ -28,19 +27,21 @@ use rusb::{Context, DeviceHandle, UsbContext};
fn main() { fn main() {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
if args.len() < 2 { if args.len() != 2 {
println!("usage: {0} <command>", args[0]); println!("usage: {0} [<command> | --root]", args[0]);
return; return;
} }
match Context::new() { match Context::new() {
Ok(mut context) => match open_orbic(&mut context) { Ok(mut context) => {
Some(mut handle) => { if args[1] == "--root" {
if &args[1] != "--root" { enable_command_mode(&mut context);
send_command(&mut handle, &args[1]) } else {
match open_orbic(&mut context) {
Some(mut handle) => send_command(&mut handle, &args[1]),
None => panic!("No Orbic device found"),
} }
} }
None => panic!("No Orbic device found"),
}, },
Err(e) => panic!("Failed to initialize libusb: {0}", e), Err(e) => panic!("Failed to initialize libusb: {0}", e),
} }
@@ -78,54 +79,54 @@ fn send_command<T: UsbContext>(handle: &mut DeviceHandle<T>, command: &str) {
.expect("Failed to read response"); .expect("Failed to read response");
let responsestr = str::from_utf8(&response).expect("Failed to parse response"); let responsestr = str::from_utf8(&response).expect("Failed to parse response");
if !responsestr.starts_with("\r\nOK\r\n") { if !responsestr.contains("\r\nOK\r\n") {
println!("Received unexpected response{0}", responsestr) println!("Received unexpected response{0}", responsestr);
std::process::exit(1);
} }
} }
/// Send a command to switch the device into generic mode, exposing serial /// Send a command to switch the device into generic mode, exposing serial
/// ///
/// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition. /// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition.
fn switch_device<T: UsbContext>(handle: &mut DeviceHandle<T>) { fn enable_command_mode<T: UsbContext>(context: &mut T) {
let timeout = Duration::from_secs(1); if open_orbic(context).is_some() {
println!("Device already in command mode. Doing nothing...");
if let Err(e) = handle.write_control(0x40, 0xa0, 0, 0, &[], timeout) { return;
// If the device reboots while the command is still executing we
// may get a pipe error here
if e == rusb::Error::Pipe {
return;
}
panic!("Failed to send device switch control request: {0}", e)
} }
let timeout = Duration::from_secs(1);
if let Some(handle) = open_device(context, 0x05c6, 0xf626) {
if let Err(e) = handle.write_control(0x40, 0xa0, 0, 0, &[], timeout) {
// If the device reboots while the command is still executing we
// may get a pipe error here
if e == rusb::Error::Pipe {
return;
}
panic!("Failed to send device switch control request: {0}", e)
}
return;
}
panic!("No Orbic device found");
} }
/// Get a handle and contet for the orbic device /// Get a handle and contet for the orbic device
///
/// If the device isn't already in command mode this function will call swtich_device to switch it into command mode
fn open_orbic<T: UsbContext>(context: &mut T) -> Option<DeviceHandle<T>> { fn open_orbic<T: UsbContext>(context: &mut T) -> Option<DeviceHandle<T>> {
// Device after initial mode switch // Device after initial mode switch
if let Some(handle) = open_device(context, 0x05c6, 0xf601) { if let Some(mut handle) = open_device(context, 0x05c6, 0xf601) {
handle.set_auto_detach_kernel_driver(true).expect("set_auto_detach_kernel_driver failed");
handle.claim_interface(1).expect("claim_interface(1) failed");
return Some(handle); return Some(handle);
} }
// Device with rndis enabled as well // Device with rndis enabled as well
if let Some(handle) = open_device(context, 0x05c6, 0xf622) { if let Some(mut handle) = open_device(context, 0x05c6, 0xf622) {
handle.set_auto_detach_kernel_driver(true).expect("set_auto_detach_kernel_driver failed");
handle.claim_interface(1).expect("claim_interface(1) failed");
return Some(handle); return Some(handle);
} }
// Device in out-of-the-box state, need to switch to diag mode None
match open_device(context, 0x05c6, 0xf626) {
Some(mut handle) => switch_device(&mut handle),
None => panic!("No Orbic device detected"),
}
for _ in 1..10 {
if let Some(handle) = open_device(context, 0x05c6, 0xf601) {
return Some(handle);
}
sleep(Duration::from_secs(10))
}
panic!("No Orbic device detected")
} }
/// Generic function to open a USB device /// Generic function to open a USB device
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/python3
import pycrate_mobile
from pycrate_mobile import NASLTE
import pycrate_core
import binascii
import sys
import pprint
from enum import Enum
import pycrate_mobile.TS24301_EMM
EPS_IMSI_ATTACH = 2
def parse_nas_message(buffer, uplink=None):
if isinstance(buffer, str): #handle string argument or raw bytes
bin = binascii.unhexlify(buffer)
else:
bin = buffer
if uplink:
parsed = NASLTE.parse_NASLTE_MO(bin)
elif uplink == None: #We don't know if its an up or downlink
parsed = NASLTE.parse_NASLTE_MO(bin)
if parsed[0] == None:
parsed = NASLTE.parse_NASLTE_MT(bin)
else:
parsed = NASLTE.parse_NASLTE_MT(bin)
if parsed[0] is None: # Not a NAS Packet
raise TypeError("Not a nas packet")
return parsed[0]
def heur_ue_imsi_sent(msg):
output = "device transmitted IMSI to base station!"
if type(msg) not in [pycrate_mobile.TS24301_EMM.EMMAttachRequest, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage]:
return (False, None)
if isinstance(msg, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage):
try:
msg = msg['EMMAttachRequest']
except pycrate_core.elt.EltErr:
return (False, None)
if msg['EPSAttachType']['V'].to_int() == EPS_IMSI_ATTACH: #EPSAttachType Value is 'Combined EPS/IMSI Attach (2)'
return (True, output)
return (False, None)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: nasparse.py [hex encoded nas message]")
exit(1)
buffer = sys.argv[1]
msg = parse_nas_message(buffer)
pprint.pprint(msg)
triggered, message = heur_ue_imsi_sent(msg)
if triggered:
print(message)
exit(1)
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/python3
import unittest
import nasparse
class TestNasparse(unittest.TestCase):
imsi_sent_msg = '07412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1000000000000'
sec_imsi_sent_msg = '1727db4b7c0207412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1'
non_nas_msg = 'deadbeefcafe'
other_nas_msg = '074413780004023fd121'
other_nas_mt_msg = "023fd12100000000000000000000000000000000000000000000000000000000"
ciphered_nas_msg = "27ed6146bd0162a5d62d62e1ce501720dc8bd84f1167fd"
def run_heur(self, msg):
buf = nasparse.parse_nas_message(msg)
return nasparse.heur_ue_imsi_sent(buf)[0]
def test_imsi_sent(self):
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "imsi_sent_msg should trigger heuristic")
def test_sec_imsi_sent(self):
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "sec_imsi_sent_msg should trigger heuristic")
def test_non_nas_msg(self):
with self.assertRaises(TypeError):
self.run_heur(self.non_nas_msg)
def test_other_nas(self):
self.assertEqual(self.run_heur(self.other_nas_msg), False, "other_nas_msg should not trigger heuristic")
def test_other_nas_mt(self):
self.assertEqual(self.run_heur(self.other_nas_mt_msg), False, "other_nas_mt_msg should not trigger heuristic")
def test_ciphered_nas(self):
self.assertEqual(self.run_heur(self.ciphered_nas_msg), False, "ciphered_nas_msg should not trigger heuristic")
if __name__ == '__main__':
unittest.main()
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/python3
import nasparse
from scapy.utils import RawPcapNgReader
import sys
TYPE_LTE_NAS = 0x12
UDP_LEN = 28
def process_pcap(pcap_path):
print('Opening {}...'.format(pcap_path))
count = 0
for pkt_data, pkt_metadata in RawPcapNgReader(pcap_path):
count += 1
gsmtap_len = pkt_data[UDP_LEN+1] * 4 # gsmtap header length is stored in the 2nd byte of GSMTAP as a number of 32 bit words
header_end = gsmtap_len + UDP_LEN #length of UDP/IP header plus GSMTAP header
gsmtap_hdr = pkt_data[UDP_LEN:header_end]
if gsmtap_hdr[2] != TYPE_LTE_NAS:
continue
# uplink status is the 7th bit of the 5th byte of the GSMTAP header.
# Uplink (Mobile originated) = 0 Downlink (mobile terminated) = 1
uplink = (gsmtap_hdr[4] & 0b01000000) >> 6
buffer = pkt_data[header_end:]
msg = nasparse.parse_nas_message(buffer, uplink)
triggered, message = nasparse.heur_ue_imsi_sent(msg)
if triggered:
print(f"Frame {count} triggered heuristic: {message}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: pcap_check.py [path/to/pcap/file]")
exit(1)
pcap_path = sys.argv[1]
process_pcap(pcap_path)