mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 02:03:35 -07:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee83613757 | |||
| 840f8ad8b0 | |||
| c9ac834ca7 | |||
| 8629aacf6b | |||
| a3fd1479f9 | |||
| 049c563f02 | |||
| a33b5a3418 | |||
| 107ba58296 | |||
| d016279172 | |||
| 5a084f1abb | |||
| 3619df32ab | |||
| 34d87d1fd7 | |||
| da4952e70f | |||
| 30323b8329 | |||
| 28b0f409db | |||
| 12640cc878 | |||
| 26eda5904f | |||
| 3e26e61b05 | |||
| 565c0f1e67 | |||
| 6bd36921d8 | |||
| c83ae30be8 | |||
| fa612241a5 | |||
| 10592bbd9d | |||
| 327eaddcd7 | |||
| 32149c3b37 | |||
| e47d4dacc4 | |||
| 4009e3d1ed | |||
| b2cd735a07 | |||
| 94e9a88a91 | |||
| f4a6c834d2 | |||
| 95e8f846d3 | |||
| 15f128add1 | |||
| 87f9cc403b | |||
| 7addf3a67f | |||
| 4d8cc9b738 | |||
| b0d797d206 | |||
| 1ae3b5020b | |||
| a23df84848 | |||
| 4e862841b3 | |||
| 2cc8404b13 | |||
| 35ae2962f2 | |||
| 1134361cca | |||
| bec680f93d | |||
| 968af93b69 | |||
| ee75326912 | |||
| 3b9a001e88 | |||
| 78d33b2cff | |||
| 6c237e884c | |||
| f3e4091e1d | |||
| 16f705f29c | |||
| a6fce6d568 | |||
| fcac6fdf16 | |||
| df84faa1f9 | |||
| c59fb7c013 | |||
| ca4f49b15f | |||
| 861aaedd47 |
@@ -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
+86
@@ -482,6 +482,16 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colored"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -602,6 +612,15 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-into-owned"
|
name = "derive-into-owned"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1310,6 +1329,12 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -1360,6 +1385,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.32.2"
|
version = "0.32.2"
|
||||||
@@ -1487,6 +1521,12 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -1679,6 +1719,7 @@ dependencies = [
|
|||||||
"rayhunter",
|
"rayhunter",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"simple_logger",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1901,6 +1942,18 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple_logger"
|
||||||
|
version = "5.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb"
|
||||||
|
dependencies = [
|
||||||
|
"colored",
|
||||||
|
"log",
|
||||||
|
"time",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -2065,6 +2118,39 @@ dependencies = [
|
|||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"libc",
|
||||||
|
"num-conv",
|
||||||
|
"num_threads",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.36.0"
|
version = "1.36.0"
|
||||||
|
|||||||
@@ -1,44 +1,29 @@
|
|||||||
|

|
||||||
# Rayhunter
|
# Rayhunter
|
||||||
|
|
||||||
```
|
|
||||||
@@@@@@@ @@@@@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@@@@@ @@@@@@@@ @@@@@@@
|
|
||||||
@@! @@@ @@! @@@ @@! !@@ @@! @@@ @@! @@@ @@!@!@@@ @@! @@! @@! @@@
|
|
||||||
@!@!!@! @!@!@!@! !@!@! @!@!@!@! @!@ !@! @!@@!!@! @!! @!!!:! @!@!!@!
|
|
||||||
!!: :!! !!: !!! !!: !!: !!! !!: !!! !!: !!! !!: !!: !!: :!!
|
|
||||||
: : : : : : .: : : : :.:: : :: : : : :: ::: : : :
|
|
||||||
|
|
||||||
|
|
||||||
_ _ _ _ _ _ _ _
|
|
||||||
)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_
|
|
||||||
|
|
||||||
O .
|
|
||||||
O ' '
|
|
||||||
o ' .
|
|
||||||
o .'
|
|
||||||
__________.-' '...___
|
|
||||||
.-' ### '''...__
|
|
||||||
/ a### ## ''--.._ ______
|
|
||||||
'. # ######## ' .-'
|
|
||||||
'-._ ..**********#### ___...---'''\ '
|
|
||||||
'-._ __________...---''' \ l
|
|
||||||
\ | apc '._|
|
|
||||||
\__;
|
|
||||||
```
|
|
||||||

|

|
||||||
|
|
||||||
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.
|
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.
|
||||||
|
|
||||||
**THIS CODE IS PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS**
|
**THIS CODE IS PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS**
|
||||||
|
|
||||||
|
|
||||||
|
## The Hardware
|
||||||
|
|
||||||
Code is built and tested for the Orbic RC400L mobile hotspot, it may work on other orbics and other
|
Code is built and tested for the Orbic RC400L mobile hotspot, it may work on other orbics and other
|
||||||
linux/qualcom devices but this is the only one we have tested on. Buy the orbic [using bezos bucks](https://www.amazon.com/gp/product/B09CLS6Z7X/)
|
linux/qualcom devices but this is the only one we have tested on.
|
||||||
|
Buy the orbic [using bezos bucks](https://www.amazon.com/Orbic-Verizon-Hotspot-Connect-Enabled/dp/B08N3CHC4Y)
|
||||||
|
Or on [Ebay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l)
|
||||||
|
|
||||||
## 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.
|
**If you are installing from the cloned github repository please see the development instructions below, running `install-linux.sh` from the git tree will not work.**
|
||||||
|
2. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`). The Linux installer has only been tested on the latest version of Ubuntu. If it fails you will need to follow the install steps outlined in **Development** below.
|
||||||
|
3. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -46,10 +31,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.
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ 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"
|
tempfile = "3.10.1"
|
||||||
|
simple_logger = "5.0.0"
|
||||||
|
|||||||
@@ -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())))
|
||||||
|
}
|
||||||
+130
-14
@@ -1,31 +1,147 @@
|
|||||||
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 log::{info, warn};
|
||||||
use tokio::fs::File;
|
use rayhunter::{analysis::analyzer::{EventType, Harness}, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlReader};
|
||||||
|
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 = 'p', long)]
|
||||||
qmdl_path: PathBuf,
|
qmdl_path: PathBuf,
|
||||||
|
|
||||||
|
#[arg(short = 'c', long)]
|
||||||
|
pcapify: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
show_skipped: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
|
||||||
async fn main() {
|
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||||
env_logger::init();
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
let mut harness = Harness::new_with_all_analyzers();
|
|
||||||
|
|
||||||
let qmdl_file = File::open(args.qmdl_path).await.expect("failed to open QMDL file");
|
|
||||||
let file_size = qmdl_file.metadata().await.expect("failed to get QMDL file metadata").len();
|
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_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||||
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
|
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
|
||||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||||
println!("{}\n", serde_json::to_string(&harness.get_metadata()).expect("failed to serialize report metadata"));
|
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") {
|
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") {
|
||||||
let row = harness.analyze_qmdl_messages(container);
|
let row = harness.analyze_qmdl_messages(container);
|
||||||
println!("{}\n", serde_json::to_string(&row).expect("failed to serialize row"));
|
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 {
|
||||||
|
let Some(event) = maybe_event else { continue };
|
||||||
|
match event.event_type {
|
||||||
|
EventType::Informational => {
|
||||||
|
info!(
|
||||||
|
"{}: INFO - {} {}",
|
||||||
|
qmdl_path,
|
||||||
|
analysis.timestamp,
|
||||||
|
event.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EventType::QualitativeWarning { severity } => {
|
||||||
|
warn!(
|
||||||
|
"{}: WARNING (Severity: {:?}) - {} {}",
|
||||||
|
qmdl_path,
|
||||||
|
severity,
|
||||||
|
analysis.timestamp,
|
||||||
|
event.message,
|
||||||
|
);
|
||||||
|
warnings += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if show_skipped && skipped > 0 {
|
||||||
|
info!("{}: messages skipped:", qmdl_path);
|
||||||
|
for (reason, count) in skipped_reasons.iter() {
|
||||||
|
info!(" - {}: \"{}\"", count, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("{}: {} 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("wrote pcap to {:?}", &pcap_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
let level = if args.verbose {
|
||||||
|
log::LevelFilter::Trace
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Warn
|
||||||
|
};
|
||||||
|
simple_logger::SimpleLogger::new()
|
||||||
|
.with_colors(true)
|
||||||
|
.without_timestamps()
|
||||||
|
.with_level(level)
|
||||||
|
.init().unwrap();
|
||||||
|
|
||||||
|
let mut harness = Harness::new_with_all_analyzers();
|
||||||
|
if args.enable_dummy_analyzer {
|
||||||
|
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
||||||
|
}
|
||||||
|
info!("Analyzers:");
|
||||||
|
for analyzer in harness.get_metadata().analyzers {
|
||||||
|
info!(" - {}: {}", analyzer.name, analyzer.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = metadata(&args.qmdl_path).await.expect("failed to get metadata");
|
||||||
|
if metadata.is_dir() {
|
||||||
|
let mut dir = read_dir(&args.qmdl_path).await.expect("failed to read dir");
|
||||||
|
while let Some(entry) = dir.next_entry().await.expect("failed to get entry") {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name_str = name.to_str().unwrap();
|
||||||
|
if name_str.ends_with(".qmdl") {
|
||||||
|
let path = entry.path();
|
||||||
|
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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)));
|
||||||
|
|
||||||
|
|||||||
+129
-47
@@ -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,32 +240,37 @@ 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 tempfile::{TempDir, Builder};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use tempfile::{Builder, TempDir};
|
||||||
|
|
||||||
fn make_temp_dir() -> TempDir {
|
fn make_temp_dir() -> TempDir {
|
||||||
Builder::new().prefix("qmdl_store_test").tempdir().unwrap()
|
Builder::new().prefix("qmdl_store_test").tempdir().unwrap()
|
||||||
@@ -226,17 +292,33 @@ mod tests {
|
|||||||
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]
|
||||||
|
|||||||
+42
-6
@@ -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,26 @@ 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_idx = qmdl_name.trim_end_matches(".qmdl");
|
||||||
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_idx)
|
||||||
.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_idx)))?;
|
||||||
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 +44,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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+124
-19
@@ -1,16 +1,105 @@
|
|||||||
|
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 !!!`;
|
||||||
|
for(warning of entry.analysis.warnings){
|
||||||
|
msg = `${warning.timestamp}: ${warning.warning.events[1].message}`
|
||||||
|
console.log(msg)
|
||||||
|
entry.analysis_result += `<br>${msg}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +107,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`, '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.innerHTML = 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 +168,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 +178,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);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-1
@@ -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
|
||||||
|
|||||||
Vendored
+36
-28
@@ -1,25 +1,21 @@
|
|||||||
#!/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() {
|
|
||||||
if ! command -v adb &> /dev/null
|
|
||||||
then
|
|
||||||
echo "adb not found, please ensure it's installed or check the README.md"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
force_debug_mode() {
|
force_debug_mode() {
|
||||||
|
echo "Using adb at $ADB"
|
||||||
echo "Force a switch into the debug mode to enable ADB"
|
echo "Force a switch into the debug mode to enable ADB"
|
||||||
"$SERIAL_PATH" --root
|
"$SERIAL_PATH" --root
|
||||||
echo -n "adb enabled, waiting for reboot..."
|
echo -n "adb enabled, waiting for reboot..."
|
||||||
@@ -31,14 +27,14 @@ force_debug_mode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wait_for_atfwd_daemon() {
|
wait_for_atfwd_daemon() {
|
||||||
until [ -n "$(adb shell 'pgrep atfwd_daemon')" ]
|
until [ -n "$(_adb_shell 'pgrep atfwd_daemon')" ]
|
||||||
do
|
do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_adb_shell() {
|
wait_for_adb_shell() {
|
||||||
until adb shell true 2> /dev/null
|
until _adb_shell true 2> /dev/null
|
||||||
do
|
do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
@@ -46,34 +42,46 @@ wait_for_adb_shell() {
|
|||||||
|
|
||||||
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
|
_adb_shell '/bin/rootshell -c id'
|
||||||
echo "we have root!"
|
echo "we have root!"
|
||||||
}
|
}
|
||||||
|
|
||||||
_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 "waiting for reboot..."
|
echo -n "waiting for reboot..."
|
||||||
adb shell '/bin/rootshell -c 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
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
@@ -86,7 +94,7 @@ setup_rayhunter() {
|
|||||||
|
|
||||||
test_rayhunter() {
|
test_rayhunter() {
|
||||||
URL="http://localhost:8080"
|
URL="http://localhost:8080"
|
||||||
adb forward tcp:8080 tcp:8080 > /dev/null
|
"$ADB" forward tcp:8080 tcp:8080 > /dev/null
|
||||||
echo -n "checking for rayhunter server..."
|
echo -n "checking for rayhunter server..."
|
||||||
|
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
|
|||||||
Vendored
+17
@@ -2,5 +2,22 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
export SERIAL_PATH="./serial-ubuntu-latest/serial"
|
export SERIAL_PATH="./serial-ubuntu-latest/serial"
|
||||||
|
|
||||||
|
if [ ! -x "$SERIAL_PATH" ]; then
|
||||||
|
echo "The serial binary cannot be found at $SERIAL_PATH. If you are running this from the git tree please instead run it from the latest release bundle https://github.com/EFForg/rayhunter/releases"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
. "$(dirname "$0")"/install-common.sh
|
. "$(dirname "$0")"/install-common.sh
|
||||||
install
|
install
|
||||||
|
|||||||
Vendored
+17
-1
@@ -1,6 +1,22 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
export SERIAL_PATH="./serial-macos-latest/serial"
|
export SERIAL_PATH="./serial-macos-latest/serial"
|
||||||
|
if [ ! -x "$SERIAL_PATH" ]; then
|
||||||
|
echo "The serial binary cannot be found at $SERIAL_PATH. If you are running this from the git tree please instead run it from the latest release bundle at https://github.com/EFForg/rayhunter/releases"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
. "$(dirname "$0")"/install-common.sh
|
. "$(dirname "$0")"/install-common.sh
|
||||||
install
|
install
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ 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,
|
||||||
|
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
|
||||||
|
priority_2g_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 +24,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 +66,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 +92,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 {
|
||||||
@@ -99,9 +118,11 @@ 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(ImsiRequestedAnalyzer::new()));
|
||||||
|
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer{}));
|
||||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
||||||
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
|
|
||||||
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
||||||
|
|
||||||
harness
|
harness
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +196,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 {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||||
|
use super::information_element::{InformationElement, LteInformationElement};
|
||||||
|
use telcom_parser::lte_rrc::{DL_DCCH_Message, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions, RRCConnectionReleaseCriticalExtensions_c1, RedirectedCarrierInfo};
|
||||||
|
use super::util::unpack;
|
||||||
|
|
||||||
|
// Based on HITBSecConf presentation "Forcing a targeted LTE cellphone into an
|
||||||
|
// eavesdropping network" by Lin Huang
|
||||||
|
pub struct ConnectionRedirect2GDowngradeAnalyzer {
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones
|
||||||
|
impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
|
||||||
|
fn get_name(&self) -> Cow<str> {
|
||||||
|
Cow::from("Connection Release/Redirected Carrier 2G Downgrade")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_description(&self) -> Cow<str> {
|
||||||
|
Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||||
|
unpack!(InformationElement::LTE(lte_ie) = ie);
|
||||||
|
unpack!(LteInformationElement::DlDcch(DL_DCCH_Message { message }) = lte_ie);
|
||||||
|
unpack!(DL_DCCH_MessageType::C1(c1) = message);
|
||||||
|
unpack!(DL_DCCH_MessageType_c1::RrcConnectionRelease(release) = c1);
|
||||||
|
unpack!(RRCConnectionReleaseCriticalExtensions::C1(c1) = &release.critical_extensions);
|
||||||
|
unpack!(RRCConnectionReleaseCriticalExtensions_c1::RrcConnectionRelease_r8(r8_ies) = c1);
|
||||||
|
unpack!(Some(carrier_info) = &r8_ies.redirected_carrier_info);
|
||||||
|
match carrier_info {
|
||||||
|
RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event {
|
||||||
|
event_type: EventType::QualitativeWarning { severity: Severity::High },
|
||||||
|
message: format!("Detected 2G downgrade"),
|
||||||
|
}),
|
||||||
|
_ => Some(Event {
|
||||||
|
event_type: EventType::Informational,
|
||||||
|
message: format!("RRCConnectionRelease CarrierInfo: {:?}", carrier_info),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, ID type IMSI
|
||||||
|
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 identity 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 identity request detected"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,5 +1,8 @@
|
|||||||
pub mod analyzer;
|
pub mod analyzer;
|
||||||
pub mod information_element;
|
pub mod information_element;
|
||||||
pub mod lte_downgrade;
|
pub mod priority_2g_downgrade;
|
||||||
|
pub mod connection_redirect_downgrade;
|
||||||
pub mod imsi_provided;
|
pub mod imsi_provided;
|
||||||
|
pub mod imsi_requested;
|
||||||
pub mod null_cipher;
|
pub mod null_cipher;
|
||||||
|
pub mod util;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
// Unpacks a pattern, or returns None.
|
||||||
|
//
|
||||||
|
// # Examples
|
||||||
|
// You can use `unpack!` to unroll highly nested enums like this:
|
||||||
|
// ```
|
||||||
|
// enum Foo {
|
||||||
|
// A(Bar),
|
||||||
|
// B,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// enum Bar {
|
||||||
|
// C(Baz)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// struct Baz;
|
||||||
|
//
|
||||||
|
// fn get_bang(foo: Foo) -> Option<Baz> {
|
||||||
|
// unpack!(Foo::A(bar) = foo);
|
||||||
|
// unpack!(Bar::C(baz) = bar);
|
||||||
|
// baz
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
macro_rules! unpack {
|
||||||
|
($pat:pat = $val:expr) => {
|
||||||
|
let $pat = $val else { return None; };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is apparently how you make a macro publicly usable from this module
|
||||||
|
pub(crate) use unpack;
|
||||||
@@ -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")]
|
||||||
|
Downlink,
|
||||||
|
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
|
||||||
|
Uplink,
|
||||||
|
}
|
||||||
|
|
||||||
#[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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -200,6 +200,11 @@ pub struct GsmtapHeader {
|
|||||||
#[deku(update = "self.gsmtap_type.get_type()")]
|
#[deku(update = "self.gsmtap_type.get_type()")]
|
||||||
pub packet_type: u8,
|
pub packet_type: u8,
|
||||||
pub timeslot: u8,
|
pub timeslot: u8,
|
||||||
|
#[deku(bits = 1)]
|
||||||
|
pub pcs_band_indicator: bool,
|
||||||
|
#[deku(bits = 1)]
|
||||||
|
pub uplink: bool,
|
||||||
|
#[deku(bits = 14)]
|
||||||
pub arfcn: u16,
|
pub arfcn: u16,
|
||||||
pub signal_dbm: i8,
|
pub signal_dbm: i8,
|
||||||
pub signal_noise_ratio_db: u8,
|
pub signal_noise_ratio_db: u8,
|
||||||
@@ -222,6 +227,8 @@ impl GsmtapHeader {
|
|||||||
header_len: 4,
|
header_len: 4,
|
||||||
packet_type: gsmtap_type.get_type(),
|
packet_type: gsmtap_type.get_type(),
|
||||||
timeslot: 0,
|
timeslot: 0,
|
||||||
|
pcs_band_indicator: false,
|
||||||
|
uplink: false,
|
||||||
arfcn: 0,
|
arfcn: 0,
|
||||||
signal_dbm: 0,
|
signal_dbm: 0,
|
||||||
signal_noise_ratio_db: 0,
|
signal_noise_ratio_db: 0,
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
_ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)),
|
_ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)),
|
||||||
};
|
};
|
||||||
let mut header = GsmtapHeader::new(gsmtap_type);
|
let mut header = GsmtapHeader::new(gsmtap_type);
|
||||||
// Wireshark GSMTAP only accepts 14 bits of ARFCN
|
|
||||||
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
|
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
|
||||||
header.frame_number = packet.get_sfn();
|
header.frame_number = packet.get_sfn();
|
||||||
header.subslot = packet.get_subfn();
|
header.subslot = packet.get_subfn();
|
||||||
@@ -108,9 +107,10 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
payload: packet.take_payload(),
|
payload: packet.take_payload(),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
LogBody::Nas4GMessage { msg, .. } => {
|
LogBody::Nas4GMessage { msg, direction, .. } => {
|
||||||
// currently we only handle "plain" (i.e. non-secure) NAS messages
|
// currently we only handle "plain" (i.e. non-secure) NAS messages
|
||||||
let header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain));
|
let mut header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain));
|
||||||
|
header.uplink = matches!(direction, Nas4GMessageDirection::Uplink);
|
||||||
Ok(Some(GsmtapMessage {
|
Ok(Some(GsmtapMessage {
|
||||||
header,
|
header,
|
||||||
payload: msg,
|
payload: msg,
|
||||||
|
|||||||
@@ -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
@@ -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,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cargo build --release --target="armv7-unknown-linux-gnueabihf"
|
cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug
|
||||||
|
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
|
||||||
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
adb push target/armv7-unknown-linux-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 start"'
|
||||||
|
|||||||
@@ -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
@@ -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"] }
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -78,7 +78,10 @@ fn send_command<T: UsbContext>(handle: &mut DeviceHandle<T>, command: &str) {
|
|||||||
.read_bulk(0x82, &mut response, timeout)
|
.read_bulk(0x82, &mut response, timeout)
|
||||||
.expect("Failed to read response");
|
.expect("Failed to read response");
|
||||||
|
|
||||||
let responsestr = str::from_utf8(&response).expect("Failed to parse response");
|
// For some reason, on macOS the response buffer gets filled with garbage data that's
|
||||||
|
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
|
||||||
|
// the garbage with `from_utf8_lossy` and look for our expected success string.
|
||||||
|
let responsestr = String::from_utf8_lossy(&response);
|
||||||
if !responsestr.contains("\r\nOK\r\n") {
|
if !responsestr.contains("\r\nOK\r\n") {
|
||||||
println!("Received unexpected response{0}", responsestr);
|
println!("Received unexpected response{0}", responsestr);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
@@ -113,12 +116,16 @@ fn enable_command_mode<T: UsbContext>(context: &mut T) {
|
|||||||
/// Get a handle and contet for the orbic device
|
/// Get a handle and contet for the orbic device
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+60
@@ -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)
|
||||||
@@ -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()
|
||||||
Executable
+38
@@ -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)
|
||||||
Reference in New Issue
Block a user