mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 14:59:26 -07:00
Compare commits
65 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 | ||
|
|
f6681a3703 | ||
|
|
d6bc307a81 | ||
|
|
7cbb3369d8 | ||
|
|
cb3dbff54a | ||
|
|
65e1cd4967 | ||
|
|
d6fb54afb3 | ||
|
|
bc93c01890 | ||
|
|
be2d70325d | ||
|
|
5c4bd161fa |
24
.github/workflows/build-release.yml
vendored
24
.github/workflows/build-release.yml
vendored
@@ -8,16 +8,16 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build_serial:
|
||||
build_serial_and_check:
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- os: ubuntu-latest
|
||||
build_name: serial
|
||||
- os: windows-latest
|
||||
build_name: serial.exe
|
||||
serial_build_name: serial
|
||||
check_build_name: rayhunter-check
|
||||
- os: macos-latest
|
||||
build_name: serial
|
||||
serial_build_name: serial
|
||||
check_build_name: rayhunter-check
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -26,7 +26,15 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
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
|
||||
build_rootshell_and_rayhunter:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -56,14 +64,14 @@ jobs:
|
||||
if-no-files-found: error
|
||||
build_release_zip:
|
||||
needs:
|
||||
- build_serial
|
||||
- build_serial_and_check
|
||||
- build_rootshell_and_rayhunter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- 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
|
||||
run: mv rayhunter-daemon/rayhunter-daemon rootshell/rootshell serial-* dist
|
||||
- name: Archive release directory
|
||||
|
||||
193
Cargo.lock
generated
193
Cargo.lock
generated
@@ -482,6 +482,16 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.6"
|
||||
@@ -602,6 +612,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "derive-into-owned"
|
||||
version = "0.2.0"
|
||||
@@ -638,6 +657,16 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.72.0"
|
||||
@@ -654,6 +683,12 @@ dependencies = [
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.4"
|
||||
@@ -697,12 +732,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@@ -1160,6 +1189,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.11"
|
||||
@@ -1294,6 +1329,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
@@ -1344,6 +1385,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.32.2"
|
||||
@@ -1471,6 +1521,12 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
@@ -1545,19 +1601,6 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
|
||||
dependencies = [
|
||||
"fuchsia-cprng",
|
||||
"libc",
|
||||
"rand_core 0.3.1",
|
||||
"rdrand",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -1566,7 +1609,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core 0.6.4",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1576,24 +1619,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
||||
dependencies = [
|
||||
"rand_core 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
@@ -1629,7 +1657,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"paste",
|
||||
"profiling",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"simd_helpers",
|
||||
"system-deps",
|
||||
@@ -1691,7 +1719,8 @@ dependencies = [
|
||||
"rayhunter",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempdir",
|
||||
"simple_logger",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -1719,15 +1748,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
||||
dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -1766,15 +1786,6 @@ version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.37"
|
||||
@@ -1807,6 +1818,19 @@ version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
@@ -1918,6 +1942,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
@@ -2031,13 +2067,15 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempdir"
|
||||
version = "0.3.7"
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
dependencies = [
|
||||
"rand 0.4.6",
|
||||
"remove_dir_all",
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2080,6 +2118,39 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tokio"
|
||||
version = "1.36.0"
|
||||
|
||||
73
README.md
73
README.md
@@ -1,44 +1,29 @@
|
||||

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

|
||||
|
||||
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**
|
||||
|
||||
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/)
|
||||
|
||||
## The Hardware
|
||||
|
||||
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/Orbic-Verizon-Hotspot-Connect-Enabled/dp/B08N3CHC4Y)
|
||||
Or on [Ebay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l)
|
||||
|
||||
## 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).
|
||||
2. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and unzip it.
|
||||
3. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`).
|
||||
4. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
|
||||
*NOTE: We don't currently support automated installs on windows, you will have to follow the manual install instructions below*
|
||||
|
||||
1. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it.
|
||||
**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
|
||||
|
||||
@@ -46,13 +31,17 @@ 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!)
|
||||
* 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
|
||||
* 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
|
||||
* 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.
|
||||
* run `sudo apt install build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf`
|
||||
|
||||
* set up cross compliing for rust:
|
||||
@@ -61,32 +50,32 @@ rustup target add x86_64-unknown-linux-gnu
|
||||
rustup target add armv7-unknown-linux-gnueabihf
|
||||
```
|
||||
|
||||
Now you can root your device and install rayhunter by running `./install.sh` - **Note:** You will have to install the cross compile tooling below before running this.
|
||||
Now you can root your device and install rayhunter by running `./tools/install-dev.sh`
|
||||
|
||||
### If you aren't on linux or can't run the install scripts
|
||||
### If you are on windows or can't run the install scripts
|
||||
* Root your device on windows using the instructions here: https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183
|
||||
|
||||
* Build for arm using `cargo build`
|
||||
* Build for arm using `cargo build`
|
||||
|
||||
* Run tests using `cargo test_pc`
|
||||
|
||||
* Push the scripts in `scripts/` to /etc/init.d on device and make a directory called /data/rayhunter using `adb shell` (and sshell for your root shell if you followed the steps above)
|
||||
* Push the scripts in `scripts/` to /etc/init.d on device and make a directory called /data/rayhunter using `adb shell` (and sshell for your root shell if you followed the steps above)
|
||||
|
||||
* you also need to copy `config.toml.example` to /data/rayhunter/config.toml
|
||||
|
||||
* Then run `./make.sh` this will build the binary and push it over adb. Restart your device or run `/etc/init.d/rayhunter_daemon start` on the device and you are good to go.
|
||||
* Then run `./make.sh` this will build the binary and push it over adb. Restart your device or run `/etc/init.d/rayhunter_daemon start` on the device and you are good to go.
|
||||
|
||||
* Write your code and write tests
|
||||
* Write your code and write tests
|
||||
|
||||
* Build for arm using `cargo build`
|
||||
* Build for arm using `cargo build`
|
||||
|
||||
* Run tests using `cargo test_pc`
|
||||
|
||||
* push to the device with `./make.sh`
|
||||
|
||||
## Documentation
|
||||
## Documentation
|
||||
* Build docs locallly using `RUSTDOCFLAGS="--cfg docsrs" cargo doc --no-deps --all-features --open`
|
||||
|
||||
**LEGAL DISCLAIMER:** Use this program at your own risk. We beilieve running this program does not currently violate any laws or regulations in the United States. However, we are not responsible for civil or criminal liability resulting from the use of this software. If you are located outside of the US please consult with an attorney in your country to help you assess the legal risks of running this program.
|
||||
**LEGAL DISCLAIMER:** Use this program at your own risk. We beilieve running this program does not currently violate any laws or regulations in the United States. However, we are not responsible for civil or criminal liability resulting from the use of this software. If you are located outside of the US please consult with an attorney in your country to help you assess the legal risks of running this program.
|
||||
|
||||
*Good Hunting!*
|
||||
|
||||
@@ -25,10 +25,11 @@ tokio-util = { version = "0.7.10", features = ["rt"] }
|
||||
futures-macro = "0.3.30"
|
||||
include_dir = "0.7.3"
|
||||
mime_guess = "2.0.4"
|
||||
tempdir = "0.3.7"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
tokio-stream = "0.1.14"
|
||||
futures = "0.3.30"
|
||||
clap = { version = "4.5.2", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
image = "0.25.1"
|
||||
tempfile = "3.10.1"
|
||||
simple_logger = "5.0.0"
|
||||
|
||||
248
bin/src/analysis.rs
Normal file
248
bin/src/analysis.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
use std::sync::Arc;
|
||||
use std::{future, pin};
|
||||
|
||||
use axum::Json;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use log::{debug, error, info};
|
||||
use rayhunter::analysis::analyzer::Harness;
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::qmdl::QmdlReader;
|
||||
use serde::Serialize;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncWriteExt, BufWriter};
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::{RwLock, RwLockWriteGuard};
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::server::ServerState;
|
||||
use crate::dummy_analyzer::TestAnalyzer;
|
||||
|
||||
pub struct AnalysisWriter {
|
||||
writer: BufWriter<File>,
|
||||
harness: Harness,
|
||||
bytes_written: usize,
|
||||
}
|
||||
|
||||
// We write our analysis results to a file immediately to minimize the amount of
|
||||
// state Rayhunter has to keep track of in memory. The analysis file's format is
|
||||
// Newline Delimited JSON
|
||||
// (https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson), which
|
||||
// lets us simply append new rows to the end without parsing the entire JSON
|
||||
// object beforehand.
|
||||
impl AnalysisWriter {
|
||||
pub async fn new(file: File, enable_dummy_analyzer: bool) -> Result<Self, std::io::Error> {
|
||||
let mut harness = Harness::new_with_all_analyzers();
|
||||
if enable_dummy_analyzer {
|
||||
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
|
||||
}
|
||||
|
||||
let mut result = Self {
|
||||
writer: BufWriter::new(file),
|
||||
bytes_written: 0,
|
||||
harness,
|
||||
};
|
||||
let metadata = result.harness.get_metadata();
|
||||
result.write(&metadata).await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// Runs the analysis harness on the given container, serializing the results
|
||||
// to the analysis file and returning the file's new length.
|
||||
pub async fn analyze(&mut self, container: MessagesContainer) -> Result<(usize, bool), std::io::Error> {
|
||||
let row = self.harness.analyze_qmdl_messages(container);
|
||||
if !row.is_empty() {
|
||||
self.write(&row).await?;
|
||||
}
|
||||
Ok((self.bytes_written, row.contains_warnings()))
|
||||
}
|
||||
|
||||
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
||||
let mut value_str = serde_json::to_string(value).unwrap();
|
||||
value_str.push('\n');
|
||||
self.bytes_written += value_str.len();
|
||||
self.writer.write_all(value_str.as_bytes()).await?;
|
||||
self.writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Flushes any pending I/O to disk before dropping the writer
|
||||
pub async fn close(mut self) -> Result<(), std::io::Error> {
|
||||
self.writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Default)]
|
||||
pub struct AnalysisStatus {
|
||||
queued: Vec<String>,
|
||||
running: Option<String>,
|
||||
}
|
||||
|
||||
pub enum AnalysisCtrlMessage {
|
||||
NewFilesQueued,
|
||||
Exit,
|
||||
}
|
||||
|
||||
async fn queued_len(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -> usize {
|
||||
analysis_status_lock.read().await.queued.len()
|
||||
}
|
||||
|
||||
async fn dequeue_to_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -> String {
|
||||
let mut analysis_status = analysis_status_lock.write().await;
|
||||
let name = analysis_status.queued.remove(0);
|
||||
assert!(analysis_status.running.is_none());
|
||||
analysis_status.running = Some(name.clone());
|
||||
name
|
||||
}
|
||||
|
||||
async fn clear_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) {
|
||||
let mut analysis_status = analysis_status_lock.write().await;
|
||||
analysis_status.running = None;
|
||||
}
|
||||
|
||||
async fn perform_analysis(
|
||||
name: &str,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
enable_dummy_analyzer: bool,
|
||||
) -> Result<(), String> {
|
||||
info!("Opening QMDL and analysis file for {}...", name);
|
||||
let (analysis_file, qmdl_file, entry_index) = {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let (entry_index, _) = qmdl_store
|
||||
.entry_for_name(&name)
|
||||
.ok_or(format!("failed to find QMDL store entry for {}", name))?;
|
||||
let analysis_file = qmdl_store
|
||||
.clear_and_open_entry_analysis(entry_index)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
let qmdl_file = qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
|
||||
(analysis_file, qmdl_file, entry_index)
|
||||
};
|
||||
|
||||
let mut analysis_writer = AnalysisWriter::new(analysis_file, enable_dummy_analyzer)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
let file_size = qmdl_file
|
||||
.metadata()
|
||||
.await
|
||||
.expect("failed to get QMDL file metadata")
|
||||
.len();
|
||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||
let mut qmdl_stream = pin::pin!(qmdl_reader
|
||||
.as_stream()
|
||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||
|
||||
info!("Starting analysis for {}...", name);
|
||||
while let Some(container) = qmdl_stream
|
||||
.try_next()
|
||||
.await
|
||||
.expect("failed getting QMDL container")
|
||||
{
|
||||
let (size_bytes, _) = analysis_writer
|
||||
.analyze(container)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
debug!("{} analysis: {} bytes written", name, size_bytes);
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
qmdl_store
|
||||
.update_entry_analysis_size(entry_index, size_bytes)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
}
|
||||
|
||||
analysis_writer
|
||||
.close()
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
info!("Analysis for {} complete!", name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_analysis_thread(
|
||||
task_tracker: &TaskTracker,
|
||||
mut analysis_rx: Receiver<AnalysisCtrlMessage>,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||
enable_dummy_analyzer: bool,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
loop {
|
||||
match analysis_rx.recv().await {
|
||||
Some(AnalysisCtrlMessage::NewFilesQueued) => {
|
||||
let count = queued_len(analysis_status_lock.clone()).await;
|
||||
for _ in 0..count {
|
||||
let name = dequeue_to_running(analysis_status_lock.clone()).await;
|
||||
if let Err(err) = perform_analysis(&name, qmdl_store_lock.clone(), enable_dummy_analyzer).await {
|
||||
error!("failed to analyze {}: {}", name, err);
|
||||
}
|
||||
clear_running(analysis_status_lock.clone()).await;
|
||||
}
|
||||
}
|
||||
Some(AnalysisCtrlMessage::Exit) | None => return,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_analysis_status(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<AnalysisStatus>, (StatusCode, String)> {
|
||||
Ok(Json(state.analysis_status_lock.read().await.clone()))
|
||||
}
|
||||
|
||||
fn queue_qmdl(name: &str, analysis_status: &mut RwLockWriteGuard<AnalysisStatus>) -> bool {
|
||||
if analysis_status.queued.iter().any(|n| n == name)
|
||||
|| analysis_status.running.iter().any(|n| n == name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
analysis_status.queued.push(name.to_string());
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn start_analysis(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(qmdl_name): Path<String>,
|
||||
) -> Result<(StatusCode, Json<AnalysisStatus>), (StatusCode, String)> {
|
||||
let mut analysis_status = state.analysis_status_lock.write().await;
|
||||
let store = state.qmdl_store_lock.read().await;
|
||||
let queued = if qmdl_name.is_empty() {
|
||||
let mut entry_names: Vec<&str> = store
|
||||
.manifest
|
||||
.entries
|
||||
.iter()
|
||||
.map(|e| e.name.as_str())
|
||||
.collect();
|
||||
if let Some(current_entry) = store.current_entry {
|
||||
entry_names.remove(current_entry);
|
||||
}
|
||||
entry_names
|
||||
.iter()
|
||||
.any(|name| queue_qmdl(name, &mut analysis_status))
|
||||
} else {
|
||||
queue_qmdl(&qmdl_name, &mut analysis_status)
|
||||
};
|
||||
if queued {
|
||||
state
|
||||
.analysis_sender
|
||||
.send(AnalysisCtrlMessage::NewFilesQueued)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("failed to queue new analysis files: {:?}", e),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok((StatusCode::ACCEPTED, Json(analysis_status.clone())))
|
||||
}
|
||||
144
bin/src/check.rs
144
bin/src/check.rs
@@ -1,31 +1,147 @@
|
||||
use std::{future, path::PathBuf, pin::pin};
|
||||
use rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader};
|
||||
use tokio::fs::File;
|
||||
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
|
||||
use log::{info, warn};
|
||||
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 futures::TryStreamExt;
|
||||
|
||||
mod dummy_analyzer;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
struct Args {
|
||||
#[arg(short, long)]
|
||||
#[arg(short = 'p', long)]
|
||||
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 main() {
|
||||
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");
|
||||
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
|
||||
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||
let file_size = qmdl_file.metadata().await.expect("failed to get QMDL file metadata").len();
|
||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
|
||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||
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") {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,20 @@ use serde::Deserialize;
|
||||
struct ConfigFile {
|
||||
qmdl_store_path: Option<String>,
|
||||
port: Option<u16>,
|
||||
readonly_mode: Option<bool>,
|
||||
debug_mode: Option<bool>,
|
||||
ui_level: Option<u8>,
|
||||
enable_dummy_analyzer: Option<bool>,
|
||||
colorblind_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub qmdl_store_path: String,
|
||||
pub port: u16,
|
||||
pub readonly_mode: bool,
|
||||
pub debug_mode: bool,
|
||||
pub ui_level: u8,
|
||||
pub enable_dummy_analyzer: bool,
|
||||
pub colorblind_mode: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -23,8 +27,10 @@ impl Default for Config {
|
||||
Config {
|
||||
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
|
||||
port: 8080,
|
||||
readonly_mode: false,
|
||||
debug_mode: false,
|
||||
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) {
|
||||
let parsed_config: ConfigFile = toml::from_str(&config_file)
|
||||
.map_err(RayhunterError::ConfigFileParsingError)?;
|
||||
if let Some(path) = parsed_config.qmdl_store_path { config.qmdl_store_path = path }
|
||||
if let Some(port) = parsed_config.port { config.port = port }
|
||||
if let Some(readonly_mode) = parsed_config.readonly_mode { config.readonly_mode = readonly_mode }
|
||||
if let Some(ui_level) = parsed_config.ui_level { config.ui_level = ui_level }
|
||||
parsed_config.qmdl_store_path.map(|v| config.qmdl_store_path = v);
|
||||
parsed_config.port.map(|v| config.port = v);
|
||||
parsed_config.debug_mode.map(|v| config.debug_mode = v);
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod analysis;
|
||||
mod config;
|
||||
mod error;
|
||||
mod pcap;
|
||||
@@ -6,6 +7,7 @@ mod stats;
|
||||
mod qmdl_store;
|
||||
mod diag;
|
||||
mod framebuffer;
|
||||
mod dummy_analyzer;
|
||||
|
||||
use crate::config::{parse_config, parse_args};
|
||||
use crate::diag::run_diag_read_thread;
|
||||
@@ -16,6 +18,7 @@ use crate::stats::get_system_stats;
|
||||
use crate::error::RayhunterError;
|
||||
use crate::framebuffer::Framebuffer;
|
||||
|
||||
use analysis::{get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus};
|
||||
use axum::response::Redirect;
|
||||
use diag::{get_analysis_report, start_recording, stop_recording, DiagDeviceCtrlMessage};
|
||||
use log::{info, error};
|
||||
@@ -23,7 +26,7 @@ use rayhunter::diag_device::DiagDevice;
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
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::task::JoinHandle;
|
||||
use tokio_util::task::TaskTracker;
|
||||
@@ -43,12 +46,20 @@ async fn run_server(
|
||||
config: &config::Config,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
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<()> {
|
||||
info!("spinning up server");
|
||||
let state = Arc::new(ServerState {
|
||||
qmdl_store_lock,
|
||||
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()
|
||||
@@ -58,7 +69,9 @@ async fn run_server(
|
||||
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
|
||||
.route("/api/start-recording", post(start_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("/*path", get(serve_static))
|
||||
.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
|
||||
// readonly mode.
|
||||
// debug mode.
|
||||
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?),
|
||||
(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,
|
||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||
server_shutdown_tx: oneshot::Sender<()>,
|
||||
ui_shutdown_tx: oneshot::Sender<()>,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>
|
||||
maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
analysis_tx: Sender<AnalysisCtrlMessage>,
|
||||
) -> JoinHandle<Result<(), RayhunterError>> {
|
||||
task_tracker.spawn(async move {
|
||||
match tokio::signal::ctrl_c().await {
|
||||
@@ -110,10 +124,14 @@ fn run_ctrl_c_thread(
|
||||
server_shutdown_tx.send(())
|
||||
.expect("couldn't send server shutdown signal");
|
||||
info!("sending UI shutdown");
|
||||
ui_shutdown_tx.send(())
|
||||
.expect("couldn't send ui shutdown signal");
|
||||
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
|
||||
ui_shutdown_tx.send(())
|
||||
.expect("couldn't send ui shutdown signal");
|
||||
}
|
||||
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
|
||||
.expect("couldn't send Exit message to diag thread");
|
||||
analysis_tx.send(AnalysisCtrlMessage::Exit).await
|
||||
.expect("couldn't send Exit message to analysis thread");
|
||||
},
|
||||
Err(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/");
|
||||
let mut display_color: framebuffer::Color565;
|
||||
let display_level = config.ui_level;
|
||||
if display_level == 0 {
|
||||
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 || {
|
||||
let mut fb: Framebuffer = Framebuffer::new();
|
||||
// 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(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 {
|
||||
2 => {
|
||||
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);
|
||||
},
|
||||
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]
|
||||
@@ -183,25 +214,36 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
// TaskTrackers give us an interface to spawn tokio threads, and then
|
||||
// eventually await all of them ending
|
||||
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 (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
|
||||
.map_err(RayhunterError::DiagInitError)?;
|
||||
dev.config_logs().await
|
||||
.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::<()>();
|
||||
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, ui_shutdown_tx, qmdl_store_lock.clone());
|
||||
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx).await;
|
||||
update_ui(&task_tracker, &config, ui_shutdown_rx).await;
|
||||
info!("create shutdown thread");
|
||||
let analysis_status_lock = Arc::new(RwLock::new(AnalysisStatus::default()));
|
||||
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.wait().await;
|
||||
|
||||
info!("see you space cowboy...");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
112
bin/src/diag.rs
112
bin/src/diag.rs
@@ -2,26 +2,25 @@ use std::pin::pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::State;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use rayhunter::analysis::analyzer::Harness;
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::diag::DataType;
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use rayhunter::qmdl::QmdlWriter;
|
||||
use log::{debug, error, info};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{BufWriter, AsyncWriteExt};
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tokio_util::task::TaskTracker;
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
|
||||
use crate::framebuffer;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::server::ServerState;
|
||||
use crate::analysis::AnalysisWriter;
|
||||
|
||||
pub enum DiagDeviceCtrlMessage {
|
||||
StopRecording,
|
||||
@@ -29,67 +28,19 @@ pub enum DiagDeviceCtrlMessage {
|
||||
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(
|
||||
task_tracker: &TaskTracker,
|
||||
mut dev: DiagDevice,
|
||||
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 {
|
||||
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 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"));
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -100,7 +51,7 @@ pub fn run_diag_read_thread(
|
||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||
}
|
||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file).await
|
||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
|
||||
.expect("failed to write to analysis file"));
|
||||
},
|
||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||
@@ -143,8 +94,14 @@ pub fn run_diag_read_thread(
|
||||
}
|
||||
|
||||
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");
|
||||
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 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
|
||||
@@ -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)> {
|
||||
if state.readonly_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_string()));
|
||||
if state.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await
|
||||
@@ -172,30 +129,47 @@ pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(S
|
||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||
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)))?;
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
pub async fn stop_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
if state.readonly_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_string()));
|
||||
if state.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
qmdl_store.close_current_entry().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't close current qmdl entry: {}", e)))?;
|
||||
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StopRecording).await
|
||||
.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()))
|
||||
}
|
||||
|
||||
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 Some(entry) = qmdl_store.get_current_entry() else {
|
||||
return Err((
|
||||
let (entry_index, _) = if qmdl_name == "live" {
|
||||
qmdl_store.get_current_entry().ok_or((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"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)))?;
|
||||
let analysis_stream = ReaderStream::new(analysis_file);
|
||||
|
||||
|
||||
45
bin/src/dummy_analyzer.rs
Normal file
45
bin/src/dummy_analyzer.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,6 @@ pub enum RayhunterError{
|
||||
TokioError(#[from] tokio::io::Error),
|
||||
#[error("QmdlStore error: {0}")]
|
||||
QmdlStoreError(#[from] RecordingStoreError),
|
||||
#[error("No QMDL store found at path {0}, but can't create a new one due to readonly mode")]
|
||||
NoStoreReadonlyMode(String),
|
||||
#[error("No QMDL store found at path {0}, but can't create a new one due to debug mode")]
|
||||
NoStoreDebugMode(String),
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ struct Dimensions {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Color565 {
|
||||
Red = 0b1111100000000000,
|
||||
Green = 0b0000011111100000,
|
||||
@@ -22,6 +23,24 @@ pub enum Color565 {
|
||||
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)]
|
||||
pub struct Framebuffer<'a> {
|
||||
dimensions: Dimensions,
|
||||
|
||||
@@ -21,7 +21,7 @@ use futures::TryStreamExt;
|
||||
// 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)> {
|
||||
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)))?;
|
||||
if entry.qmdl_size_bytes == 0 {
|
||||
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()
|
||||
));
|
||||
}
|
||||
|
||||
let qmdl_file = qmdl_store.open_entry_qmdl(&entry).await
|
||||
let qmdl_size_bytes = entry.qmdl_size_bytes;
|
||||
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
||||
// the QMDL reader should stop at the last successfully written data chunk
|
||||
// (entry.size_bytes)
|
||||
@@ -39,10 +39,10 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
|
||||
pcap_writer.write_iface_header().await.unwrap();
|
||||
|
||||
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()
|
||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||
|
||||
|
||||
while let Some(container) = messages_stream.try_next().await.expect("failed getting QMDL container") {
|
||||
for maybe_msg in container.into_messages() {
|
||||
match maybe_msg {
|
||||
|
||||
@@ -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 serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use tokio::{
|
||||
fs::{self, try_exists, File, OpenOptions},
|
||||
io::AsyncWriteExt,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RecordingStoreError {
|
||||
@@ -19,7 +22,7 @@ pub enum RecordingStoreError {
|
||||
#[error("Couldn't write manifest file: {0}")]
|
||||
WriteManifestError(tokio::io::Error),
|
||||
#[error("Couldn't parse QMDL store manifest file: {0}")]
|
||||
ParseManifestError(toml::de::Error)
|
||||
ParseManifestError(toml::de::Error),
|
||||
}
|
||||
|
||||
pub struct RecordingStore {
|
||||
@@ -70,16 +73,26 @@ impl ManifestEntry {
|
||||
impl RecordingStore {
|
||||
// Returns whether a directory with a "manifest.toml" exists at the given
|
||||
// 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 dir_exists = try_exists(path).await.map_err(RecordingStoreError::OpenDirError)?;
|
||||
let manifest_exists = try_exists(manifest_path).await.map_err(RecordingStoreError::ReadManifestError)?;
|
||||
let dir_exists = try_exists(path)
|
||||
.await
|
||||
.map_err(RecordingStoreError::OpenDirError)?;
|
||||
let manifest_exists = try_exists(manifest_path)
|
||||
.await
|
||||
.map_err(RecordingStoreError::ReadManifestError)?;
|
||||
Ok(dir_exists && manifest_exists)
|
||||
}
|
||||
|
||||
// Loads an existing RecordingStore at the given path. Errors if no store exists,
|
||||
// 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 manifest = RecordingStore::read_manifest(&path).await?;
|
||||
Ok(RecordingStore {
|
||||
@@ -91,26 +104,38 @@ impl RecordingStore {
|
||||
|
||||
// Creates a new RecordingStore at the given path. This involves creating a dir
|
||||
// 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");
|
||||
fs::create_dir_all(&path).await
|
||||
fs::create_dir_all(&path)
|
||||
.await
|
||||
.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)?;
|
||||
let empty_manifest = Manifest { entries: Vec::new() };
|
||||
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
|
||||
let empty_manifest = Manifest {
|
||||
entries: Vec::new(),
|
||||
};
|
||||
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)?;
|
||||
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 file_contents = fs::read_to_string(&manifest_path).await
|
||||
let file_contents = fs::read_to_string(&manifest_path)
|
||||
.await
|
||||
.map_err(RecordingStoreError::ReadManifestError)?;
|
||||
toml::from_str(&file_contents)
|
||||
.map_err(RecordingStoreError::ParseManifestError)
|
||||
toml::from_str(&file_contents).map_err(RecordingStoreError::ParseManifestError)
|
||||
}
|
||||
|
||||
// Closes the current entry (if needed), creates a new entry based on the
|
||||
@@ -126,13 +151,15 @@ impl RecordingStore {
|
||||
let qmdl_file = File::options()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(&qmdl_filepath).await
|
||||
.open(&qmdl_filepath)
|
||||
.await
|
||||
.map_err(RecordingStoreError::CreateFileError)?;
|
||||
let analysis_filepath = new_entry.get_analysis_filepath(&self.path);
|
||||
let analysis_file = File::options()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(&analysis_filepath).await
|
||||
.open(&analysis_filepath)
|
||||
.await
|
||||
.map_err(RecordingStoreError::CreateFileError)?;
|
||||
self.manifest.entries.push(new_entry);
|
||||
self.current_entry = Some(self.manifest.entries.len() - 1);
|
||||
@@ -141,37 +168,71 @@ impl RecordingStore {
|
||||
}
|
||||
|
||||
// Returns the corresponding QMDL file for a given entry
|
||||
pub async fn open_entry_qmdl(&self, entry: &ManifestEntry) -> Result<File, RecordingStoreError> {
|
||||
File::open(entry.get_qmdl_filepath(&self.path)).await
|
||||
pub async fn open_entry_qmdl(
|
||||
&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)
|
||||
}
|
||||
|
||||
// Returns the corresponding QMDL file for a given entry
|
||||
pub async fn open_entry_analysis(&self, entry: &ManifestEntry) -> Result<File, RecordingStoreError> {
|
||||
File::open(entry.get_analysis_filepath(&self.path)).await
|
||||
pub async fn open_entry_analysis(
|
||||
&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)
|
||||
}
|
||||
|
||||
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
|
||||
pub async fn close_current_entry(&mut self) -> Result<(), RecordingStoreError> {
|
||||
match self.current_entry {
|
||||
Some(_) => {
|
||||
self.current_entry = None;
|
||||
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
|
||||
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].last_message_time = Some(Local::now());
|
||||
self.write_manifest().await
|
||||
}
|
||||
|
||||
// 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.write_manifest().await
|
||||
}
|
||||
@@ -179,36 +240,45 @@ impl RecordingStore {
|
||||
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
|
||||
let mut manifest_file = File::options()
|
||||
.write(true)
|
||||
.open(self.path.join("manifest.toml")).await
|
||||
.open(self.path.join("manifest.toml"))
|
||||
.await
|
||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||
let manifest_contents = toml::to_string_pretty(&self.manifest)
|
||||
.expect("failed to serialize manifest");
|
||||
manifest_file.write_all(manifest_contents.as_bytes()).await
|
||||
let manifest_contents =
|
||||
toml::to_string_pretty(&self.manifest).expect("failed to serialize manifest");
|
||||
manifest_file
|
||||
.write_all(manifest_contents.as_bytes())
|
||||
.await
|
||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Finds an entry by filename
|
||||
pub fn entry_for_name(&self, name: &str) -> Option<ManifestEntry> {
|
||||
self.manifest.entries.iter()
|
||||
.find(|entry| entry.name == name)
|
||||
.cloned()
|
||||
pub fn entry_for_name(&self, name: &str) -> Option<(usize, &ManifestEntry)> {
|
||||
let entry_index = self.manifest
|
||||
.entries
|
||||
.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?;
|
||||
self.manifest.entries.get(entry_index)
|
||||
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempdir::TempDir;
|
||||
use super::*;
|
||||
use tempfile::{Builder, TempDir};
|
||||
|
||||
fn make_temp_dir() -> TempDir {
|
||||
Builder::new().prefix("qmdl_store_test").tempdir().unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_from_empty_dir() {
|
||||
let dir = TempDir::new("qmdl_store_test").unwrap();
|
||||
let dir = make_temp_dir();
|
||||
assert!(!RecordingStore::exists(dir.path()).await.unwrap());
|
||||
let _created_store = RecordingStore::create(dir.path()).await.unwrap();
|
||||
assert!(RecordingStore::exists(dir.path()).await.unwrap());
|
||||
@@ -218,26 +288,42 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_creating_updating_and_closing_entries() {
|
||||
let dir = TempDir::new("qmdl_store_test").unwrap();
|
||||
let dir = make_temp_dir();
|
||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||
let _ = store.new_entry().await.unwrap();
|
||||
let entry_index = store.current_entry.unwrap();
|
||||
assert_eq!(RecordingStore::read_manifest(dir.path()).await.unwrap(), store.manifest);
|
||||
assert!(store.manifest.entries[entry_index].last_message_time.is_none());
|
||||
assert_eq!(
|
||||
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();
|
||||
let entry = store.entry_for_name(&store.manifest.entries[entry_index].name).unwrap();
|
||||
store
|
||||
.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_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();
|
||||
assert!(matches!(store.close_current_entry().await, Err(RecordingStoreError::NoCurrentEntry)));
|
||||
assert!(matches!(
|
||||
store.close_current_entry().await,
|
||||
Err(RecordingStoreError::NoCurrentEntry)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_repeated_new_entries() {
|
||||
let dir = TempDir::new("qmdl_store_test").unwrap();
|
||||
let dir = make_temp_dir();
|
||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||
let _ = store.new_entry().await.unwrap();
|
||||
let entry_index = store.current_entry.unwrap();
|
||||
|
||||
@@ -4,6 +4,7 @@ use axum::extract::State;
|
||||
use axum::http::{StatusCode, HeaderValue};
|
||||
use axum::response::{Response, IntoResponse};
|
||||
use axum::extract::Path;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
@@ -11,20 +12,26 @@ use tokio::sync::RwLock;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use include_dir::{include_dir, Dir};
|
||||
|
||||
use crate::DiagDeviceCtrlMessage;
|
||||
use crate::{framebuffer, DiagDeviceCtrlMessage};
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
|
||||
pub struct ServerState {
|
||||
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
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)> {
|
||||
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
let entry = qmdl_store.entry_for_name(&qmdl_name)
|
||||
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
|
||||
let qmdl_file = qmdl_store.open_entry_qmdl(&entry).await
|
||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx)
|
||||
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_idx)))?;
|
||||
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
|
||||
.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 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
|
||||
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 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) {
|
||||
None => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
|
||||
@@ -22,6 +22,11 @@ th[scope='row'] {
|
||||
}
|
||||
|
||||
tr.current {
|
||||
background-color: #53fe7b;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr.warning {
|
||||
background-color: #fe537b;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -27,15 +27,16 @@
|
||||
<th scope="col">Size (bytes)</th>
|
||||
<th scope="col">PCAP</th>
|
||||
<th scope="col">QMDL</th>
|
||||
<th scope="col">Analysis Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div>
|
||||
<h3>System stats</h3>
|
||||
<h3>Live System stats</h3>
|
||||
<pre id="system-stats">Loading...</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Analysis Report</h3>
|
||||
<h3>Analysis Report of Current Capture</h3>
|
||||
<pre id="analysis-report">Loading...</pre>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -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() {
|
||||
const systemStats = await getSystemStats();
|
||||
const systemStatsDiv = document.getElementById('system-stats');
|
||||
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
|
||||
|
||||
const analysisReport = await getAnalysisReport();
|
||||
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();
|
||||
await updateAnalysisStatus(qmdlManifest);
|
||||
await updateAnalysisResults(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) {
|
||||
const table = document.getElementById('qmdl-manifest-table');
|
||||
const numRows = table.rows.length;
|
||||
@@ -18,43 +107,55 @@ function updateQmdlManifestTable(manifest) {
|
||||
table.deleteRow(1);
|
||||
}
|
||||
if (manifest.current_entry) {
|
||||
const row = createEntryRow(manifest.current_entry);
|
||||
const row = createEntryRow(manifest.current_entry, true);
|
||||
row.classList.add('current');
|
||||
table.appendChild(row)
|
||||
}
|
||||
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 name = document.createElement('th');
|
||||
name.scope = 'row';
|
||||
name.innerText = entry.name;
|
||||
row.appendChild(name);
|
||||
|
||||
for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) {
|
||||
const td = document.createElement('td');
|
||||
td.innerText = entry[key];
|
||||
row.appendChild(td);
|
||||
}
|
||||
const pcap_td = document.createElement('td');
|
||||
const pcap_link = document.createElement('a');
|
||||
pcap_link.href = `/api/pcap/${entry.name}`;
|
||||
pcap_link.innerText = 'pcap';
|
||||
pcap_td.appendChild(pcap_link);
|
||||
row.appendChild(pcap_td);
|
||||
const qmdl_td = document.createElement('td');
|
||||
const qmdl_link = document.createElement('a');
|
||||
qmdl_link.href = `/api/qmdl/${entry.name}`;
|
||||
qmdl_link.innerText = 'qmdl';
|
||||
qmdl_td.appendChild(qmdl_link);
|
||||
row.appendChild(qmdl_td);
|
||||
|
||||
const pcapTd = document.createElement('td');
|
||||
pcapTd.appendChild(createLink(`/api/pcap/${entry.name}`, 'pcap'));
|
||||
row.appendChild(pcapTd);
|
||||
|
||||
const qmdlTd = document.createElement('td');
|
||||
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}.qmdl`, 'qmdl'));
|
||||
row.appendChild(qmdlTd);
|
||||
|
||||
const analysisResult = document.createElement('td');
|
||||
analysisResult.innerHTML = entry.analysis_result;
|
||||
if (entry.analysis.warnings.length > 0) {
|
||||
row.classList.add("warning");
|
||||
}
|
||||
row.appendChild(analysisResult);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
async function getAnalysisReport() {
|
||||
const rows = await req('GET', '/api/analysis-report');
|
||||
async function getAnalysisReport(name) {
|
||||
const rows = await req('GET', `/api/analysis-report/${name}`);
|
||||
return rows.split('\n')
|
||||
.filter(row => row.length > 0)
|
||||
.map(row => JSON.parse(row));
|
||||
@@ -67,6 +168,8 @@ async function getSystemStats() {
|
||||
async function getQmdlManifest() {
|
||||
const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest'));
|
||||
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);
|
||||
if (manifest.current_entry.last_message_time === undefined) {
|
||||
manifest.current_entry.last_message_time = "N/A";
|
||||
@@ -75,6 +178,8 @@ async function getQmdlManifest() {
|
||||
}
|
||||
}
|
||||
for (entry of manifest.entries) {
|
||||
entry.status = STATUS_NEEDS_UPDATE;
|
||||
entry.analysis_result = 'Waiting...';
|
||||
entry.start_time = new Date(entry.start_time);
|
||||
entry.last_message_time = new Date(entry.last_message_time);
|
||||
}
|
||||
|
||||
12
dist/config.toml.example
vendored
12
dist/config.toml.example
vendored
@@ -1,10 +1,12 @@
|
||||
# cat config.toml
|
||||
qmdl_store_path = "/data/rayhunter/qmdl"
|
||||
port = 8080
|
||||
readonly_mode = false
|
||||
# UI Levels:
|
||||
# 0 = invisible mode, no indicator that rayhunter is running
|
||||
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
|
||||
# 2 = Demo Mode, display a fun orca gif
|
||||
debug_mode = false
|
||||
enable_dummy_analyzer = false
|
||||
colorblind_mode = false
|
||||
# UI Levels:
|
||||
# 0 = invisible mode, no indicator that rayhunter is running
|
||||
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
|
||||
# 2 = Demo Mode, display a fun orca gif
|
||||
# 3 = display the EFF logo
|
||||
ui_level = 1
|
||||
|
||||
93
dist/install-common.sh
vendored
93
dist/install-common.sh
vendored
@@ -1,97 +1,110 @@
|
||||
#!/bin/env bash
|
||||
|
||||
#!/usr/bin/env bash
|
||||
install() {
|
||||
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
|
||||
fi
|
||||
check_adb
|
||||
force_debug_mode
|
||||
setup_rootshell
|
||||
setup_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() {
|
||||
echo "Using adb at $ADB"
|
||||
echo "Force a switch into the debug mode to enable ADB"
|
||||
"$SERIAL_PATH" --root
|
||||
echo -n "adb enabled, waiting for reboot..."
|
||||
wait_for_adb_shell
|
||||
echo " it's alive!"
|
||||
echo -n "waiting for atfwd_daemon to startup..."
|
||||
wait_for_atfwd_daemon
|
||||
echo " done!"
|
||||
}
|
||||
|
||||
force_debug_mode() {
|
||||
# Force a switch into the debug mode to enable ADB
|
||||
"$SERIAL_PATH" AT
|
||||
echo -n "adb enabled, waiting for reboot"
|
||||
wait_for_adb_shell
|
||||
echo "it's alive!"
|
||||
wait_for_atfwd_daemon() {
|
||||
until [ -n "$(_adb_shell 'pgrep atfwd_daemon')" ]
|
||||
do
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_adb_shell() {
|
||||
until adb shell true 2> /dev/null
|
||||
until _adb_shell true 2> /dev/null
|
||||
do
|
||||
echo -n .
|
||||
sleep 1
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
setup_rootshell() {
|
||||
_adb_push rootshell /tmp/
|
||||
"$SERIAL_PATH" "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell"
|
||||
_at_syscmd "cp /tmp/rootshell /bin/rootshell"
|
||||
sleep 1
|
||||
"$SERIAL_PATH" "AT+SYSCMD=chown root /bin/rootshell"
|
||||
_at_syscmd "chown root /bin/rootshell"
|
||||
sleep 1
|
||||
"$SERIAL_PATH" "AT+SYSCMD=chmod 4755 /bin/rootshell"
|
||||
_at_syscmd "chmod 4755 /bin/rootshell"
|
||||
_adb_shell '/bin/rootshell -c id'
|
||||
echo "we have root!"
|
||||
adb shell /bin/rootshell -c id
|
||||
}
|
||||
|
||||
_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() {
|
||||
adb shell '/bin/rootshell -c "mkdir -p /data/rayhunter"'
|
||||
_adb_push config.toml.example /data/rayhunter/config.toml
|
||||
_adb_push rayhunter-daemon /data/rayhunter/
|
||||
_at_syscmd "mkdir -p /data/rayhunter"
|
||||
_adb_push config.toml.example /tmp/config.toml
|
||||
_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
|
||||
_at_syscmd "mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"
|
||||
_adb_push scripts/misc-daemon /tmp/misc-daemon
|
||||
adb shell '/bin/rootshell -c "cp /tmp/rayhunter_daemon /etc/init.d/rayhunter_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"'
|
||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/misc-daemon"'
|
||||
echo -n "rebooting, this may take a sec..."
|
||||
adb shell '/bin/rootshell -c reboot'
|
||||
_at_syscmd "mv /tmp/misc-daemon /etc/init.d/misc-daemon"
|
||||
|
||||
_at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon"
|
||||
_at_syscmd "chmod 755 /etc/init.d/misc-daemon"
|
||||
|
||||
echo -n "waiting for reboot..."
|
||||
_at_syscmd "shutdown -r -t 1 now"
|
||||
|
||||
# first wait for shutdown (it can take ~10s)
|
||||
until ! adb shell true 2> /dev/null
|
||||
until ! _adb_shell true 2> /dev/null
|
||||
do
|
||||
echo -n '.'
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# now wait for boot to finish
|
||||
wait_for_adb_shell
|
||||
|
||||
echo "rebooted successfully!"
|
||||
echo " done!"
|
||||
}
|
||||
|
||||
test_rayhunter() {
|
||||
URL="http://localhost:8080"
|
||||
adb forward tcp:8080 tcp:8080
|
||||
"$ADB" forward tcp:8080 tcp:8080 > /dev/null
|
||||
echo -n "checking for rayhunter server..."
|
||||
|
||||
SECONDS=0
|
||||
while (( SECONDS < 30 )); do
|
||||
if curl -L --fail-with-body "$URL" -o /dev/null -s; then
|
||||
echo
|
||||
echo "success! you can access rayhunter at $URL"
|
||||
echo "success!"
|
||||
echo "you can access rayhunter at $URL"
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
echo -n "."
|
||||
done
|
||||
echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :("
|
||||
}
|
||||
|
||||
17
dist/install-linux.sh
vendored
17
dist/install-linux.sh
vendored
@@ -2,5 +2,22 @@
|
||||
|
||||
set -e
|
||||
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
|
||||
install
|
||||
|
||||
22
dist/install-mac.sh
vendored
22
dist/install-mac.sh
vendored
@@ -1,6 +1,22 @@
|
||||
#!/bin/env bash
|
||||
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
export SERIAL_PATH="./serial-mac-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
|
||||
install
|
||||
|
||||
34
install.sh
34
install.sh
@@ -1,34 +0,0 @@
|
||||
#!/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cargo build --bin serial
|
||||
cargo build --bin rootshell --target armv7-unknown-linux-gnueabihf --release
|
||||
|
||||
# Force a switch into the debug mode to enable ADB
|
||||
cargo run --bin serial -- AT
|
||||
echo -n "adb enabled, waiting for reboot"
|
||||
until adb shell true 2> /dev/null
|
||||
do
|
||||
echo -n .
|
||||
sleep 1
|
||||
done
|
||||
echo
|
||||
echo "it's alive!"
|
||||
adb push target/armv7-unknown-linux-gnueabihf/release/rootshell /tmp/
|
||||
cargo run --bin serial -- "AT+SYSCMD=mv /tmp/rootshell /bin/rootshell"
|
||||
sleep 1
|
||||
cargo run --bin serial -- "AT+SYSCMD=chown root /bin/rootshell"
|
||||
sleep 1
|
||||
cargo run --bin serial -- "AT+SYSCMD=chmod 4755 /bin/rootshell"
|
||||
echo "we have root!"
|
||||
adb shell /bin/rootshell -c id
|
||||
adb shell '/bin/rootshell -c "mkdir /data/rayhunter"'
|
||||
adb push config.toml.example /data/rayhunter/config.toml
|
||||
adb push scripts/rayhunter_daemon /tmp/rayhunter_daemon
|
||||
adb push scripts/misc-daemon /tmp/misc-daemon
|
||||
adb shell '/bin/rootshell -c "mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"'
|
||||
adb shell '/bin/rootshell -c "mv /tmp/misc-daemon /etc/init.d/misc-daemon"'
|
||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/rayhunter_daemon"'
|
||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/misc-daemon"'
|
||||
./make.sh
|
||||
@@ -4,7 +4,13 @@ use serde::Serialize;
|
||||
|
||||
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.
|
||||
/// The levels should break down like this:
|
||||
@@ -18,7 +24,7 @@ pub enum Severity {
|
||||
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.
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -60,19 +66,19 @@ pub trait Analyzer {
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct AnalyzerMetadata {
|
||||
name: String,
|
||||
description: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct ReportMetadata {
|
||||
analyzers: Vec<AnalyzerMetadata>,
|
||||
pub analyzers: Vec<AnalyzerMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct PacketAnalysis {
|
||||
timestamp: DateTime<FixedOffset>,
|
||||
events: Vec<Option<Event>>,
|
||||
pub timestamp: DateTime<FixedOffset>,
|
||||
pub events: Vec<Option<Event>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -86,6 +92,19 @@ impl AnalysisRow {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
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 {
|
||||
@@ -99,9 +118,11 @@ impl Harness {
|
||||
|
||||
pub fn new_with_all_analyzers() -> Self {
|
||||
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(ImsiProvidedAnalyzer{}));
|
||||
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
||||
|
||||
harness
|
||||
}
|
||||
|
||||
@@ -175,7 +196,7 @@ impl Harness {
|
||||
|
||||
pub fn get_metadata(&self) -> ReportMetadata {
|
||||
let names = self.get_names();
|
||||
let descriptions = self.get_names();
|
||||
let descriptions = self.get_descriptions();
|
||||
let mut analyzers = Vec::new();
|
||||
for (name, description) in names.iter().zip(descriptions.iter()) {
|
||||
analyzers.push(AnalyzerMetadata {
|
||||
|
||||
42
lib/src/analysis/connection_redirect_downgrade.rs
Normal file
42
lib/src/analysis/connection_redirect_downgrade.rs
Normal file
@@ -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),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
59
lib/src/analysis/imsi_requested.rs
Normal file
59
lib/src/analysis/imsi_requested.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
|
||||
const PACKET_THRESHHOLD: usize = 150;
|
||||
|
||||
pub struct ImsiRequestedAnalyzer {
|
||||
packet_num: usize,
|
||||
}
|
||||
|
||||
impl ImsiRequestedAnalyzer {
|
||||
pub fn new() -> Self {
|
||||
Self { packet_num: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Analyzer for ImsiRequestedAnalyzer {
|
||||
fn get_name(&self) -> Cow<str> {
|
||||
Cow::from("IMSI Requested")
|
||||
}
|
||||
|
||||
fn get_description(&self) -> Cow<str> {
|
||||
Cow::from("Tests whether the ME sends an IMSI Identity Request NAS message")
|
||||
}
|
||||
|
||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||
self.packet_num += 1;
|
||||
let InformationElement::LTE(LteInformationElement::NAS(payload)) = ie else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// NAS identity request, 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 thiserror::Error;
|
||||
use crate::gsmtap::{GsmtapType, LteRrcSubtype, GsmtapMessage};
|
||||
use crate::gsmtap::{GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InformationElementError {
|
||||
@@ -40,6 +40,9 @@ pub enum LteInformationElement {
|
||||
SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message),
|
||||
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
|
||||
//DlCcchNb(),
|
||||
//DlDcchNb(),
|
||||
@@ -79,6 +82,9 @@ impl TryFrom<&GsmtapMessage> for InformationElement {
|
||||
};
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
pub mod analyzer;
|
||||
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_requested;
|
||||
pub mod null_cipher;
|
||||
pub mod util;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use telcom_parser::lte_rrc::{CipheringAlgorithm_r12, DL_CCCH_MessageType, DL_CCCH_MessageType_c1, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Identity, RRCConnectionReconfiguration, RRCConnectionReconfigurationCriticalExtensions, RRCConnectionReconfigurationCriticalExtensions_c1, RRCConnectionReconfiguration_r8_IEs, RRCConnectionRelease_v890_IEs, SCG_Configuration_r12, SecurityConfigHO_v1530HandoverType_v1530, SecurityModeCommand, SecurityModeCommandCriticalExtensions, SecurityModeCommandCriticalExtensions_c1};
|
||||
use telcom_parser::lte_rrc::{CipheringAlgorithm_r12, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReconfiguration, RRCConnectionReconfigurationCriticalExtensions, RRCConnectionReconfigurationCriticalExtensions_c1, SCG_Configuration_r12, SecurityConfigHO_v1530HandoverType_v1530, SecurityModeCommand, SecurityModeCommandCriticalExtensions, SecurityModeCommandCriticalExtensions_c1};
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
|
||||
32
lib/src/analysis/util.rs
Normal file
32
lib/src/analysis/util.rs
Normal file
@@ -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)
|
||||
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
||||
Nas4GMessage {
|
||||
#[deku(ctx = "log_type")]
|
||||
direction: Nas4GMessageDirection,
|
||||
ext_header_version: u8,
|
||||
rrc_rel: 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)]
|
||||
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
|
||||
pub enum LteRrcOtaPacket {
|
||||
|
||||
@@ -63,10 +63,14 @@ const MEMORY_DEVICE_MODE: i32 = 2;
|
||||
const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
||||
|
||||
#[cfg(target_arch = "arm")]
|
||||
const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||
|
||||
pub struct DiagDevice {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub enum GsmtapType {
|
||||
UmtsRlcMac,
|
||||
UmtsRrc(UmtsRrcSubtype),
|
||||
LteRrc(LteRrcSubtype), /* LTE interface */
|
||||
LteMac, /* LTE MAC interface */
|
||||
LteMac, /* LTE MAC interface */
|
||||
LteMacFramed, /* LTE MAC with context hdr */
|
||||
OsmocoreLog, /* libosmocore logging */
|
||||
QcDiag, /* Qualcomm DIAG frame */
|
||||
@@ -200,6 +200,11 @@ pub struct GsmtapHeader {
|
||||
#[deku(update = "self.gsmtap_type.get_type()")]
|
||||
pub packet_type: 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 signal_dbm: i8,
|
||||
pub signal_noise_ratio_db: u8,
|
||||
@@ -222,6 +227,8 @@ impl GsmtapHeader {
|
||||
header_len: 4,
|
||||
packet_type: gsmtap_type.get_type(),
|
||||
timeslot: 0,
|
||||
pcs_band_indicator: false,
|
||||
uplink: false,
|
||||
arfcn: 0,
|
||||
signal_dbm: 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)),
|
||||
};
|
||||
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.frame_number = packet.get_sfn();
|
||||
header.subslot = packet.get_subfn();
|
||||
@@ -108,9 +107,10 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
payload: packet.take_payload(),
|
||||
}))
|
||||
},
|
||||
LogBody::Nas4GMessage { msg, .. } => {
|
||||
LogBody::Nas4GMessage { msg, direction, .. } => {
|
||||
// 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 {
|
||||
header,
|
||||
payload: msg,
|
||||
|
||||
@@ -7,3 +7,6 @@ pub mod gsmtap;
|
||||
pub mod gsmtap_parser;
|
||||
pub mod pcap;
|
||||
pub mod analysis;
|
||||
|
||||
// re-export telcom_parser, since we use its types in our API
|
||||
pub use telcom_parser;
|
||||
|
||||
@@ -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 self.bytes_read >= max_bytes {
|
||||
if self.bytes_read > max_bytes {
|
||||
|
||||
5
make.sh
5
make.sh
@@ -1,4 +1,5 @@
|
||||
#!/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 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::env;
|
||||
|
||||
#[cfg(target_arch = "arm")]
|
||||
use nix::unistd::Gid;
|
||||
|
||||
fn main() {
|
||||
@@ -14,11 +15,13 @@ fn main() {
|
||||
// Android's "paranoid network" feature restricts network access to
|
||||
// processes in specific groups. More info here:
|
||||
// https://www.elinux.org/Android_Security#Paranoid_network-ing
|
||||
let gids = &[
|
||||
Gid::from_raw(3003), // AID_INET
|
||||
Gid::from_raw(3004), // AID_NET_RAW
|
||||
];
|
||||
nix::unistd::setgroups(gids).expect("setgroups failed");
|
||||
#[cfg(target_arch = "arm")] {
|
||||
let gids = &[
|
||||
Gid::from_raw(3003), // AID_INET
|
||||
Gid::from_raw(3004), // AID_NET_RAW
|
||||
];
|
||||
nix::unistd::setgroups(gids).expect("setgroups failed");
|
||||
}
|
||||
|
||||
// discard argv[0]
|
||||
let _ = args.next();
|
||||
|
||||
@@ -6,5 +6,4 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rusb = "0.9.3"
|
||||
|
||||
rusb = { version = "0.9.3", features = ["vendored"] }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Serial communication with the orbic device
|
||||
//!
|
||||
//! This binary has two main functions, putting the orbic device in update mode which enables ADB
|
||||
//! Serial communication with the orbic device
|
||||
//!
|
||||
//! This binary has two main functions, putting the orbic device in update mode which enables ADB
|
||||
//! and running AT commands on the serial modem interface which can be used to upload a shell and chown it to root
|
||||
//!
|
||||
//!
|
||||
//! # Panics
|
||||
//!
|
||||
//! No device found - make sure your device is plugged in and turned on. If it is, it's possible you have a device with a different
|
||||
//! usb id, file a bug with the output of `lsusb` attached.
|
||||
//!
|
||||
//!
|
||||
//! No device found - make sure your device is plugged in and turned on. If it is, it's possible you have a device with a different
|
||||
//! usb id, file a bug with the output of `lsusb` attached.
|
||||
//!
|
||||
//! # Examples
|
||||
//! ```
|
||||
//! match rusb::Context::new() {
|
||||
@@ -20,38 +20,36 @@
|
||||
//! Err(e) => panic!("Failed to initialize libusb: {0}", e),
|
||||
//! ````
|
||||
use std::str;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use rusb::{
|
||||
Context, DeviceHandle, UsbContext,
|
||||
};
|
||||
use rusb::{Context, DeviceHandle, UsbContext};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
println!("usage: {0} <command>", args[0]);
|
||||
if args.len() != 2 {
|
||||
println!("usage: {0} [<command> | --root]", args[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
match Context::new() {
|
||||
Ok(mut context) => match open_orbic(&mut context) {
|
||||
Some(mut handle) => {
|
||||
send_command(&mut handle, &args[1])
|
||||
},
|
||||
None => panic!("No Orbic device found"),
|
||||
},
|
||||
Err(e) => panic!("Failed to initialize libusb: {0}", e),
|
||||
Ok(mut context) => {
|
||||
if args[1] == "--root" {
|
||||
enable_command_mode(&mut context);
|
||||
} else {
|
||||
match open_orbic(&mut context) {
|
||||
Some(mut handle) => send_command(&mut handle, &args[1]),
|
||||
None => panic!("No Orbic device found"),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => panic!("Failed to initialize libusb: {0}", e),
|
||||
}
|
||||
}
|
||||
/// Sends an AT command to the usb device over the serial port
|
||||
///
|
||||
///
|
||||
/// First establish a USB handle and context by calling `open_orbic(<T>)
|
||||
fn send_command<T: UsbContext>(
|
||||
handle: &mut DeviceHandle<T>,
|
||||
command: &str,
|
||||
) {
|
||||
fn send_command<T: UsbContext>(handle: &mut DeviceHandle<T>, command: &str) {
|
||||
let mut data = String::new();
|
||||
data.push_str("\r\n");
|
||||
data.push_str(command);
|
||||
@@ -61,95 +59,98 @@ fn send_command<T: UsbContext>(
|
||||
let mut response = [0; 256];
|
||||
|
||||
// Set up the serial port appropriately
|
||||
handle.write_control(0x21, 0x22, 3, 1, &[], timeout).expect("Failed to send control request");
|
||||
handle
|
||||
.write_control(0x21, 0x22, 3, 1, &[], timeout)
|
||||
.expect("Failed to send control request");
|
||||
|
||||
// Send the command
|
||||
handle.write_bulk(0x2, data.as_bytes(), timeout).expect("Failed to write command");
|
||||
handle
|
||||
.write_bulk(0x2, data.as_bytes(), timeout)
|
||||
.expect("Failed to write command");
|
||||
|
||||
// Consume the echoed command
|
||||
handle.read_bulk(0x82, &mut response, timeout).expect("Failed to read submitted command");
|
||||
handle
|
||||
.read_bulk(0x82, &mut response, timeout)
|
||||
.expect("Failed to read submitted command");
|
||||
|
||||
// Read the actual response
|
||||
handle.read_bulk(0x82, &mut response, timeout).expect("Failed to read response");
|
||||
handle
|
||||
.read_bulk(0x82, &mut response, timeout)
|
||||
.expect("Failed to read response");
|
||||
|
||||
let responsestr = str::from_utf8(&response).expect("Failed to parse response");
|
||||
if !responsestr.starts_with("\r\nOK\r\n") {
|
||||
println!("Received unexpected response{0}", responsestr)
|
||||
// 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") {
|
||||
println!("Received unexpected response{0}", responsestr);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a command to switch the device into generic mode, exposing serial
|
||||
///
|
||||
///
|
||||
/// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition.
|
||||
fn switch_device<T: UsbContext>(
|
||||
handle: &mut DeviceHandle<T>,
|
||||
) {
|
||||
let timeout = Duration::from_secs(1);
|
||||
|
||||
if let Err(e) = handle.write_control(0x40, 0xa0, 0, 0, &[], timeout) {
|
||||
// If the device reboots while the command is still executing we
|
||||
// may get a pipe error here
|
||||
if e == rusb::Error::Pipe {
|
||||
return
|
||||
}
|
||||
panic!("Failed to send device switch control request: {0}", e)
|
||||
fn enable_command_mode<T: UsbContext>(context: &mut T) {
|
||||
if open_orbic(context).is_some() {
|
||||
println!("Device already in command mode. Doing nothing...");
|
||||
return;
|
||||
}
|
||||
|
||||
let timeout = Duration::from_secs(1);
|
||||
if let Some(handle) = open_device(context, 0x05c6, 0xf626) {
|
||||
if let Err(e) = handle.write_control(0x40, 0xa0, 0, 0, &[], timeout) {
|
||||
// If the device reboots while the command is still executing we
|
||||
// may get a pipe error here
|
||||
if e == rusb::Error::Pipe {
|
||||
return;
|
||||
}
|
||||
panic!("Failed to send device switch control request: {0}", e)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
panic!("No Orbic device found");
|
||||
}
|
||||
|
||||
/// Get a handle and contet for the orbic device
|
||||
///
|
||||
/// If the device isn't already in command mode this function will call swtich_device to switch it into command mode
|
||||
fn open_orbic<T: UsbContext>(
|
||||
context: &mut T,
|
||||
) -> Option<DeviceHandle<T>> {
|
||||
fn open_orbic<T: UsbContext>(context: &mut T) -> Option<DeviceHandle<T>> {
|
||||
// Device after initial mode switch
|
||||
if let Some(handle) = open_device(context, 0x05c6, 0xf601) {
|
||||
return Some(handle)
|
||||
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);
|
||||
}
|
||||
|
||||
// Device with rndis enabled as well
|
||||
if let Some(handle) = open_device(context, 0x05c6, 0xf622) {
|
||||
return Some(handle)
|
||||
}
|
||||
|
||||
// Device in out-of-the-box state, need to switch to diag mode
|
||||
match open_device(context, 0x05c6, 0xf626) {
|
||||
Some(mut handle) => switch_device(&mut handle),
|
||||
None => panic!("No Orbic device detected")
|
||||
}
|
||||
|
||||
for _ in 1..10 {
|
||||
if let Some(handle) = open_device(context, 0x05c6, 0xf601) {
|
||||
return Some(handle)
|
||||
}
|
||||
sleep(Duration::from_secs(10))
|
||||
}
|
||||
panic!("No Orbic device detected")
|
||||
}
|
||||
|
||||
/// Generic function to open a USB device
|
||||
fn open_device<T: UsbContext>(
|
||||
context: &mut T,
|
||||
vid: u16,
|
||||
pid: u16,
|
||||
) -> Option<DeviceHandle<T>> {
|
||||
let devices = match context.devices() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
for device in devices.iter() {
|
||||
let device_desc = match device.device_descriptor() {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if device_desc.vendor_id() == vid && device_desc.product_id() == pid {
|
||||
match device.open() {
|
||||
Ok(handle) => return Some(handle),
|
||||
Err(e) => panic!("device found but failed to open: {}", e),
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Generic function to open a USB device
|
||||
fn open_device<T: UsbContext>(context: &mut T, vid: u16, pid: u16) -> Option<DeviceHandle<T>> {
|
||||
let devices = match context.devices() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
for device in devices.iter() {
|
||||
let device_desc = match device.device_descriptor() {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if device_desc.vendor_id() == vid && device_desc.product_id() == pid {
|
||||
match device.open() {
|
||||
Ok(handle) => return Some(handle),
|
||||
Err(e) => panic!("device found but failed to open: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
|
||||
18
tools/install-dev.sh
Executable file
18
tools/install-dev.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
mkdir build
|
||||
cd build
|
||||
curl -LOs "https://github.com/EFForg/rayhunter/releases/latest/download/release.tar"
|
||||
curl -LOs "https://github.com/EFForg/rayhunter/releases/latest/download/release.tar.sha256"
|
||||
if ! sha256sum -c --quiet release.tar.sha256; then
|
||||
echo "Download corrupted! (╯°□°)╯︵ ┻━┻"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tar -xf release.tar
|
||||
./install-linux.sh
|
||||
|
||||
cd ..
|
||||
rm -rf build
|
||||
60
tools/nasparse.py
Executable file
60
tools/nasparse.py
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/python3
|
||||
import pycrate_mobile
|
||||
from pycrate_mobile import NASLTE
|
||||
import pycrate_core
|
||||
import binascii
|
||||
import sys
|
||||
import pprint
|
||||
from enum import Enum
|
||||
|
||||
import pycrate_mobile.TS24301_EMM
|
||||
|
||||
EPS_IMSI_ATTACH = 2
|
||||
|
||||
def parse_nas_message(buffer, uplink=None):
|
||||
if isinstance(buffer, str): #handle string argument or raw bytes
|
||||
bin = binascii.unhexlify(buffer)
|
||||
else:
|
||||
bin = buffer
|
||||
if uplink:
|
||||
parsed = NASLTE.parse_NASLTE_MO(bin)
|
||||
elif uplink == None: #We don't know if its an up or downlink
|
||||
parsed = NASLTE.parse_NASLTE_MO(bin)
|
||||
if parsed[0] == None:
|
||||
parsed = NASLTE.parse_NASLTE_MT(bin)
|
||||
else:
|
||||
parsed = NASLTE.parse_NASLTE_MT(bin)
|
||||
|
||||
if parsed[0] is None: # Not a NAS Packet
|
||||
raise TypeError("Not a nas packet")
|
||||
return parsed[0]
|
||||
|
||||
def heur_ue_imsi_sent(msg):
|
||||
output = "device transmitted IMSI to base station!"
|
||||
|
||||
if type(msg) not in [pycrate_mobile.TS24301_EMM.EMMAttachRequest, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage]:
|
||||
return (False, None)
|
||||
|
||||
if isinstance(msg, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage):
|
||||
try:
|
||||
msg = msg['EMMAttachRequest']
|
||||
except pycrate_core.elt.EltErr:
|
||||
return (False, None)
|
||||
|
||||
if msg['EPSAttachType']['V'].to_int() == EPS_IMSI_ATTACH: #EPSAttachType Value is 'Combined EPS/IMSI Attach (2)'
|
||||
return (True, output)
|
||||
return (False, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: nasparse.py [hex encoded nas message]")
|
||||
exit(1)
|
||||
|
||||
buffer = sys.argv[1]
|
||||
msg = parse_nas_message(buffer)
|
||||
pprint.pprint(msg)
|
||||
triggered, message = heur_ue_imsi_sent(msg)
|
||||
if triggered:
|
||||
print(message)
|
||||
exit(1)
|
||||
38
tools/nasparse_test.py
Normal file
38
tools/nasparse_test.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/python3
|
||||
import unittest
|
||||
import nasparse
|
||||
|
||||
|
||||
class TestNasparse(unittest.TestCase):
|
||||
imsi_sent_msg = '07412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1000000000000'
|
||||
sec_imsi_sent_msg = '1727db4b7c0207412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1'
|
||||
non_nas_msg = 'deadbeefcafe'
|
||||
other_nas_msg = '074413780004023fd121'
|
||||
other_nas_mt_msg = "023fd12100000000000000000000000000000000000000000000000000000000"
|
||||
ciphered_nas_msg = "27ed6146bd0162a5d62d62e1ce501720dc8bd84f1167fd"
|
||||
|
||||
def run_heur(self, msg):
|
||||
buf = nasparse.parse_nas_message(msg)
|
||||
return nasparse.heur_ue_imsi_sent(buf)[0]
|
||||
|
||||
def test_imsi_sent(self):
|
||||
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "imsi_sent_msg should trigger heuristic")
|
||||
|
||||
def test_sec_imsi_sent(self):
|
||||
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "sec_imsi_sent_msg should trigger heuristic")
|
||||
|
||||
def test_non_nas_msg(self):
|
||||
with self.assertRaises(TypeError):
|
||||
self.run_heur(self.non_nas_msg)
|
||||
|
||||
def test_other_nas(self):
|
||||
self.assertEqual(self.run_heur(self.other_nas_msg), False, "other_nas_msg should not trigger heuristic")
|
||||
|
||||
def test_other_nas_mt(self):
|
||||
self.assertEqual(self.run_heur(self.other_nas_mt_msg), False, "other_nas_mt_msg should not trigger heuristic")
|
||||
|
||||
def test_ciphered_nas(self):
|
||||
self.assertEqual(self.run_heur(self.ciphered_nas_msg), False, "ciphered_nas_msg should not trigger heuristic")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
38
tools/pcap_check.py
Executable file
38
tools/pcap_check.py
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/python3
|
||||
import nasparse
|
||||
from scapy.utils import RawPcapNgReader
|
||||
import sys
|
||||
|
||||
TYPE_LTE_NAS = 0x12
|
||||
UDP_LEN = 28
|
||||
|
||||
def process_pcap(pcap_path):
|
||||
print('Opening {}...'.format(pcap_path))
|
||||
|
||||
count = 0
|
||||
for pkt_data, pkt_metadata in RawPcapNgReader(pcap_path):
|
||||
count += 1
|
||||
gsmtap_len = pkt_data[UDP_LEN+1] * 4 # gsmtap header length is stored in the 2nd byte of GSMTAP as a number of 32 bit words
|
||||
header_end = gsmtap_len + UDP_LEN #length of UDP/IP header plus GSMTAP header
|
||||
|
||||
gsmtap_hdr = pkt_data[UDP_LEN:header_end]
|
||||
|
||||
if gsmtap_hdr[2] != TYPE_LTE_NAS:
|
||||
continue
|
||||
|
||||
# uplink status is the 7th bit of the 5th byte of the GSMTAP header.
|
||||
# Uplink (Mobile originated) = 0 Downlink (mobile terminated) = 1
|
||||
uplink = (gsmtap_hdr[4] & 0b01000000) >> 6
|
||||
buffer = pkt_data[header_end:]
|
||||
msg = nasparse.parse_nas_message(buffer, uplink)
|
||||
triggered, message = nasparse.heur_ue_imsi_sent(msg)
|
||||
if triggered:
|
||||
print(f"Frame {count} triggered heuristic: {message}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: pcap_check.py [path/to/pcap/file]")
|
||||
exit(1)
|
||||
|
||||
pcap_path = sys.argv[1]
|
||||
process_pcap(pcap_path)
|
||||
Reference in New Issue
Block a user