150 Commits

Author SHA1 Message Date
Cooper Quintin
941ea59e11 I forgot rootshell and telecom parser 2025-04-22 11:04:42 -07:00
Cooper Quintin
8082e013f4 bump version 2025-04-22 11:04:42 -07:00
Cooper Quintin
a7ce1ad4d3 update mm invite 2025-04-18 12:41:25 -07:00
Markus Unterwaditzer
531e9aa6fb this documentation is also not useful to end users 2025-04-17 09:46:23 -07:00
Markus Unterwaditzer
833d0e41b4 more cleanup 2025-04-17 09:46:23 -07:00
Markus Unterwaditzer
056cdac546 remove Silicon from Mac, we do support intel 2025-04-17 09:46:23 -07:00
Markus Unterwaditzer
6ea2b0a4e6 Remove outdated instructions from README
`cargo test_pc` is not a thing, remove these instructions although there
currently is no replacement.
2025-04-17 09:46:23 -07:00
Markus Unterwaditzer
1cc5eb4c4c README: Do not mention SD card
Apparently SD card is not a thing on Orbic, only on TP-Link.
2025-04-14 16:42:09 -07:00
oopsbagel
99676f1590 chore: add blame ignore rev file
Do not display formatting commit in git blame. Use this file locally by running:

git config blame.ignoreRevsFile .git-blame-ignore-revs

This file is read by github automatically.[0]

[0] https://docs.github.com/en/repositories/working-with-files/using-files/viewing-and-understanding-files#ignore-commits-in-the-blame-view
2025-04-14 11:49:24 -07:00
oopsbagel
9fe75ac961 chore: cargo fmt 2025-04-14 11:49:24 -07:00
Markus Unterwaditzer
151e186ef9 Fix delete all recordings, and panic on server startup
* Delete All Recordings did not work when recording was paused
* Because of the upgrade to axum 0.8, the webserver did not actually
  start but panic.
2025-04-11 12:42:23 -07:00
Cooper Quintin
06c4dd468e Merge branch 'untitaker-build-features' 2025-04-11 11:30:23 -07:00
Markus Unterwaditzer
740f979293 Merge remote-tracking branch 'origin/main' into build-features 2025-04-11 20:15:18 +02:00
oopsbagel
700258b0f2 ci: test build release on PRs 2025-04-11 11:13:07 -07:00
oopsbagel
f661e2e318 ci(windows): compile serial for x86_64-pc-windows-gnu 2025-04-11 11:13:07 -07:00
Markus Unterwaditzer
b12a159f0a Merge remote-tracking branch 'origin/main' into build-features 2025-04-11 19:57:15 +02:00
oopsbagel
4e40994577 ci: add windows target for serial 2025-04-11 10:42:29 -07:00
Cooper Quintin
1b29cf0dee Merge branch 'main' into build-features 2025-04-11 10:38:49 -07:00
Markus Unterwaditzer
aafd83d636 Upgrade axum to reduce binary size
For some reason upgrading axum to 0.8 reduces the binary size by 300kB
2025-04-11 10:32:02 -07:00
oopsbagel
dd67fbf645 ci: statically compile serial binary
fix unreleased rayhunter-check binary names
2025-04-11 10:30:38 -07:00
Markus Unterwaditzer
e440dab736 Add dockerfile for easier building on MacOS 2025-04-11 10:09:35 -07:00
oopsbagel
30e543898b ci: add windows-latest (x86_64) release 2025-04-11 10:07:02 -07:00
oopsbagel
01e762a3d6 fix(lib): enable building for windows targets
- conditionally build diag_device.rs only for unix
- use build time target for runtime metadata on unix
2025-04-11 10:07:02 -07:00
oopsbagel
fa9e9319c2 fix(serial.enable_command_mode): claim usb device interface
Windows does not support nusb::Device.control_out_blocking

Claim the interface before writing as required on Windows.
2025-04-11 10:07:02 -07:00
oopsbagel
b317200307 ci: add windows serial cargo check and test 2025-04-11 10:07:02 -07:00
Markus Unterwaditzer
55f78cf749 Document what the red line means
Fix https://github.com/EFForg/rayhunter/issues/134
2025-04-10 16:51:28 -07:00
Markus Unterwaditzer
cb9e8254a8 cargo fmt 2025-04-09 15:37:20 +02:00
Markus Unterwaditzer
a9afa347f0 turn pixelart macro into const expr 2025-04-09 15:37:03 +02:00
zoracon
75944a7d16 Fix template bugs 2025-04-08 15:53:27 -07:00
Markus Unterwaditzer
e11bb2518e fix tests 2025-04-08 21:33:41 +02:00
Markus Unterwaditzer
31076ec8b2 replace with exclamation mark 2025-04-08 21:24:33 +02:00
Markus Unterwaditzer
5e22b5c6a8 Update bin/src/display/tplink_onebit.rs
Co-authored-by: Will Greenberg <ifnspifn@gmail.com>
2025-04-08 21:21:36 +02:00
Markus Unterwaditzer
3dc373f0d3 add code comment 2025-04-08 21:21:20 +02:00
Markus Unterwaditzer
bccdcf36e1 Merge remote-tracking branch 'origin/main' into build-features 2025-04-08 21:16:08 +02:00
Will Greenberg
fb9c4ab85b Update pull_request_template.md 2025-04-08 09:57:23 -07:00
Will Greenberg
e864ce0a51 Add PR template 2025-04-08 09:57:23 -07:00
zoracon
7f990ae4bd Move issue templates to correct location 2025-04-08 09:56:13 -07:00
Sashanoraa
3ac4acd83c Indent rootshell's code to 4 space like everything else
It was three for some reason.
2025-04-08 08:59:40 -07:00
Markus Unterwaditzer
5c5333f0c7 Remove RecordingCBM
Colorblind mode is a property of the respective display, and decision
whether to display something in colorblind mode should lie with the
display thread. The display thread already needs to know about
colorblind mode for the initial state.

In #226, there are multiple implementations of display thread, and at
least one of them is dealing with a one-bit display anyway.

Aside, I think rayhunter should send an initial DisplayState on startup,
UI threads should not assume that the device is already recording. But
this can be discussed separately.
2025-04-08 08:58:08 -07:00
Sashanoraa
60934e593b Add the content length header to the qmdl file response 2025-04-08 08:54:39 -07:00
oopsbagel
4099eb30a5 ci: build on ubuntu-24.04-arm (aarch64) 2025-04-08 08:53:56 -07:00
Evan Rusmisel
f81adad897 rusty 2025-04-08 08:47:54 -07:00
dependabot[bot]
775468f037 Bump tokio from 1.44.1 to 1.44.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.1 to 1.44.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.1...tokio-1.44.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.44.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 08:44:46 -07:00
Will Greenberg
91e825adff mac installer: if we've already removed quarantine bit, continue 2025-04-07 16:16:54 -07:00
Markus Unterwaditzer
499b86aca6 Add build features for multiple device types
The bin crate now has two features, one for each supported device.

* The IOCTL change from #142 is compiled in conditionally.
* Tp-link display is supported & tested for HW rev 3 and HW rev 5.

The release tarballs now contain two rayhunter-daemon binaries, for
orbic and tplink. An installer for tplink is not yet included.

Co-authored-by: m0veax <m0veax@chaospott.de>
2025-04-07 21:15:32 +02:00
Sashanoraa
7b897c335d Remove unneeded image dependencies
This removes a bunch of dependencies we aren't using and reduces the
binary size by 0.7 MB.
2025-04-04 12:30:57 -07:00
Sashanoraa
c47be1074b Add delete all recordings button to web ui 2025-04-04 12:21:51 -07:00
Sashanoraa
326d4106bd Add a delete option to each recording in the web view 2025-04-04 12:21:51 -07:00
Markus Unterwaditzer
df8a1f5606 Fix RecordingStore::create
Bug introduced in #225
2025-04-03 15:22:08 -07:00
Markus Unterwaditzer
b0f5296c20 disable quirks mode 2025-04-03 12:15:33 -07:00
Markus Unterwaditzer
4e792b1402 Fix rendering of last_message_time in UI
* last_message_time is shown inconsistently for current entry vs other
  entries -- deduplicate code
* last_message_time is N/A for undefined -- but the API response was
  null instead of undefined.
2025-04-03 12:15:33 -07:00
zoracon
9144259202 Add issue templates 2025-04-03 12:14:15 -07:00
Markus Unterwaditzer
58f0071864 Fix malformed QMDL store writes
Fix https://github.com/EFForg/rayhunter/issues/199
Fix https://github.com/EFForg/rayhunter/issues/151

rayhunter updates manifest files using write **without truncation**.
This means that if the new manifest is shorter than the old one,
trailing bytes of the old data will persist in the new file.

Switch over to atomic file writes so that this bug is fixed + rayhunter
behaves correctly if it is killed mid-write.

https://github.com/EFForg/rayhunter/pull/182 could be reverted as it
seems to mostly be a workaround.
2025-04-03 12:09:19 -07:00
oopsbagel
3c0716c877 feat(serial): replace all panics with error handling from anyhow
Support -h and --help arguments.
Print a better error message when the Orbic device is not found.
Replace every panic! with anyhow::bail!
Replace .expect() with .context()?
Wraps all function returns with anyhow::Result
2025-04-01 13:40:01 -07:00
Jeremy Blanchard
bf8f1fb8eb Add troubleshooting step for macOS 2025-04-01 12:23:39 -07:00
Jeremy Blanchard
2a808245fb Fix anchor link for setup section 2025-04-01 12:23:39 -07:00
Jeremy Blanchard
208ccbafaa Fix another rebase merge issue 2025-04-01 12:23:39 -07:00
Jeremy Blanchard
b150f9dc4f Fix header issue from the rebase 2025-04-01 12:23:39 -07:00
Jeremy Blanchard
b6ef48e0f6 Corrent path of unzipped folder 2025-04-01 12:23:39 -07:00
Jeremy Blanchard
fddb18546c Unify install scripts in docs 2025-04-01 12:23:39 -07:00
Jeremy Blanchard
2911838b1c Cleanup language and formatting 2025-04-01 12:23:39 -07:00
Jeremy Blanchard
adbe3991dd Improve installation doc clarity. Cleanup capitalization. 2025-04-01 12:23:39 -07:00
Sashanoraa
fbc47187c5 Create ServerState outside of run_server
This reduces the number of arguments of run_server to make clippy happy
and also makes the code easier to understand.
2025-03-27 11:57:01 -07:00
Sashanoraa
5f601a209e Collapse nested if statements 2025-03-27 11:57:01 -07:00
Sashanoraa
04652d2097 Add implement Default on types with ::new
This fixes a clippy lint warning
2025-03-27 11:57:01 -07:00
Sashanoraa
034e0632e4 Box some of the larger information element enum variants
An enum is always the size needed to store its largest variant. Some of
the variants of the InformationElement and LteInformationElement are
substantially larger than the rest. Boxing the larger variants reduces
the size of the enum, in some cases by several kilobytes.

Since Rust does not currently support destructing a Box via pattern
matching, some code that destructures these enums had to be modified.
2025-03-27 11:57:01 -07:00
Sashanoraa
4edf001ca4 Fix small clippy warnings 2025-03-27 11:57:01 -07:00
Sashanoraa
b41f61bfa6 Replace unnecessary File::options with File::create 2025-03-27 11:57:01 -07:00
Will Greenberg
46a5bf8a84 Add signal link 2025-03-27 11:18:39 -07:00
Will Greenberg
2ee45382fc Update README.md 2025-03-27 11:18:39 -07:00
cycloarcane
f507cc0269 Added an FAQ entry on how to use the rootshell to delete data from the device 2025-03-27 09:52:19 -07:00
Tim Kerby
0780b527b9 Update rayhunter_daemon for TPLINK Compatibility
TPLink devices dont have bash - only sh
2025-03-26 10:41:15 -07:00
Sashanoraa
b0a1b14160 Remove unused import due to e79dc4a
The referenced commit disabled the null-cipher but did not remove the
now unused import.
2025-03-26 10:41:05 -07:00
Sashanoraa
b7243dae62 Add missing Cargo.lock updates from 0.2.6 version bump 2025-03-26 10:40:26 -07:00
Sashanoraa
0c4a0123aa Add missing Cargo.lock changes from 9af8e00 2025-03-26 10:40:26 -07:00
Will Greenberg
9bc8a7892b fix typo in installer script 2025-03-26 10:22:17 -07:00
oopsbagel
431a97ca65 chore: bump all Cargo.toml versions to 0.2.6 2025-03-25 17:02:01 -07:00
Will Greenberg
0364bfbc98 bump version number
we uhh forgot to do this for every release lol
2025-03-25 16:53:20 -07:00
Ben Brown
996e47684c Fix typo on readme
sensetive -> sensitive
2025-03-25 16:52:16 -07:00
Cooper Quintin
266f2b2e53 more nesting 2025-03-25 16:49:08 -07:00
Will Greenberg
2080cd7845 web ui: fix issue causing no entries
We weren't correctly handling all possible events from the heuristics
list
2025-03-25 16:49:08 -07:00
oopsbagel
9af8e006b0 fix(serial): use tokio's timeout with USB bulk in/out
Replace futures_lite::future::block_on (which will block indefinitely) with
tokio::time::timeout to restore the original behaviour of this utility, where
communication over USB interface bulk endpoints times out after 1 second.
2025-03-25 16:46:35 -07:00
oopsbagel
e841e22774 refactor(serial): replace rusb with nusb
nusb is a pure Rust library providing the same low level access to USB devices
that rusb/libusb provide.

This commit removes rusb (and thus the dependence on libusb) and replaces it
with nusb in the serial utility.

The only functional change is that nusb does not support timeouts for bulk data
commands. nusb is async. This commit contains a naïve implementation that simply
blocks on bulk reads and writes in send_command().
2025-03-25 16:46:35 -07:00
Will Greenberg
0d9f53f602 Update make.sh
reboot the orbic instead of starting up the process again, since rootshell seems to have insufficient privileges to start rayhunter
2025-03-25 16:34:23 -07:00
Will Greenberg
c9dcbbe5d6 daemon: if we fail to parse the QMDL manifest, make a new one
If rayhunter doesn't exit cleanly (e.g. during a battery outage), the
QMDL manifest may end up in a corrupted state. If that's the case,
rayhunter should try to recover by creating a new manifest. This'll let
it continue, and will preserve previous recordings, but they won't be
visible through the UI.
2025-03-25 15:36:12 -07:00
Will Greenberg
61d6ff6510 Add an update section 2025-03-25 15:14:54 -07:00
Will Greenberg
e79dc4a8f0 lib: diable null-cipher heuristic due to false positives
Due to an upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133),
our RRC parser is reporting false-positives for the null cipher
heuristic.
2025-03-25 15:13:36 -07:00
Will Greenberg
6204bc0195 update installer script for macOS Intel 2025-03-24 16:42:58 -07:00
Will Greenberg
65b9843e39 test macOS intel builds 2025-03-24 16:42:58 -07:00
Sashanoraa
d0d01089dd Fix various clippy warnings
This commit fixes various clippy warnings that do not affect the
function of the code and aren't stylistic in nature.
2025-03-24 13:47:20 -07:00
Sashanoraa
9c26e89b24 Modify config load to use serde default
This commit refactors the config loading code to no longer require a
separate ConfigFile struct by taking advantage of serde's `default`
attribute. This causes serde to use the Config struct's default value
for that attribute for any missing attributes, which is what the
existing code was doing anyway.

This also fixes several clippy warnings.

Serde docs: https://serde.rs/container-attrs.html#default
2025-03-24 13:47:20 -07:00
Sashanoraa
1f4786db19 Have rootshell print errors and exit 1 if exec fails
Previously was ignoring the possible error retuned by exec, this commit
has rootshell print the error if exec returns and have the process exit
with a code of 1 instead of 0.
2025-03-24 13:47:20 -07:00
Kirk Strauser
88f81d86fa Remove the quarantine bit from the serial command on macOS 2025-03-20 10:49:07 -07:00
oopsbagel
0b3c0de481 fix(lib/util): use better names for runtime metadata
- document RuntimeMetadata fields
- rename RayhunterMetadata to RuntimeMetadata
- rename RuntimeMetadata.os to RuntimeMetadata.system_os
- remove unpopulated hardware field
- remove unnecessary duplication of datastructure in analyzer harness
2025-03-19 11:48:54 -07:00
oopsbagel
188e9f436b fix(qmdl-manifest): store os/arch/hardware in qmdl manifest.toml
Do not superfluously prefix these names with rayhunter_, as they describe the
hardware and not the binary.
2025-03-19 11:48:54 -07:00
oopsbagel
f2b5aa2743 feat: show rayhunter version/os/arch in pcap, ndjson, qmdl manifest
Create a util mod to provide information about the rayhunter binary and
system.
2025-03-19 11:48:54 -07:00
oopsbagel
b785a7f21c feat(qmdl): add rayhunter version and os to manifest.toml 2025-03-19 11:48:54 -07:00
oopsbagel
09d35ccec7 feat(pcap): add operating system kernel name and release
Display the uname sysname and release as the OS option in the pcap Section
Header Block, falling back on just the std::env::consts::OS name ("linux") in
the case of runtime errors.

Co-authored-by: Nat Budin <natbudin@gmail.com>
2025-03-19 11:48:54 -07:00
oopsbagel
5ae186bc73 feat(pcap): add rayhunter name and version to metadata
Add the compile-time name and version to the pcap's Section Header Block
as the shb_userappl option, the canonical place for storing the name of
the application used to create the pcap.[0]

[0] https://ietf-opsawg-wg.github.io/draft-ietf-opsawg-pcap/draft-ietf-opsawg-pcapng.html#section-4.1-10
2025-03-19 11:48:54 -07:00
Inhishonor
c765a40426 Improve grammer. 2025-03-19 09:27:01 -07:00
Inhishonor
93cfbea361 Fix various sentences in README. 2025-03-19 09:27:01 -07:00
Cooper Quintin
8e6bed97b7 Merge branch 'allpoints-132_Merge_OS_variant_install_scripts' 2025-03-18 18:22:33 -07:00
Cooper Quintin
4214b27c0f fix nits in install.sh and update readme with new instructions 2025-03-18 18:21:43 -07:00
rbomze
f69487853a minimized the binary size 2025-03-18 17:59:07 -07:00
Jeremy
7eb61748d7 Update readme: Add link to PGP key for contact email address 2025-03-18 17:59:07 -07:00
Will Greenberg
ca4e560e92 Update README.md 2025-03-18 17:59:07 -07:00
Alexis
2ffb1d4620 Update SECURITY.md
Just fixing the relative link to this project
2025-03-18 17:59:07 -07:00
Cooper Quintin
77944dd17c add security file 2025-03-18 17:59:07 -07:00
rbomze
50301076f0 minimized the binary size 2025-03-18 17:37:24 -07:00
Jeremy
21c839678b Update readme: Add link to PGP key for contact email address 2025-03-17 11:24:19 -07:00
Will Greenberg
332a7ffbd0 Update README.md 2025-03-12 11:56:12 -07:00
Alexis
8d250553b7 Update SECURITY.md
Just fixing the relative link to this project
2025-03-11 15:35:47 -07:00
Cooper Quintin
fa897e73fa add security file 2025-03-11 14:53:28 -07:00
Paul Beltrani
c3494e338f Merge install scripts into a single, isntall.sh 2025-03-09 22:27:48 -04:00
Cooper Quintin
f9b2cd6a59 add link to code of conduct 2025-03-07 11:40:37 -08:00
Will Greenberg
eb072fb38c fix various typos 2025-03-07 11:28:29 -08:00
Will Greenberg
91f82fc71d add curl to apt install list 2025-03-07 11:21:36 -08:00
Will Greenberg
6fda8450dc a few more FAQ adjustments 2025-03-07 11:21:36 -08:00
Cooper Quintin
bbfe5877fe More FAQ work 2025-03-07 11:21:36 -08:00
Will Greenberg
75d3740f66 Add FAQ to readme 2025-03-07 11:21:36 -08:00
oopsbagel
94c576fd96 fix(tools): add pycrate dependency to requirements.txt
nasparse.py and nasparse_test.py require the pycrate_mobile and
pycrate_core libraries provided by the pycrate package.

This commit adds the required package to requirements.txt.
2025-03-07 11:08:20 -08:00
Cooper Quintin
ee83613757 update readme 2025-02-27 17:29:48 -08:00
Cooper Quintin
840f8ad8b0 stop before upload in case file is locked from writing by running process 2025-02-10 11:26:27 -08:00
Cooper Quintin
c9ac834ca7 show warnings in web UI 2025-02-10 11:26:27 -08:00
Cooper Quintin
8629aacf6b switch default to not see trace messages, switch arg from quiet to verbose 2025-02-10 11:26:27 -08:00
Cooper Quintin
a3fd1479f9 rename qmdl path so that downloaded files have a qmdl extension 2025-02-10 11:26:27 -08:00
Cooper Quintin
049c563f02 fix shortcodes on rayhunter_check 2025-02-10 11:26:27 -08:00
Cooper Quintin
a33b5a3418 Update README.md
Co-authored-by: Will Greenberg <willg@eff.org>
2025-01-31 17:00:44 -08:00
Cooper Quintin
107ba58296 warn if running install scritps from git tree 2025-01-31 17:00:44 -08:00
Cooper Quintin
d016279172 some tweaks to readme 2025-01-31 17:00:44 -08:00
Will Greenberg
5a084f1abb lib: set uplink flag for NAS 2025-01-30 11:33:14 -08:00
Will Greenberg
3619df32ab check: give qmdl-path a shorthand arg 2025-01-28 11:02:19 -08:00
Will Greenberg
34d87d1fd7 this macro isn't public, so docstrings won't work 2025-01-28 11:02:19 -08:00
Will Greenberg
da4952e70f fix docstring code 2025-01-28 11:02:19 -08:00
Will Greenberg
30323b8329 Keep old 2G downgrade analyzer 2025-01-28 11:02:19 -08:00
Will Greenberg
28b0f409db fix attribution 2025-01-28 11:02:19 -08:00
Will Greenberg
12640cc878 Rewrite our 2G downgrade analyzer 2025-01-28 11:02:19 -08:00
Will Greenberg
26eda5904f Better wording on IMSI requested warning 2025-01-28 11:02:19 -08:00
Will Greenberg
3e26e61b05 check: don't count informational events as warnings, better logging 2025-01-28 11:02:19 -08:00
Will Greenberg
565c0f1e67 serial: fix UTF-8 panic on macOS 2025-01-26 17:05:42 -08:00
Will Greenberg
6bd36921d8 consider early IMSI request medium sev 2025-01-08 15:23:59 -08:00
Will Greenberg
c83ae30be8 fix language 2025-01-08 15:23:59 -08:00
Will Greenberg
fa612241a5 lib: add IMSI requested heuristic 2025-01-08 15:23:59 -08:00
Will Greenberg
10592bbd9d lib: add inbound/outbound field to NAS 2025-01-06 16:24:11 -08:00
Will Greenberg
327eaddcd7 rayhunter-check: pcapify qmdl 2025-01-06 16:24:11 -08:00
Will Greenberg
32149c3b37 Update tools/nasparse.py 2024-12-17 14:46:31 -08:00
Cooper Quintin
e47d4dacc4 raise error on non nas message 2024-12-17 14:46:31 -08:00
Cooper Quintin
4009e3d1ed fix nits 2024-12-17 14:46:31 -08:00
Cooper Quintin
b2cd735a07 proof of concept pcap reader for nas heuristic 2024-12-17 14:46:31 -08:00
Cooper Quintin
94e9a88a91 PoC of python nas heuristic 2024-12-17 14:46:31 -08:00
77 changed files with 3365 additions and 1967 deletions

View File

@@ -1,3 +1,11 @@
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
# optimizations to reduce the binary size
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

1
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1 @@
c5bbaabe15d4ccfee97b9997a13569fbfea13c45

59
.github/ISSUE_TEMPLATE/bug.yaml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Bug Report
description: File a bug report.
title: "[Bug]: "
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
attributes:
label: Rayhunter Version
description: |
Which version did you install?
placeholder: "v0.2.6"
- type: input
attributes:
label: Capture Date
description: |
YYYY-MM-DD
placeholder: "2025-05-01"
validations:
required: true
- type: input
attributes:
label: Capture Location
description: |
(If comfortable disclosing) What region or country were you in?
placeholder: Washington State
- type: input
attributes:
label: Device and Model
description: |
Device you installed Rayhunter on to.
placeholder: Orbic RC400L
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: |
What steps did you take to get to your issue?
placeholder: "Tell us what you see!"
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: Rayhunter's behavior differed from what I expected because.
placeholder: "What was expected?"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Rayhunter data captures (QMDL and PCAP logs) or error codes
render: shell

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Rayhunter Mattermost
url: https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw&md=link&sbr=su
about: If you're having trouble using Rayhunter and aren't sure you've found a bug or request for a new feature, please first try asking for help here. There is a much larger community there of people familiar with the project who will be able to more quickly answer your questions.
- name: Rayhunter Security Policy
url: https://github.com/EFForg/rayhunter/security/advisories/new
about: Please report security vulnerabilities here.

27
.github/ISSUE_TEMPLATE/feature.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Feature Request
description: Suggest a new feature or improvement to Rayhunter
title: "[Feature Request]: "
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: What problem does this feature solve or what does it enhance?
description: Explain what this feature addresses, ors the benefit it provides.
placeholder: For example, "Currently, users have to manually do X, which is time-consuming."
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like to see implemented.
placeholder: For example, "Implement a new button that automatically does X."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions?
placeholder: For example, "We considered Y, but Z is a better approach because..."

6
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,6 @@
## Pull Request Checklist
- [ ] The Rayhunter team has recently expressed interest in reviewing a PR for this. If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
- [ ] Added or updated any documentation as needed to support the changes in this PR.
- [ ] Code has been linted and run through `cargo fmt`
- [ ] If any new functionality has been added, unit tests were also added

View File

@@ -0,0 +1,62 @@
name: Bug Report
description: File a bug report.
title: "[Bug]: "
type: Bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
attributes:
label: Rayhunter Version
description: |
Which version did you install?
placeholder: v0.2.6
- type: input
attributes:
label: Capture Date
description: |
YYYY-MM-DD
placeholder: 2025-05-01
validations:
required: true
- type: input
attributes:
label: Capture Location
description: |
(If comfortable disclosing) What region or country were you in?
placeholder: Washington State
validations:
required: false
- type: input
attributes:
label: Device and Model
description: |
Device you installed Rayhunter on to.
placeholder: Orbic RC400L
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: |
What steps did you take to get to your issue?
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: Rayhunter's behavior differed from what I expected because.
placeholder: "What was expected?"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Rayhunter data captures (QMDL and PCAP logs) or error codes
render: shell

View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Rayhunter Mattermost
url: https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw&md=link&sbr=su
about: If you're having trouble using Rayhunter and aren't sure you've found a bug or request for a new feature, please first try asking for help here. There is a much larger community there of people familiar with the project who will be able to more quickly answer your questions.
- name: Rayhunter Security Policy
url: https://github.com/EFForg/rayhunter/security/advisories/new
about: Please report security vulnerabilities here.

View File

@@ -0,0 +1,27 @@
name: Feature Request
description: Suggest a new feature or improvement to Rayhunter
title: "[Feature Request]: "
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: What problem does this feature solve or what does it enhance?
description: Explain what this feature addresses, ors the benefit it provides.
placeholder: For example, "Currently, users have to manually do X, which is time-consuming."
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like to see implemented.
placeholder: For example, "Implement a new button that automatically does X."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions?
placeholder: For example, "We considered Y, but Z is a better approach because..."

View File

@@ -3,6 +3,8 @@ name: Build Release
on:
push:
branches: [main, "release-*"]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
@@ -12,31 +14,43 @@ jobs:
strategy:
matrix:
platform:
- os: ubuntu-latest
serial_build_name: serial
check_build_name: rayhunter-check
- os: macos-latest
serial_build_name: serial
check_build_name: rayhunter-check
- name: ubuntu-24
os: ubuntu-latest
target: x86_64-unknown-linux-musl
- name: ubuntu-24-aarch64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
- name: macos-arm
os: macos-latest
target: aarch64-apple-darwin
- name: macos-intel
os: macos-13
target: x86_64-apple-darwin
- name: windows-x86_64
os: windows-latest
target: x86_64-pc-windows-gnu
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- name: Build serial
run: cargo build --bin serial --release
run: cargo build --bin serial --release --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: serial-${{ matrix.platform.os }}
path: ./target/release/${{ matrix.platform.serial_build_name }}
name: serial-${{ matrix.platform.name }}
path: target/${{ matrix.platform.target }}/release/serial${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
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 }}
name: rayhunter-check-${{ matrix.platform.name }}
path: target/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
if-no-files-found: error
build_rootshell_and_rayhunter:
build_rootshell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -55,25 +69,44 @@ jobs:
name: rootshell
path: target/armv7-unknown-linux-gnueabihf/release/rootshell
if-no-files-found: error
build_rayhunter:
strategy:
matrix:
device:
- name: tplink
- name: orbic
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: armv7-unknown-linux-gnueabihf
- name: Install cross-compilation dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf
version: 1.0
- name: Build rayhunter-daemon (arm32)
run: cargo build --bin rayhunter-daemon --target armv7-unknown-linux-gnueabihf --release
run: cargo build --bin rayhunter-daemon --target armv7-unknown-linux-gnueabihf --release --no-default-features --features ${{ matrix.device.name }}
- uses: actions/upload-artifact@v4
with:
name: rayhunter-daemon
name: rayhunter-daemon-${{ matrix.device.name }}
path: target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon
if-no-files-found: error
build_release_zip:
needs:
- build_serial_and_check
- build_rootshell_and_rayhunter
- build_rootshell
- build_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-check-*/rayhunter-check 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
run: mv rayhunter-daemon-* rootshell/rootshell serial-* dist
- name: Archive release directory
run: tar -cvf release.tar -C dist .
# TODO: have this create a release directly

View File

@@ -11,10 +11,31 @@ env:
jobs:
check_and_test:
strategy:
matrix:
device:
- name: tplink
- name: orbic
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check
run: cargo check --verbose
run: cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
- name: Run tests
run: cargo test --verbose
run: cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
windows_serial_check_and_test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: cargo check
shell: bash
run: |
cd serial
cargo check --verbose
- name: cargo test
shell: bash
run: |
cd serial
cargo test --verbose --no-default-features --features=${{ matrix.device.name }}

1
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1 @@
This project is governed by [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode).

844
Cargo.lock generated

File diff suppressed because it is too large Load Diff

165
README.md
View File

@@ -1,97 +1,130 @@
![Rayhunter Logo - An Orca taking a bite out of a cellular signal bar](https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png)
# Rayhunter
```
@@@@@@@ @@@@@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@ @@@@@@@ @@@@@@@@ @@@@@@@
@@! @@@ @@! @@@ @@! !@@ @@! @@@ @@! @@@ @@!@!@@@ @@! @@! @@! @@@
@!@!!@! @!@!@!@! !@!@! @!@!@!@! @!@ !@! @!@@!!@! @!! @!!!:! @!@!!@!
!!: :!! !!: !!! !!: !!: !!! !!: !!! !!: !!! !!: !!: !!: :!!
: : : : : : .: : : : :.:: : :: : : : :: ::: : : :
_ _ _ _ _ _ _ _
)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_)`'-.,_
O .
O ' '
o ' .
o .'
__________.-' '...___
.-' ### '''...__
/ a### ## ''--.._ ______
'. # ######## ' .-'
'-._ ..**********#### ___...---'''\ '
'-._ __________...---''' \ l
\ | apc '._|
\__;
```
![Tests](https://github.com/EFForg/rayhunter/actions/workflows/check-and-test.yml/badge.svg)
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.
**THIS CODE IS PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS**
**THIS CODE IS A 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
## Setup
Rayhunter has been 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.
You can 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).
*NOTE: We don't currently support automated installs on windows, you will have to follow the manual install instructions below*
## Setup (Mac, Linux)
1. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and extract it.
2. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`).
3. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
1. Download the latest `release.tar` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases)
2. Unzip the `release.tar`. Open the terminal and navigate to the folder
## Usage
```bash
cd ~/Downloads/release
```
Once installed, rayhunter will run automatically whenever your Orbic device is running. It serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, and view heuristic analyses of captures. You can access this UI in one of two ways:
3. Turn on the Orbic device by holding the power button for 3 seconds. Plug it into your computer using a USB-C Cable.
4. Run the install script for your operating system:
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`. 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).
```bash
./install.sh
```
The device will restart multiple times over the next few minutes.
You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
5. Rayhunter should now be running! You can verify this by following the instructions below to [view the web UI](#usage-viewing-the-web-ui). You should also see a green line flash along the top of top the display on the device.
### Installation Notes
* Note: If you are installing from the cloned GitHub repository please see the development instructions below, running `install.sh` from the git tree will not work.
* The install script has only been tested for Linux on the latest version of Ubuntu. If it fails you will need to follow the install steps outlined in **Development** below.
* On macOS if you encounter an error that says "No Orbic device found," it may because you the "Allow accessories to connect" security setting set to "Ask for approval." You may need to temporarily change it to "Always" for the script to run. Make sure to change it back to a more secure setting when you're done.
## Setup (Windows)
We don't currently support automated installs on Windows.
## Updating
Great news: if you've successfully installed rayhunter, you already know how to update it! Our update process is identical to the setup process: simply download the latest release and follow the steps in the [setup section](#setup-silicon-mac-linux).
## Usage (viewing the web UI)
Once installed, Rayhunter will run automatically whenever your Orbic device is running. You'll see a green line on top of the device's display to indicate that it's running and recording. [The line will turn red](#red) once a potential IMSI catcher has been found, until the device is rebooted or a new recording is started through the web UI.
It also serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, and view heuristic analyses of captures.
You can access this UI in one of two ways:
1. **Connect over wifi:** Connect your phone/laptop to the Orbic's 2.4GHz wifi network and visit [http://192.168.1.1:8080](http://192.168.1.1:8080). (Click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet).
* You can find the wifi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
2. **Connect over USB:** Connect the Orbic device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit [http://localhost:8080](http://localhost:8080).
* For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the `releases/platform-tools/` folder to somewhere else in your path or you can install it manually.
* You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet).
* On macOS, the easiest way to install ADB is with Homebrew: First [install Homebrew](https://brew.sh/), then run `brew install android-platform-tools`.
## Frequently Asked Questions
### Do I need an active SIM card to use Rayhunter?
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but whether that SIM card has to be currently active for our tests to work is still under investigation. If you want to use the device as a hotspot in addition to a research device an active plan would of course be necessary, however we have not done enough testing yet to know whether an active subscription is required for detection. If you want to test the device with an inactive SIM card, we would certainly be interested in seeing any data you collect, and especially any runs that trigger an alert!
<a name="red"></a>
### Help, Rayhunter's line is red! What should I do?
Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with the a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area (or put it on airplane mode) and tell your friends to do the same!
If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (QMDL and PCAP logs) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
Please note that this file may contain sensitive information such as your IMSI and the unique IDs of cell towers you were near which could be used to ascertain your location at the time.
### Does Rayhunter work outside of the US?
**Probably**. Some Rayhunter users have reported successfully using it in other countries with unlocked devices and SIM cards from local telcos. We can't guarantee whether or not it will work for you though.
### Should I get a locked or unlocked orbic device? What is the difference?
If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear how locked the locked devices are nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices.
### Does Rayhunter work on any other devices besides the Orbic RC400L?
**Maybe**. We have not tested Rayhunter on any other hardware but we would love to expand the supported platforms. We will consider giving official support to any hardware platform that can be bought for around $20-30USD. The Rayhunter daemon should theoretically work on any Linux/Android device that has a qualcomm chip with a `/dev/diag` interface and root access, though our installer script has only been tested with an Orbic. If you get it working on another device, please let us know!
### How do I delete capture files from the Rayhunter device?
You can get a shell on the device by inputting `adb shell` to a terminal with the device connected, you can check if it is detected with `adb devices`.
The capture files are located at */data/rayhunter/qmdl* but you will need root access to modify or delete them. From the adb shell run `/bin/rootshell` and you can now use commands like 'rm' as root to modify and delete entries in the */data/rayhunter/qmdl* directory. **Be careful not to delete important files in other directories as you may seriously damage the device**
## Development
Follow these instructions if you need to build Rayhunter from source rather than using our [compiled builds](https://github.com/EFForg/rayhunter/releases).
* 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.
* run `sudo apt install build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf`
### If you're on x86 linux
* set up cross compliing for rust:
```
Install Rust the usual way and then install cross compiling dependences:
```bash
sudo apt install curl build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf
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 `./tools/install-dev.sh`
Now you can root your device and install Rayhunter by running `./tools/install-dev.sh`
### 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
## Support and Discussion
* Build for arm using `cargo build`
If you're having issues installing or using Rayhunter, please open an issue in this repo. Join us in the `#rayhunter` channel of [EFF's Mattermost](https://opensource.eff.org/signup_user_complete/?id=r1b6cnta9bysxk6im3kuabiu1y&md=link&sbr=su) instance to chat!
* 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)
* 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.
* Write your code and write tests
* Build for arm using `cargo build`
* Run tests using `cargo test_pc`
* push to the device with `./make.sh`
## 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 believe 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!*

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Security vulnerabilities can be reported using GitHub's [private vulnerability reporting tool](https://github.com/EFForg/rayhunter/security/advisories/new).

View File

@@ -1,8 +1,15 @@
[package]
name = "rayhunter-daemon"
version = "0.1.0"
version = "0.2.8"
edition = "2021"
[features]
# These feature flags are mutually exclusive, and exactly one must be enabled.
orbic = ["rayhunter/orbic"]
tplink = ["rayhunter/tplink"]
default = ["orbic"]
[[bin]]
name = "rayhunter-daemon"
path = "src/daemon.rs"
@@ -15,13 +22,14 @@ path = "src/check.rs"
rayhunter = { path = "../lib" }
toml = "0.8.8"
serde = { version = "1.0.193", features = ["derive"] }
tokio = { version = "1.35.1", features = ["full"] }
axum = "0.7.3"
tokio = { version = "1.44.2", features = ["full"] }
axum = "0.8"
futures-core = "0.3.30"
thiserror = "1.0.52"
libc = "0.2.150"
log = "0.4.20"
env_logger = "0.10.1"
tokio-util = { version = "0.7.10", features = ["rt"] }
tokio-util = { version = "0.7.10", features = ["rt", "io"] }
futures-macro = "0.3.30"
include_dir = "0.7.3"
mime_guess = "2.0.4"
@@ -30,5 +38,6 @@ 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"
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
tempfile = "3.10.1"
simple_logger = "5.0.0"

View File

@@ -18,9 +18,9 @@ use tokio::sync::mpsc::Receiver;
use tokio::sync::{RwLock, RwLockWriteGuard};
use tokio_util::task::TaskTracker;
use crate::dummy_analyzer::TestAnalyzer;
use crate::qmdl_store::RecordingStore;
use crate::server::ServerState;
use crate::dummy_analyzer::TestAnalyzer;
pub struct AnalysisWriter {
writer: BufWriter<File>,
@@ -53,7 +53,10 @@ impl AnalysisWriter {
// 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> {
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?;
@@ -114,7 +117,7 @@ async fn perform_analysis(
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)
.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)
@@ -182,7 +185,10 @@ pub fn run_analysis_thread(
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 {
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;

View File

@@ -1,35 +1,57 @@
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
use rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader};
use tokio::fs::{metadata, read_dir, File};
use clap::Parser;
use futures::TryStreamExt;
use log::{info, warn};
use rayhunter::{
analysis::analyzer::{EventType, Harness},
diag::DataType,
gsmtap_parser,
pcap::GsmtapPcapWriter,
qmdl::QmdlReader,
};
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
use tokio::fs::{metadata, read_dir, File};
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,
}
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 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()
let mut qmdl_stream = pin!(qmdl_reader
.as_stream()
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
let mut skipped_reasons: HashMap<String, i32> = HashMap::new();
let mut total_messages = 0;
let mut warnings = 0;
let mut skipped = 0;
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") {
while let Some(container) = qmdl_stream
.try_next()
.await
.expect("failed getting QMDL container")
{
let row = harness.analyze_qmdl_messages(container);
total_messages += 1;
for reason in row.skipped_message_reasons {
@@ -38,47 +60,113 @@ async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool
}
for analysis in row.analysis {
for maybe_event in analysis.events {
if let Some(event) = maybe_event {
warnings += 1;
println!("{}: {:?}", analysis.timestamp, event);
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 {
println!("{}: messages skipped:", qmdl_path);
info!("{}: messages skipped:", qmdl_path);
for (reason, count) in skipped_reasons.iter() {
println!(" - {}: \"{}\"", count, reason);
info!(" - {}: \"{}\"", count, reason);
}
}
println!("{}: {} messages analyzed, {} warnings, {} messages skipped", qmdl_path, total_messages, warnings, skipped);
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 msg in container.into_messages().into_iter().flatten() {
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() {
env_logger::init();
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 }));
}
println!("Analyzers:");
info!("Analyzers:");
for analyzer in harness.get_metadata().analyzers {
println!(" - {}: {}", analyzer.name, analyzer.description);
info!(" - {}: {}", analyzer.name, analyzer.description);
}
let metadata = metadata(&args.qmdl_path).await.expect("failed to get metadata");
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") {
analyze_file(&mut harness, entry.path().to_str().unwrap(), args.show_skipped).await;
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 {
analyze_file(&mut harness, args.qmdl_path.to_str().unwrap(), args.show_skipped).await;
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;
}
}
}

View File

@@ -2,17 +2,8 @@ use crate::error::RayhunterError;
use serde::Deserialize;
#[derive(Deserialize)]
struct ConfigFile {
qmdl_store_path: Option<String>,
port: Option<u16>,
debug_mode: Option<bool>,
ui_level: Option<u8>,
enable_dummy_analyzer: Option<bool>,
colorblind_mode: Option<bool>,
}
#[derive(Debug)]
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Config {
pub qmdl_store_path: String,
pub port: u16,
@@ -35,19 +26,15 @@ impl Default for Config {
}
}
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef<std::path::Path> {
let mut config = Config::default();
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
where
P: AsRef<std::path::Path>,
{
if let Ok(config_file) = std::fs::read_to_string(&path) {
let parsed_config: ConfigFile = toml::from_str(&config_file)
.map_err(RayhunterError::ConfigFileParsingError)?;
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(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
} else {
Ok(Config::default())
}
Ok(config)
}
pub struct Args {

View File

@@ -1,42 +1,62 @@
mod analysis;
mod config;
mod diag;
mod display;
mod dummy_analyzer;
mod error;
mod pcap;
mod qmdl_store;
mod server;
mod stats;
mod qmdl_store;
mod diag;
mod framebuffer;
mod dummy_analyzer;
use crate::config::{parse_config, parse_args};
use crate::config::{parse_args, parse_config};
use crate::diag::run_diag_read_thread;
use crate::qmdl_store::RecordingStore;
use crate::server::{ServerState, get_qmdl, serve_static};
use crate::pcap::get_pcap;
use crate::stats::get_system_stats;
use crate::error::RayhunterError;
use crate::framebuffer::Framebuffer;
use crate::pcap::get_pcap;
use crate::qmdl_store::RecordingStore;
use crate::server::{get_qmdl, serve_static, ServerState};
use crate::stats::get_system_stats;
use analysis::{get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus};
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};
use rayhunter::diag_device::DiagDevice;
use axum::routing::{get, post};
use axum::Router;
use diag::{
delete_all_recordings, delete_recording, get_analysis_report, start_recording, stop_recording,
DiagDeviceCtrlMessage,
};
use log::{error, info};
use qmdl_store::RecordingStoreError;
use rayhunter::diag_device::DiagDevice;
use stats::get_qmdl_manifest;
use tokio::sync::mpsc::{self, Sender, Receiver};
use tokio::sync::oneshot::error::TryRecvError;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::mpsc::{self, Sender};
use tokio::sync::{oneshot, RwLock};
use tokio::task::JoinHandle;
use tokio_util::task::TaskTracker;
use std::net::SocketAddr;
use std::thread::sleep;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::{RwLock, oneshot};
use std::sync::Arc;
use include_dir::{include_dir, Dir};
type AppRouter = Router<Arc<ServerState>>;
fn get_router() -> AppRouter {
Router::new()
.route("/api/pcap/{name}", get(get_pcap))
.route("/api/qmdl/{name}", get(get_qmdl))
.route("/api/system-stats", get(get_system_stats))
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
.route("/api/start-recording", post(start_recording))
.route("/api/stop-recording", post(stop_recording))
.route("/api/delete-recording/{name}", post(delete_recording))
.route("/api/delete-all-recordings", post(delete_all_recordings))
.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))
}
// Runs the axum server, taking all the elements needed to build up our
// ServerState and a oneshot Receiver that'll fire when it's time to shutdown
@@ -44,44 +64,19 @@ use include_dir::{include_dir, Dir};
async fn run_server(
task_tracker: &TaskTracker,
config: &config::Config,
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
state: Arc<ServerState>,
server_shutdown_rx: oneshot::Receiver<()>,
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,
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()
.route("/api/pcap/*name", get(get_pcap))
.route("/api/qmdl/*name", get(get_qmdl))
.route("/api/system-stats", get(get_system_stats))
.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/*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);
let app = get_router().with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
let listener = TcpListener::bind(&addr).await.unwrap();
task_tracker.spawn(async move {
info!("The orca is hunting for stingrays...");
axum::serve(listener, app)
.with_graceful_shutdown(server_shutdown_signal(server_shutdown_rx))
.await.unwrap();
.await
.unwrap();
})
}
@@ -90,13 +85,31 @@ async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
info!("Server received shutdown signal, exiting...");
}
// Loads a QmdlStore if one exists, and if not, only create one if we're not in
// debug mode.
// Loads a RecordingStore if one exists, and if not, only create one if we're
// not in debug mode. If we fail to parse the manifest AND we're not in debug
// mode, try to recover by making a new (empty) manifest in the same directory.
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
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::NoStoreDebugMode(config.qmdl_store_path.clone())),
let store_exists = RecordingStore::exists(&config.qmdl_store_path).await?;
if config.debug_mode {
if store_exists {
Ok(RecordingStore::load(&config.qmdl_store_path).await?)
} else {
Err(RayhunterError::NoStoreDebugMode(
config.qmdl_store_path.clone(),
))
}
} else if store_exists {
match RecordingStore::load(&config.qmdl_store_path).await {
Ok(store) => Ok(store),
Err(RecordingStoreError::ParseManifestError(err)) => {
error!("failed to parse QMDL manifest: {}", err);
info!("creating new empty manifest...");
Ok(RecordingStore::create(&config.qmdl_store_path).await?)
}
Err(err) => Err(err.into()),
}
} else {
Ok(RecordingStore::create(&config.qmdl_store_path).await?)
}
}
@@ -121,18 +134,24 @@ fn run_ctrl_c_thread(
info!("Done!");
}
server_shutdown_tx.send(())
server_shutdown_tx
.send(())
.expect("couldn't send server shutdown signal");
info!("sending UI shutdown");
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
ui_shutdown_tx.send(())
ui_shutdown_tx
.send(())
.expect("couldn't send ui shutdown signal");
}
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
diag_device_sender
.send(DiagDeviceCtrlMessage::Exit)
.await
.expect("couldn't send Exit message to diag thread");
analysis_tx.send(AnalysisCtrlMessage::Exit).await
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);
}
@@ -141,69 +160,6 @@ fn run_ctrl_c_thread(
})
}
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?
let mut img: Option<&[u8]> = None;
if display_level == 2 {
img = Some(IMAGE_DIR.get_file("orca.gif").expect("failed to read orca.gif").contents());
} else if display_level == 3 {
img = Some(IMAGE_DIR.get_file("eff.png").expect("failed to read eff.png").contents());
}
loop {
match ui_shutdown_rx.try_recv() {
Ok(_) => {
info!("received UI shutdown");
break;
},
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());
},
3 => {
fb.draw_img(img.unwrap())
},
128 => {
fb.draw_line(framebuffer::Color565::Cyan, 128);
fb.draw_line(framebuffer::Color565::Pink, 102);
fb.draw_line(framebuffer::Color565::White, 76);
fb.draw_line(framebuffer::Color565::Pink, 50);
fb.draw_line(framebuffer::Color565::Cyan, 25);
},
1 | _ => {
fb.draw_line(display_color, 2);
},
};
sleep(Duration::from_millis(1000));
}
})
}
#[tokio::main]
async fn main() -> Result<(), RayhunterError> {
env_logger::init();
@@ -218,28 +174,58 @@ async fn main() -> Result<(), RayhunterError> {
let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
let (ui_update_tx, ui_update_rx) = mpsc::channel::<framebuffer::DisplayState>(1);
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
let mut maybe_ui_shutdown_tx = None;
if !config.debug_mode {
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
let mut dev = DiagDevice::new().await
let mut dev = DiagDevice::new()
.await
.map_err(RayhunterError::DiagInitError)?;
dev.config_logs().await
dev.config_logs()
.await
.map_err(RayhunterError::DiagInitError)?;
info!("Starting Diag Thread");
run_diag_read_thread(&task_tracker, dev, rx, ui_update_tx.clone(), qmdl_store_lock.clone(), config.enable_dummy_analyzer);
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);
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
}
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
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;
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(),
);
let state = Arc::new(ServerState {
qmdl_store_lock: qmdl_store_lock.clone(),
diag_device_ctrl_sender: tx,
ui_update_sender: ui_update_tx,
debug_mode: config.debug_mode,
analysis_status_lock,
analysis_sender: analysis_tx,
});
run_server(&task_tracker, &config, state, server_shutdown_rx).await;
task_tracker.close();
task_tracker.wait().await;
@@ -247,3 +233,14 @@ async fn main() -> Result<(), RayhunterError> {
info!("see you space cowboy...");
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_get_router() {
// assert that creating the router does not panic from invalid route patterns.
let _ = get_router();
}
}

View File

@@ -6,21 +6,21 @@ use axum::extract::{Path, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use futures::{StreamExt, TryStreamExt};
use log::{debug, error, info};
use rayhunter::diag::DataType;
use rayhunter::diag_device::DiagDevice;
use tokio::sync::RwLock;
use tokio::sync::mpsc::{Receiver, Sender};
use rayhunter::qmdl::QmdlWriter;
use log::{debug, error, info};
use tokio::fs::File;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::RwLock;
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;
use crate::display;
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
use crate::server::ServerState;
pub enum DiagDeviceCtrlMessage {
StopRecording,
@@ -32,7 +32,7 @@ pub fn run_diag_read_thread(
task_tracker: &TaskTracker,
mut dev: DiagDevice,
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
ui_update_sender: Sender<framebuffer::DisplayState>,
ui_update_sender: Sender<display::DisplayState>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
enable_dummy_analyzer: bool,
) {
@@ -99,12 +99,12 @@ pub fn run_diag_read_thread(
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
ui_update_sender.send(display::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
qmdl_store.update_entry_analysis_size(index, analysis_file_len).await
.expect("failed to update analysis file size");
}
},
@@ -119,57 +119,186 @@ pub fn run_diag_read_thread(
});
}
pub async fn start_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
pub async fn start_recording(
State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
}
let mut qmdl_store = state.qmdl_store_lock.write().await;
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't create new qmdl entry: {}", e)))?;
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't create new qmdl entry: {}", e),
)
})?;
let qmdl_writer = QmdlWriter::new(qmdl_file);
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
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)))?;
let display_state = display::DisplayState::Recording;
state
.ui_update_sender
.send(display_state)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send ui update message: {}", e),
)
})?;
Ok((StatusCode::ACCEPTED, "ok".to_string()))
}
pub async fn stop_recording(State(state): State<Arc<ServerState>>) -> Result<(StatusCode, String), (StatusCode, String)> {
pub async fn stop_recording(
State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, String)> {
if state.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)))?;
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(display::DisplayState::Paused)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send ui update message: {}", e),
)
})?;
Ok((StatusCode::ACCEPTED, "ok".to_string()))
}
pub async fn get_analysis_report(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
pub async fn delete_recording(
State(state): State<Arc<ServerState>>,
Path(qmdl_name): Path<String>,
) -> Result<(StatusCode, String), (StatusCode, 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;
match qmdl_store.delete_entry(&qmdl_name).await {
Err(RecordingStoreError::NoSuchEntryError) => {
return Err((
StatusCode::BAD_REQUEST,
format!("no recording with name {qmdl_name}"),
))
}
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't delete recording: {e}"),
))
}
Ok(_) => {}
}
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(display::DisplayState::Paused)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send ui update message: {}", e),
)
})?;
Ok((StatusCode::ACCEPTED, "ok".to_string()))
}
pub async fn delete_all_recordings(
State(state): State<Arc<ServerState>>,
) -> Result<(StatusCode, String), (StatusCode, 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.delete_all_entries().await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't delete all recordings: {}", 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(display::DisplayState::Paused)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send ui update message: {}", e),
)
})?;
Ok((StatusCode::ACCEPTED, "ok".to_string()))
}
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 (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()
"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)
format!("Couldn't find QMDL entry with name \"{}\"", qmdl_name),
))?
};
let analysis_file = qmdl_store.open_entry_analysis(entry_index).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);

View File

@@ -0,0 +1,202 @@
use image::{codecs::gif::GifDecoder, imageops::FilterType, AnimationDecoder, DynamicImage};
use std::io::Cursor;
use std::time::Duration;
use crate::config;
use crate::display::DisplayState;
use log::{error, info};
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio::sync::oneshot::error::TryRecvError;
use tokio_util::task::TaskTracker;
use std::thread::sleep;
use include_dir::{include_dir, Dir};
#[derive(Copy, Clone)]
pub struct Dimensions {
pub height: u32,
pub width: u32,
}
#[allow(dead_code)]
#[derive(Copy, Clone)]
pub enum Color {
Red,
Green,
Blue,
White,
Black,
Cyan,
Yellow,
Pink,
}
impl Color {
fn rgb(self) -> (u8, u8, u8) {
match self {
Color::Red => (0xff, 0, 0),
Color::Green => (0, 0xff, 0),
Color::Blue => (0, 0, 0xff),
Color::White => (0xff, 0xff, 0xff),
Color::Black => (0, 0, 0),
Color::Cyan => (0, 0xff, 0xff),
Color::Yellow => (0xff, 0xff, 0),
Color::Pink => (0xfe, 0x24, 0xff),
}
}
}
impl Color {
fn from_state(state: DisplayState, colorblind_mode: bool) -> Self {
match state {
DisplayState::Paused => Color::White,
DisplayState::Recording => {
if colorblind_mode {
Color::Blue
} else {
Color::Green
}
}
DisplayState::WarningDetected => Color::Red,
}
}
}
pub trait GenericFramebuffer: Send + 'static {
fn dimensions(&self) -> Dimensions;
fn write_buffer(
&mut self,
buffer: &[(u8, u8, u8)], // rgb, row-wise, left-to-right, top-to-bottom
);
fn write_dynamic_image(&mut self, img: DynamicImage) {
let dimensions = self.dimensions();
let mut width = img.width();
let mut height = img.height();
let resized_img: DynamicImage;
if height > dimensions.height || width > dimensions.width {
resized_img = img.resize(dimensions.width, dimensions.height, FilterType::CatmullRom);
width = dimensions.width.min(resized_img.width());
height = dimensions.height.min(resized_img.height());
} else {
resized_img = img;
}
let img_rgba8 = resized_img.as_rgba8().unwrap();
let mut buf = Vec::new();
for y in 0..height {
for x in 0..width {
let px = img_rgba8.get_pixel(x, y);
buf.push((px[0], px[1], px[2]));
}
}
self.write_buffer(&buf);
}
fn draw_gif(&mut self, img_buffer: &[u8]) {
// this is dumb and i'm sure there's a better way to loop this
let cursor = Cursor::new(img_buffer);
let decoder = GifDecoder::new(cursor).unwrap();
for maybe_frame in decoder.into_frames() {
let frame = maybe_frame.unwrap();
let (numerator, _) = frame.delay().numer_denom_ms();
let img = DynamicImage::from(frame.into_buffer());
self.write_dynamic_image(img);
std::thread::sleep(Duration::from_millis(numerator as u64));
}
}
fn draw_img(&mut self, img_buffer: &[u8]) {
let img = image::load_from_memory(img_buffer).unwrap();
self.write_dynamic_image(img);
}
fn draw_line(&mut self, color: Color, height: u32) {
let width = self.dimensions().width;
let px_num = height * width;
let mut buffer = Vec::new();
for _ in 0..px_num {
buffer.push(color.rgb());
}
self.write_buffer(&buffer);
}
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
mut fb: impl GenericFramebuffer,
mut ui_shutdown_rx: oneshot::Receiver<()>,
mut ui_update_rx: Receiver<DisplayState>,
) {
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
let display_level = config.ui_level;
if display_level == 0 {
info!("Invisible mode, not spawning UI.");
}
let colorblind_mode = config.colorblind_mode;
let mut display_color = Color::from_state(DisplayState::Recording, colorblind_mode);
task_tracker.spawn_blocking(move || {
// this feels wrong, is there a more rusty way to do this?
let mut img: Option<&[u8]> = None;
if display_level == 2 {
img = Some(
IMAGE_DIR
.get_file("orca.gif")
.expect("failed to read orca.gif")
.contents(),
);
} else if display_level == 3 {
img = Some(
IMAGE_DIR
.get_file("eff.png")
.expect("failed to read eff.png")
.contents(),
);
}
loop {
match ui_shutdown_rx.try_recv() {
Ok(_) => {
info!("received UI shutdown");
break;
}
Err(TryRecvError::Empty) => {}
Err(e) => panic!("error receiving shutdown message: {e}"),
}
match ui_update_rx.try_recv() {
Ok(state) => {
display_color = Color::from_state(state, colorblind_mode);
}
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());
}
3 => fb.draw_img(img.unwrap()),
128 => {
fb.draw_line(Color::Cyan, 128);
fb.draw_line(Color::Pink, 102);
fb.draw_line(Color::White, 76);
fb.draw_line(Color::Pink, 50);
fb.draw_line(Color::Cyan, 25);
}
_ => {
// this branch id for ui_level 1, which is also the default if an
// unknown value is used
fb.draw_line(display_color, 2);
}
};
sleep(Duration::from_millis(1000));
}
});
}

28
bin/src/display/mod.rs Normal file
View File

@@ -0,0 +1,28 @@
mod generic_framebuffer;
#[cfg(feature = "tplink")]
mod tplink;
#[cfg(feature = "tplink")]
mod tplink_framebuffer;
#[cfg(feature = "tplink")]
mod tplink_onebit;
#[cfg(feature = "tplink")]
pub use tplink::update_ui;
#[cfg(feature = "orbic")]
mod orbic;
#[cfg(feature = "orbic")]
pub use orbic::update_ui;
pub enum DisplayState {
Recording,
Paused,
WarningDetected,
}
#[cfg(all(feature = "orbic", feature = "tplink"))]
compile_error!("cannot compile for many devices at once");
#[cfg(not(any(feature = "orbic", feature = "tplink")))]
compile_error!("cannot compile for no device at all");

49
bin/src/display/orbic.rs Normal file
View File

@@ -0,0 +1,49 @@
use crate::config;
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
use crate::display::DisplayState;
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
const FB_PATH: &str = "/dev/fb0";
#[derive(Copy, Clone, Default)]
struct Framebuffer;
impl GenericFramebuffer for Framebuffer {
fn dimensions(&self) -> Dimensions {
// TODO actually poll for this, maybe w/ fbset?
Dimensions {
height: 128,
width: 128,
}
}
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
let mut raw_buffer = Vec::new();
for (r, g, b) in buffer {
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
rgb565 |= (*g as u16 & 0b11111100) << 3;
rgb565 |= (*b as u16) >> 3;
raw_buffer.extend(rgb565.to_le_bytes());
}
std::fs::write(FB_PATH, &raw_buffer).unwrap();
}
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
ui_shutdown_rx: oneshot::Receiver<()>,
ui_update_rx: Receiver<DisplayState>,
) {
generic_framebuffer::update_ui(
task_tracker,
config,
Framebuffer,
ui_shutdown_rx,
ui_update_rx,
)
}

29
bin/src/display/tplink.rs Normal file
View File

@@ -0,0 +1,29 @@
use log::info;
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
use crate::config;
use crate::display::{tplink_framebuffer, tplink_onebit, DisplayState};
use std::fs;
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
ui_shutdown_rx: oneshot::Receiver<()>,
ui_update_rx: Receiver<DisplayState>,
) {
let display_level = config.ui_level;
if display_level == 0 {
info!("Invisible mode, not spawning UI.");
}
if fs::exists(tplink_onebit::OLED_PATH).unwrap_or_default() {
info!("detected one-bit display");
tplink_onebit::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx)
} else {
info!("fallback to framebuffer");
tplink_framebuffer::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx)
}
}

View File

@@ -0,0 +1,90 @@
use std::fs::File;
use std::io::Write;
use std::os::fd::AsRawFd;
use crate::config;
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
use crate::display::DisplayState;
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
const FB_PATH: &str = "/dev/fb0";
struct Framebuffer;
#[repr(C)]
struct fb_fillrect {
dx: u32,
dy: u32,
width: u32,
height: u32,
color: u32,
rop: u32,
}
impl GenericFramebuffer for Framebuffer {
fn dimensions(&self) -> Dimensions {
// TODO actually poll for this, maybe w/ fbset?
Dimensions {
height: 128,
width: 128,
}
}
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
// for how to write to the buffer, consult M7350v5_en_gpl/bootable/recovery/recovery_color_oled.c
let dimensions = self.dimensions();
let width = dimensions.width;
let height = buffer.len() as u32 / width;
let mut f = File::options().write(true).open(FB_PATH).unwrap();
let mut arg = fb_fillrect {
dx: 0,
dy: 0,
width,
height,
color: 0xffff, // not sure what this is
rop: 0,
};
let mut raw_buffer = Vec::new();
for (r, g, b) in buffer {
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
rgb565 |= (*g as u16 & 0b11111100) << 3;
rgb565 |= (*b as u16) >> 3;
// note: big-endian!
raw_buffer.extend(rgb565.to_be_bytes());
}
f.write_all(&raw_buffer).unwrap();
unsafe {
let res = libc::ioctl(
f.as_raw_fd(),
0x4619, // FBIORECT_DISPLAY
&mut arg as *mut _,
std::mem::size_of::<fb_fillrect>(),
);
if res < 0 {
panic!("failed to send FBIORECT_DISPLAY ioctl, {}", res);
}
}
}
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
ui_shutdown_rx: oneshot::Receiver<()>,
ui_update_rx: Receiver<DisplayState>,
) {
generic_framebuffer::update_ui(
task_tracker,
config,
Framebuffer,
ui_shutdown_rx,
ui_update_rx,
)
}

View File

@@ -0,0 +1,170 @@
/// Display module for the TP-Link M7350 oled one-bit display.
///
/// https://github.com/m0veax/tplink_m7350/tree/main/oled
use crate::config;
use crate::display::DisplayState;
use log::{error, info};
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio::sync::oneshot::error::TryRecvError;
use tokio_util::task::TaskTracker;
use std::fs;
use std::thread::sleep;
use std::time::Duration;
pub const OLED_PATH: &str = "/sys/class/display/oled/oled_buffer";
// those coordinates were mainly chosen for a spot that doesn't get regularly updated by the main
// oledd service. otherwise we'd have to write to the display more than once per second to prevent
// the icon from flickering.
const STATUS_X: u8 = 104;
const STATUS_Y: u8 = 40;
const STATUS_W: u8 = 16;
const STATUS_H: u8 = 16;
macro_rules! pixel {
(x) => {
0
};
(_) => {
1
};
}
macro_rules! pixelart {
(x=$x:expr, y=$y:expr, width=$width:expr, height=$height:expr; $($a:tt $b:tt $c:tt $d:tt $e:tt $f:tt $g:tt $h:tt)*) => {{
// one bit per pixel + 4 bytes for header
const BUF_SIZE: usize = ($width as usize * $height as usize) / 8 + 4;
const BUF_BYTES: [u8; BUF_SIZE] = [
$x,
$y,
$width,
$height,
$(
(pixel!($a) << 7 | pixel!($b) << 6 | pixel!($c) << 5 | pixel!($d) << 4 | pixel!($e) << 3 | pixel!($f) << 2 | pixel!($g) << 1 | pixel!($h)),
)*
];
&BUF_BYTES
}}
}
const STATUS_PAUSED: &[u8] = pixelart! {
x=STATUS_X, y=STATUS_Y, width=STATUS_W, height=STATUS_H;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ x x x x x x x x x x _ _ _
_ x x _ _ _ _ _ _ _ _ _ _ x x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ x _ _ _ _ x _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x x _ _ _ _ _ _ _ _ _ _ x x _
_ _ _ x x x x x x x x x x _ _ _
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
};
const STATUS_SMILING: &[u8] = pixelart! {
x=STATUS_X, y=STATUS_Y, width=STATUS_W, height=STATUS_H;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ x x x x x x x x x x _ _ _
_ x x _ _ _ _ _ _ _ _ _ _ x x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ x _ _ _ _ x _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ x _ _ _ _ x _ _ _ x _
_ x _ _ _ x _ _ _ _ x _ _ _ x _
_ x _ _ _ x x x x x x _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x x _ _ _ _ _ _ _ _ _ _ x x _
_ _ _ x x x x x x x x x x _ _ _
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
};
const STATUS_WARNING: &[u8] = pixelart! {
x=STATUS_X, y=STATUS_Y, width=STATUS_W, height=STATUS_H;
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
_ _ _ x x x x x x x x x x _ _ _
_ x x _ _ _ _ _ _ _ _ _ _ x x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ x x _ _ _ _ _ x _
_ x _ _ _ _ _ x x _ _ _ _ _ x _
_ x _ _ _ _ _ x x _ _ _ _ _ x _
_ x _ _ _ _ _ x x _ _ _ _ _ x _
_ x _ _ _ _ _ x x _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x _ _ _ _ _ x x _ _ _ _ _ x _
_ x _ _ _ _ _ x x _ _ _ _ _ x _
_ x _ _ _ _ _ _ _ _ _ _ _ _ x _
_ x x _ _ _ _ _ _ _ _ _ _ x x _
_ _ _ x x x x x x x x x x _ _ _
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
};
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
mut ui_shutdown_rx: oneshot::Receiver<()>,
mut ui_update_rx: Receiver<DisplayState>,
) {
let display_level = config.ui_level;
if display_level == 0 {
info!("Invisible mode, not spawning UI.");
}
task_tracker.spawn_blocking(move || {
let mut pixels = STATUS_SMILING;
loop {
match ui_shutdown_rx.try_recv() {
Ok(_) => {
info!("received UI shutdown");
break;
}
Err(TryRecvError::Empty) => {}
Err(e) => panic!("error receiving shutdown message: {e}"),
}
match ui_update_rx.try_recv() {
Ok(DisplayState::Paused) => pixels = STATUS_PAUSED,
Ok(DisplayState::Recording) => pixels = STATUS_SMILING,
Ok(DisplayState::WarningDetected) => pixels = STATUS_WARNING,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
Err(e) => {
error!("error receiving framebuffer update message: {e}");
}
};
// we write the status every second because it may have been overwritten through menu
// navigation.
if display_level != 0 {
if let Err(e) = fs::write(OLED_PATH, &pixels) {
error!("failed to write to display: {e}");
}
}
sleep(Duration::from_millis(1000));
}
});
}
#[test]
fn test_pixelart_macro() {
assert_eq!(
STATUS_WARNING,
[
104, 40, 16, 16, 255, 255, 224, 7, 159, 249, 191, 253, 190, 125, 190, 125, 190, 125,
190, 125, 190, 125, 191, 253, 190, 125, 190, 125, 191, 253, 159, 249, 224, 7, 255, 255
]
);
}

View File

@@ -5,11 +5,11 @@ use rayhunter::telcom_parser::lte_rrc::{PCCH_MessageType, PCCH_MessageType_c1, P
use rayhunter::analysis::analyzer::{Analyzer, Event, EventType, Severity};
use rayhunter::analysis::information_element::{InformationElement, LteInformationElement};
pub struct TestAnalyzer{
pub struct TestAnalyzer {
pub count: i32,
}
impl Analyzer for TestAnalyzer{
impl Analyzer for TestAnalyzer {
fn get_name(&self) -> Cow<str> {
Cow::from("Example Analyzer")
}
@@ -22,12 +22,16 @@ impl Analyzer for TestAnalyzer{
self.count += 1;
if self.count % 100 == 0 {
return Some(Event {
event_type: EventType::Informational ,
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_msg = match ie {
InformationElement::LTE(lte_ie) => match &**lte_ie {
LteInformationElement::PCCH(pcch_msg) => pcch_msg,
_ => return None,
},
_ => return None,
};
let PCCH_MessageType::C1(PCCH_MessageType_c1::Paging(paging)) = &pcch_msg.message else {
return None;
@@ -35,9 +39,11 @@ impl Analyzer for TestAnalyzer{
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 },
event_type: EventType::QualitativeWarning {
severity: Severity::Low,
},
message: "TMSI was provided to cell".to_string(),
})
});
}
}
None

View File

@@ -1,10 +1,10 @@
use thiserror::Error;
use rayhunter::diag_device::DiagDeviceError;
use thiserror::Error;
use crate::qmdl_store::RecordingStoreError;
#[derive(Error, Debug)]
pub enum RayhunterError{
pub enum RayhunterError {
#[error("Config file parsing error: {0}")]
ConfigFileParsingError(#[from] toml::de::Error),
#[error("Diag intialization error: {0}")]

View File

@@ -1,111 +0,0 @@
use image::{codecs::gif::GifDecoder, imageops::FilterType, AnimationDecoder, DynamicImage};
use std::{io::Cursor, time::Duration};
const FB_PATH:&str = "/dev/fb0";
#[derive(Copy, Clone)]
// TODO actually poll for this, maybe w/ fbset?
struct Dimensions {
height: u32,
width: u32,
}
#[allow(dead_code)]
#[derive(Copy, Clone)]
pub enum Color565 {
Red = 0b1111100000000000,
Green = 0b0000011111100000,
Blue = 0b0000000000011111,
White = 0b1111111111111111,
Black = 0b0000000000000000,
Cyan = 0b0000011111111111,
Yellow = 0b1111111111100000,
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,
path: &'a str,
}
impl Framebuffer<'_>{
pub const fn new() -> Self {
Framebuffer{
dimensions: Dimensions{height: 128, width: 128},
path: FB_PATH,
}
}
fn write(&mut self, img: DynamicImage) {
let mut width = img.width();
let mut height = img.height();
let resized_img: DynamicImage;
if height > self.dimensions.height ||
width > self.dimensions.width {
resized_img = img.resize( self.dimensions.width, self.dimensions.height, FilterType::CatmullRom);
width = self.dimensions.width.min(resized_img.width());
height = self.dimensions.height.min(resized_img.height());
} else {
resized_img = img;
}
let img_rgba8 = resized_img.as_rgba8().unwrap();
let mut buf = Vec::new();
for y in 0..height {
for x in 0..width {
let px = img_rgba8.get_pixel(x, y);
let mut rgb565: u16 = (px[0] as u16 & 0b11111000) << 8;
rgb565 |= (px[1] as u16 & 0b11111100) << 3;
rgb565 |= (px[2] as u16) >> 3;
buf.extend(rgb565.to_le_bytes());
}
}
std::fs::write(self.path, &buf).unwrap();
}
pub fn draw_gif(&mut self, img_buffer: &[u8]) {
// this is dumb and i'm sure there's a better way to loop this
let cursor = Cursor::new(img_buffer);
let decoder = GifDecoder::new(cursor).unwrap();
for maybe_frame in decoder.into_frames() {
let frame = maybe_frame.unwrap();
let (numerator, _) = frame.delay().numer_denom_ms();
let img = DynamicImage::from(frame.into_buffer());
self.write(img);
std::thread::sleep(Duration::from_millis(numerator as u64));
}
}
pub fn draw_img(&mut self, img_buffer: &[u8]) {
let img = image::load_from_memory(img_buffer).unwrap();
self.write(img);
}
pub fn draw_line(&mut self, color: Color565, height: u32){
let px_num= height * self.dimensions.width;
let color: u16 = color as u16;
let mut buffer: Vec<u8> = Vec::new();
for _ in 0..px_num {
buffer.extend(color.to_le_bytes());
}
std::fs::write(self.path, &buffer).unwrap();
}
}

View File

@@ -1,36 +1,43 @@
use crate::ServerState;
use axum::body::Body;
use axum::extract::{Path, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use futures::TryStreamExt;
use log::error;
use rayhunter::diag::DataType;
use rayhunter::gsmtap_parser;
use rayhunter::pcap::GsmtapPcapWriter;
use rayhunter::qmdl::QmdlReader;
use axum::body::Body;
use axum::http::header::CONTENT_TYPE;
use axum::extract::{State, Path};
use axum::http::StatusCode;
use axum::response::{Response, IntoResponse};
use std::sync::Arc;
use std::{future, pin::pin};
use tokio::io::duplex;
use tokio_util::io::ReaderStream;
use std::{future, pin::pin};
use std::sync::Arc;
use log::error;
use futures::TryStreamExt;
// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
// written so far. This is done by spawning a thread which streams chunks of
// pcap data to a channel that's piped to the client.
pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
pub async fn get_pcap(
State(state): State<Arc<ServerState>>,
Path(qmdl_name): Path<String>,
) -> Result<Response, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
let (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)))?;
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((
StatusCode::SERVICE_UNAVAILABLE,
"QMDL file is empty, try again in a bit!".to_string()
"QMDL file is empty, try again in a bit!".to_string(),
));
}
let qmdl_size_bytes = entry.qmdl_size_bytes;
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await
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)
@@ -40,20 +47,27 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
tokio::spawn(async move {
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
let mut messages_stream = pin!(reader.as_stream()
let mut messages_stream = pin!(reader
.as_stream()
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
while let Some(container) = messages_stream.try_next().await.expect("failed getting QMDL container") {
while let Some(container) = messages_stream
.try_next()
.await
.expect("failed getting QMDL container")
{
for maybe_msg in container.into_messages() {
match maybe_msg {
Ok(msg) => {
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)
.expect("error parsing gsmtap message");
let maybe_gsmtap_msg =
gsmtap_parser::parse(msg).expect("error parsing gsmtap message");
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
pcap_writer.write_gsmtap_message(gsmtap_msg, timestamp).await
pcap_writer
.write_gsmtap_message(gsmtap_msg, timestamp)
.await
.expect("error writing pcap packet");
}
},
}
Err(e) => error!("error parsing message: {:?}", e),
}
}

View File

@@ -1,4 +1,5 @@
use chrono::{DateTime, Local};
use rayhunter::util::RuntimeMetadata;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thiserror::Error;
@@ -11,10 +12,14 @@ use tokio::{
pub enum RecordingStoreError {
#[error("Can't close an entry when there's no current entry")]
NoCurrentEntry,
#[error("An entry with that name doesn't exist")]
NoSuchEntryError,
#[error("Couldn't create file: {0}")]
CreateFileError(tokio::io::Error),
#[error("Couldn't read file: {0}")]
ReadFileError(tokio::io::Error),
#[error("Couldn't delete file: {0}")]
DeleteFileError(tokio::io::Error),
#[error("Couldn't open directory at path: {0}")]
OpenDirError(tokio::io::Error),
#[error("Couldn't read manifest file: {0}")]
@@ -43,17 +48,24 @@ pub struct ManifestEntry {
pub last_message_time: Option<DateTime<Local>>,
pub qmdl_size_bytes: usize,
pub analysis_size_bytes: usize,
pub rayhunter_version: Option<String>,
pub system_os: Option<String>,
pub arch: Option<String>,
}
impl ManifestEntry {
fn new() -> Self {
let now = Local::now();
let metadata = RuntimeMetadata::new();
ManifestEntry {
name: format!("{}", now.timestamp()),
start_time: now,
last_message_time: None,
qmdl_size_bytes: 0,
analysis_size_bytes: 0,
rayhunter_version: Some(metadata.rayhunter_version),
system_os: Some(metadata.system_os),
arch: Some(metadata.arch),
}
}
@@ -108,23 +120,20 @@ impl RecordingStore {
where
P: AsRef<Path>,
{
let manifest_path = path.as_ref().join("manifest.toml");
fs::create_dir_all(&path)
.await
.map_err(RecordingStoreError::OpenDirError)?;
let mut manifest_file = File::create(&manifest_path)
.await
.map_err(RecordingStoreError::WriteManifestError)?;
let empty_manifest = Manifest {
entries: Vec::new(),
let mut store = RecordingStore {
path: path.as_ref().to_owned(),
manifest: Manifest {
entries: Vec::new(),
},
current_entry: None,
};
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
store.write_manifest().await?;
Ok(store)
}
async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError>
@@ -148,17 +157,11 @@ impl RecordingStore {
}
let new_entry = ManifestEntry::new();
let qmdl_filepath = new_entry.get_qmdl_filepath(&self.path);
let qmdl_file = File::options()
.create(true)
.write(true)
.open(&qmdl_filepath)
let qmdl_file = File::create(&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)
let analysis_file = File::create(&analysis_filepath)
.await
.map_err(RecordingStoreError::CreateFileError)?;
self.manifest.entries.push(new_entry);
@@ -168,10 +171,7 @@ impl RecordingStore {
}
// Returns the corresponding QMDL file for a given entry
pub async fn open_entry_qmdl(
&self,
entry_index: usize,
) -> Result<File, RecordingStoreError> {
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
@@ -200,8 +200,7 @@ impl RecordingStore {
.open(entry.get_analysis_filepath(&self.path))
.await
.map_err(RecordingStoreError::ReadFileError)?;
self.update_entry_analysis_size(entry_index, 0)
.await?;
self.update_entry_analysis_size(entry_index, 0).await?;
Ok(file)
}
@@ -238,23 +237,29 @@ impl RecordingStore {
}
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
let mut manifest_file = File::options()
.write(true)
.open(self.path.join("manifest.toml"))
let tmp_path = self.path.join("manifest.toml.new");
let mut manifest_tmp_file = File::create(&tmp_path)
.await
.map_err(RecordingStoreError::WriteManifestError)?;
let manifest_contents =
toml::to_string_pretty(&self.manifest).expect("failed to serialize manifest");
manifest_file
manifest_tmp_file
.write_all(manifest_contents.as_bytes())
.await
.map_err(RecordingStoreError::WriteManifestError)?;
fs::rename(tmp_path, self.path.join("manifest.toml"))
.await
.map_err(RecordingStoreError::WriteManifestError)?;
Ok(())
}
// Finds an entry by filename
pub fn entry_for_name(&self, name: &str) -> Option<(usize, &ManifestEntry)> {
let entry_index = self.manifest
let entry_index = self
.manifest
.entries
.iter()
.position(|entry| entry.name == name)?;
@@ -265,6 +270,53 @@ impl RecordingStore {
let entry_index = self.current_entry?;
Some((entry_index, &self.manifest.entries[entry_index]))
}
pub async fn delete_entry(&mut self, name: &str) -> Result<ManifestEntry, RecordingStoreError> {
let entry_to_delete_idx = self
.manifest
.entries
.iter()
.position(|entry| entry.name == name)
.ok_or(RecordingStoreError::NoSuchEntryError)?;
if let Some(current_entry) = self.current_entry {
if current_entry == entry_to_delete_idx {
self.close_current_entry().await?;
} else {
self.current_entry = Some(current_entry - 1);
}
}
let entry_to_delete = self.manifest.entries.remove(entry_to_delete_idx);
self.write_manifest().await?;
let qmdl_filepath = entry_to_delete.get_qmdl_filepath(&self.path);
let analysis_filepath = entry_to_delete.get_analysis_filepath(&self.path);
tokio::fs::remove_file(qmdl_filepath)
.await
.map_err(RecordingStoreError::DeleteFileError)?;
tokio::fs::remove_file(analysis_filepath)
.await
.map_err(RecordingStoreError::DeleteFileError)?;
Ok(entry_to_delete)
}
pub async fn delete_all_entries(&mut self) -> Result<(), RecordingStoreError> {
if self.current_entry.is_some() {
self.close_current_entry().await?;
}
for entry in &self.manifest.entries {
let qmdl_filepath = entry.get_qmdl_filepath(&self.path);
let analysis_filepath = entry.get_analysis_filepath(&self.path);
tokio::fs::remove_file(qmdl_filepath)
.await
.map_err(RecordingStoreError::DeleteFileError)?;
tokio::fs::remove_file(analysis_filepath)
.await
.map_err(RecordingStoreError::DeleteFileError)?;
}
self.manifest.entries.drain(..);
self.write_manifest().await?;
Ok(())
}
}
#[cfg(test)]
@@ -321,6 +373,20 @@ mod tests {
));
}
#[tokio::test]
async fn test_create_on_existing_store() {
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();
store
.update_entry_qmdl_size(entry_index, 1000)
.await
.unwrap();
let store = RecordingStore::create(dir.path()).await.unwrap();
assert_eq!(store.manifest.entries.len(), 0);
}
#[tokio::test]
async fn test_repeated_new_entries() {
let dir = make_temp_dir();
@@ -332,4 +398,20 @@ mod tests {
assert_ne!(entry_index, new_entry_index);
assert_eq!(store.manifest.entries.len(), 2);
}
#[tokio::test]
async fn test_delete_all_entries() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
assert!(store.current_entry.is_some());
store.delete_all_entries().await.unwrap();
assert!(store.current_entry.is_none());
// regression test: deleting all entries should also work when there's no current
// recording.
store.delete_all_entries().await.unwrap();
assert!(store.current_entry.is_none());
}
}

View File

@@ -1,41 +1,53 @@
use axum::body::Body;
use axum::http::header::{CONTENT_TYPE, self};
use axum::extract::State;
use axum::http::{StatusCode, HeaderValue};
use axum::response::{Response, IntoResponse};
use axum::extract::Path;
use axum::extract::State;
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use include_dir::{include_dir, Dir};
use std::sync::Arc;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_util::io::ReaderStream;
use include_dir::{include_dir, Dir};
use crate::{framebuffer, DiagDeviceCtrlMessage};
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
use crate::qmdl_store::RecordingStore;
use crate::{display, DiagDeviceCtrlMessage};
pub struct ServerState {
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
pub ui_update_sender: Sender<framebuffer::DisplayState>,
pub ui_update_sender: Sender<display::DisplayState>,
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
pub analysis_sender: Sender<AnalysisCtrlMessage>,
pub debug_mode: bool,
pub colorblind_mode: bool,
}
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
pub async fn get_qmdl(
State(state): State<Arc<ServerState>>,
Path(qmdl_name): Path<String>,
) -> Result<Response, (StatusCode, String)> {
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
let qmdl_store = state.qmdl_store_lock.read().await;
let (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)))?;
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 (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);
let headers = [(CONTENT_TYPE, "application/octet-stream")];
let headers = [
(CONTENT_TYPE, "application/octet-stream"),
(CONTENT_LENGTH, &entry.qmdl_size_bytes.to_string()),
];
let body = Body::from_stream(qmdl_stream);
Ok((headers, body).into_response())
}
@@ -43,7 +55,10 @@ 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(State(state): State<Arc<ServerState>>, 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();
@@ -59,7 +74,9 @@ pub async fn serve_static(State(state): State<Arc<ServerState>>, Path(path): Pat
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");
file.read_to_string(&mut body)
.await
.expect("failed to read file");
Response::builder()
.status(StatusCode::OK)
.header(
@@ -68,11 +85,11 @@ pub async fn serve_static(State(state): State<Arc<ServerState>>, Path(path): Pat
)
.body(Body::from(body))
.unwrap()
},
}
Err(_) => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())
.unwrap()
.unwrap(),
};
}

View File

@@ -3,9 +3,9 @@ use std::sync::Arc;
use crate::qmdl_store::ManifestEntry;
use crate::server::ServerState;
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use axum::Json;
use log::error;
use serde::Serialize;
use tokio::process::Command;
@@ -65,10 +65,16 @@ pub struct MemoryStats {
// runs the given command and returns its stdout as a string
async fn get_cmd_output(mut cmd: Command) -> Result<String, String> {
let cmd_str = format!("{:?}", &cmd);
let output = cmd.output().await
let output = cmd
.output()
.await
.map_err(|e| format!("error running command {}: {}", &cmd_str, e))?;
if !output.status.success() {
return Err(format!("command {} failed with exit code {}", &cmd_str, output.status.code().unwrap()));
return Err(format!(
"command {} failed with exit code {}",
&cmd_str,
output.status.code().unwrap()
));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
@@ -79,7 +85,8 @@ impl MemoryStats {
let mut free_cmd = Command::new("free");
free_cmd.arg("-k");
let stdout = get_cmd_output(free_cmd).await?;
let mut numbers = stdout.split_whitespace()
let mut numbers = stdout
.split_whitespace()
.flat_map(|part| part.parse::<usize>());
Ok(Self {
total: humanize_kb(numbers.next().ok_or("error parsing free output")?),
@@ -91,13 +98,15 @@ impl MemoryStats {
// turns a number of kilobytes (like 28293) into a human-readable string (like "28.3M")
fn humanize_kb(kb: usize) -> String {
if kb < 1000{
if kb < 1000 {
return format!("{}K", kb);
}
format!("{:.1}M", kb as f64 / 1024.0)
}
pub async fn get_system_stats(State(state): State<Arc<ServerState>>) -> Result<Json<SystemStats>, (StatusCode, String)> {
pub async fn get_system_stats(
State(state): State<Arc<ServerState>>,
) -> Result<Json<SystemStats>, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
match SystemStats::new(qmdl_store.path.to_str().unwrap()).await {
Ok(stats) => Ok(Json(stats)),
@@ -105,9 +114,9 @@ pub async fn get_system_stats(State(state): State<Arc<ServerState>>) -> Result<J
error!("error getting system stats: {}", err);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
"error getting system stats".to_string()
"error getting system stats".to_string(),
))
},
}
}
}
@@ -117,7 +126,9 @@ pub struct ManifestStats {
pub current_entry: Option<ManifestEntry>,
}
pub async fn get_qmdl_manifest(State(state): State<Arc<ServerState>>) -> Result<Json<ManifestStats>, (StatusCode, String)> {
pub async fn get_qmdl_manifest(
State(state): State<Arc<ServerState>>,
) -> Result<Json<ManifestStats>, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
let mut entries = qmdl_store.manifest.entries.clone();
let current_entry = qmdl_store.current_entry.map(|index| entries.remove(index));

View File

@@ -1,3 +1,4 @@
<!DOCTYPE html>
<html>
<head>
<title>rayhunter</title>
@@ -17,6 +18,7 @@
<div>
<button onclick="startRecording()">Start Recording</button>
<button onclick="stopRecording()">Stop Recording</button>
<button onclick="deleteAllRecodings()">Delete All Recordings</button>
</div>
<table id="qmdl-manifest-table">
<thead>
@@ -28,6 +30,7 @@
<th scope="col">PCAP</th>
<th scope="col">QMDL</th>
<th scope="col">Analysis Result</th>
<th scope="col">Actions</th>
</tr>
</thead>
</table>

View File

@@ -80,6 +80,13 @@ async function updateEntryAnalysisResult(entry) {
entry.analysis_result = `0 warnings!`;
} else {
entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`;
for (const warning of entry.analysis.warnings) {
for (const event of warning.warning.events) {
if (event === null) continue;
msg = `${warning.timestamp}: ${event.message}`
entry.analysis_result += `<br>${msg}`
}
}
}
}
@@ -118,6 +125,16 @@ function createLink(uri, text) {
return link;
}
function createButton(uri, text) {
const link = document.createElement('button');
link.innerText = text;
link.onclick = async () => {
await req('POST', uri);
populateDivs();
};
return link;
}
function createEntryRow(entry, isCurrent) {
const row = document.createElement('tr');
const name = document.createElement('th');
@@ -136,16 +153,20 @@ function createEntryRow(entry, isCurrent) {
row.appendChild(pcapTd);
const qmdlTd = document.createElement('td');
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}`, 'qmdl'));
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}.qmdl`, 'qmdl'));
row.appendChild(qmdlTd);
const analysisResult = document.createElement('td');
analysisResult.innerText = entry.analysis_result;
analysisResult.innerHTML = entry.analysis_result;
if (entry.analysis.warnings.length > 0) {
row.classList.add("warning");
}
row.appendChild(analysisResult);
const actionsButtons = document.createElement('td');
actionsButtons.appendChild(createButton(`/api/delete-recording/${entry.name}`, 'Delete'));
row.appendChild(actionsButtons);
return row;
}
@@ -163,26 +184,27 @@ 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";
} else {
manifest.current_entry.last_message_time = new Date(manifest.current_entry.last_message_time);
}
parseQmdlEntry(manifest.current_entry);
}
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);
parseQmdlEntry(entry);
}
// sort them in reverse chronological order
manifest.entries.reverse();
return manifest;
}
function parseQmdlEntry(entry) {
entry.status = STATUS_NEEDS_UPDATE;
entry.analysis_result = 'Waiting...';
entry.start_time = new Date(entry.start_time);
if (entry.last_message_time === null) {
entry.last_message_time = "N/A";
} else {
entry.last_message_time = new Date(entry.last_message_time);
}
}
async function startRecording() {
await req('POST', '/api/start-recording');
populateDivs();
@@ -193,6 +215,13 @@ async function stopRecording() {
populateDivs();
}
async function deleteAllRecodings() {
if (window.confirm("Are you sure you want to permanently delete all of your recordings?")) {
await req('POST', '/api/delete-all-recordings');
populateDivs();
}
}
async function req(method, url) {
const response = await fetch(url, {
method: method,

View File

@@ -5,8 +5,14 @@ debug_mode = false
enable_dummy_analyzer = false
colorblind_mode = false
# UI Levels:
#
# Orbic and TP-Link with color display:
# 0 = invisible mode, no indicator that rayhunter is running
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
# 1 = Subtle mode, display a colored line at the top of the screen when rayhunter is running (green=running, white=paused, red=warnings)
# 2 = Demo Mode, display a fun orca gif
# 3 = display the EFF logo
#
# TP-Link with one-bit display:
# 0 = invisible mode
# 1..3 = show emoji for status. :) for running, :( for warnings, no mouth for paused.
ui_level = 1

17
dist/install-linux.sh vendored
View File

@@ -1,17 +0,0 @@
#!/bin/env bash
set -e
if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ] ; then
echo "adb not found, downloading local copy"
curl -O "https://dl.google.com/android/repository/platform-tools-latest-linux.zip"
unzip platform-tools-latest-linux.zip
fi
export ADB="./platform-tools/adb"
else
export ADB=`which adb`
fi
export SERIAL_PATH="./serial-ubuntu-latest/serial"
. "$(dirname "$0")"/install-common.sh
install

17
dist/install-mac.sh vendored
View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -e
if ! command -v adb &> /dev/null; then
if [ ! -d ./platform-tools ]; then
echo "adb not found, downloading local copy"
curl -O "https://dl.google.com/android/repository/platform-tools-latest-darwin.zip"
unzip platform-tools-latest-darwin.zip
fi
export ADB="./platform-tools/adb"
else
export ADB=`which adb`
fi
export SERIAL_PATH="./serial-macos-latest/serial"
. "$(dirname "$0")"/install-common.sh
install

View File

@@ -1,18 +1,5 @@
#!/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?"
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
force_debug_mode
setup_rootshell
setup_rayhunter
test_rayhunter
}
set -e
force_debug_mode() {
echo "Using adb at $ADB"
@@ -67,7 +54,7 @@ setup_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
_adb_push rayhunter-daemon-orbic/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"
@@ -108,3 +95,48 @@ test_rayhunter() {
done
echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :("
}
##### ##### #####
##### Main #####
##### ##### #####
if [[ `uname -s` == "Linux" ]]; then
if [[ `uname -m` == "arm64" ]]; then
export SERIAL_PATH="./serial-ubuntu-24-aarch64/serial"
elif [[ `uname -m` == "x86_64" ]]; then
export SERIAL_PATH="./serial-ubuntu-24/serial"
fi
export PLATFORM_TOOLS="platform-tools-latest-linux.zip"
elif [[ `uname -s` == "Darwin" ]]; then
if [[ `uname -m` == "arm64" ]]; then
export SERIAL_PATH="./serial-macos-arm/serial"
elif [[ `uname -m` == "x86_64" ]]; then
export SERIAL_PATH="./serial-macos-intel/serial"
fi
export PLATFORM_TOOLS="platform-tools-latest-darwin.zip"
# if we've already deleted this attribute, xattr errors out
xattr -d com.apple.quarantine "$SERIAL_PATH" || echo
else
echo "This script only supports Linux or macOS"
exit 1
fi
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}"
unzip $PLATFORM_TOOLS
fi
export ADB="./platform-tools/adb"
else
export ADB=`which adb`
fi
force_debug_mode
setup_rootshell
setup_rayhunter
test_rayhunter

View File

@@ -1,4 +1,4 @@
#! /bin/bash
#! /bin/sh
set -e
@@ -6,7 +6,7 @@ case "$1" in
start)
echo -n "Starting rayhunter: "
start-stop-daemon -S -b --make-pidfile --pidfile /tmp/rayhunter.pid \
--startas /bin/bash -- -c "RUST_LOG=info exec /data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml > /data/rayhunter/rayhunter.log 2>&1"
--startas /bin/sh -- -c "RUST_LOG=info exec /data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml > /data/rayhunter/rayhunter.log 2>&1"
echo "done"
;;
stop)

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter"
version = "0.1.0"
version = "0.2.8"
edition = "2021"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
@@ -9,6 +9,11 @@ description = "Realtime cellular data decoding and analysis for IMSI catcher det
name = "rayhunter"
path = "src/lib.rs"
[features]
default = []
orbic = []
tplink = []
[dependencies]
bytes = "1.5.0"
chrono = "0.4.31"
@@ -17,10 +22,11 @@ deku = { version = "0.16.0", features = ["logging"] }
env_logger = "0.10.1"
libc = "0.2.150"
log = "0.4.20"
nix = { version = "0.29.0", features = ["feature"] }
pcap-file-tokio = "0.1.0"
thiserror = "1.0.50"
telcom-parser = { path = "../telcom-parser" }
tokio = { version = "1.35.1", features = ["full"] }
tokio = { version = "1.44.2", features = ["full"] }
futures-core = "0.3.30"
futures = "0.3.30"
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -1,10 +1,15 @@
use std::borrow::Cow;
use chrono::{DateTime, FixedOffset};
use serde::Serialize;
use std::borrow::Cow;
use crate::util::RuntimeMetadata;
use crate::{diag::MessagesContainer, gsmtap_parser};
use super::{/*imsi_provided::ImsiProvidedAnalyzer,*/ information_element::InformationElement, lte_downgrade::LteSib6And7DowngradeAnalyzer, null_cipher::NullCipherAnalyzer};
use super::{
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement,
priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
};
/// Qualitative measure of how severe a Warning event type is.
/// The levels should break down like this:
@@ -18,7 +23,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")]
@@ -67,6 +72,7 @@ pub struct AnalyzerMetadata {
#[derive(Serialize, Debug)]
pub struct ReportMetadata {
pub analyzers: Vec<AnalyzerMetadata>,
pub rayhunter: RuntimeMetadata,
}
#[derive(Serialize, Debug, Clone)]
@@ -89,11 +95,9 @@ impl AnalysisRow {
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;
}
for event in analysis.events.iter().flatten() {
if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
return true;
}
}
}
@@ -105,16 +109,29 @@ pub struct Harness {
analyzers: Vec<Box<dyn Analyzer + Send>>,
}
impl Default for Harness {
fn default() -> Self {
Self::new()
}
}
impl Harness {
pub fn new() -> Self {
Self { analyzers: Vec::new() }
Self {
analyzers: Vec::new(),
}
}
pub fn new_with_all_analyzers() -> Self {
let mut harness = Harness::new();
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
//harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
// FIXME: our RRC parser is reporting false positives for this due to an
// upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133).
// once that's fixed, we should regenerate our parser and re-enable this
// harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
harness
}
@@ -170,19 +187,22 @@ impl Harness {
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
self.analyzers.iter_mut()
self.analyzers
.iter_mut()
.map(|analyzer| analyzer.analyze_information_element(ie))
.collect()
}
pub fn get_names(&self) -> Vec<Cow<'_, str>> {
self.analyzers.iter()
self.analyzers
.iter()
.map(|analyzer| analyzer.get_name())
.collect()
}
pub fn get_descriptions(&self) -> Vec<Cow<'_, str>> {
self.analyzers.iter()
self.analyzers
.iter()
.map(|analyzer| analyzer.get_description())
.collect()
}
@@ -198,8 +218,11 @@ impl Harness {
});
}
let rayhunter = RuntimeMetadata::new();
ReportMetadata {
analyzers,
rayhunter,
}
}
}

View File

@@ -0,0 +1,49 @@
use std::borrow::Cow;
use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement};
use super::util::unpack;
use telcom_parser::lte_rrc::{
DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions,
RRCConnectionReleaseCriticalExtensions_c1, RedirectedCarrierInfo,
};
// 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);
let message = match &**lte_ie {
LteInformationElement::DlDcch(msg_cont) => &msg_cont.message,
_ => return None,
};
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: "Detected 2G downgrade".to_owned(),
}),
_ => Some(Event {
event_type: EventType::Informational,
message: format!("RRCConnectionRelease CarrierInfo: {:?}", carrier_info),
}),
}
}
}

View File

@@ -5,8 +5,7 @@ use telcom_parser::lte_rrc::{PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Ide
use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement};
pub struct ImsiProvidedAnalyzer {
}
pub struct ImsiProvidedAnalyzer {}
impl Analyzer for ImsiProvidedAnalyzer {
fn get_name(&self) -> Cow<str> {
@@ -18,8 +17,12 @@ impl Analyzer for ImsiProvidedAnalyzer {
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
let InformationElement::LTE(LteInformationElement::PCCH(pcch_msg)) = ie else {
return None;
let pcch_msg = match ie {
InformationElement::LTE(lte_ie) => match &**lte_ie {
LteInformationElement::PCCH(pcch_msg) => pcch_msg,
_ => return None,
},
_ => return None,
};
let PCCH_MessageType::C1(PCCH_MessageType_c1::Paging(paging)) = &pcch_msg.message else {
return None;
@@ -27,9 +30,11 @@ impl Analyzer for ImsiProvidedAnalyzer {
for record in &paging.paging_record_list.as_ref()?.0 {
if let PagingUE_Identity::Imsi(_) = record.ue_identity {
return Some(Event {
event_type: EventType::QualitativeWarning { severity: Severity::High },
event_type: EventType::QualitativeWarning {
severity: Severity::High,
},
message: "IMSI was provided to cell".to_string(),
})
});
}
}
None

View File

@@ -0,0 +1,69 @@
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 Default for ImsiRequestedAnalyzer {
fn default() -> Self {
Self::new()
}
}
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 payload = match ie {
InformationElement::LTE(inner) => match &**inner {
LteInformationElement::NAS(payload) => payload,
_ => return None,
},
_ => 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: "NAS IMSI identity request detected".to_owned(),
});
}
}
None
}
}

View File

@@ -3,9 +3,9 @@
//! the term to refer to a structured, fully parsed message in any telcom
//! standard.
use crate::gsmtap::{GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
use telcom_parser::{decode, lte_rrc};
use thiserror::Error;
use crate::gsmtap::{GsmtapType, LteRrcSubtype, GsmtapMessage};
#[derive(Error, Debug)]
pub enum InformationElementError {
@@ -19,14 +19,18 @@ pub enum InformationElementError {
pub enum InformationElement {
GSM,
UMTS,
LTE(LteInformationElement),
// This element of the enum is substantially larger than the others,
// so we box it to prevent the size of the enum (any variant) from blowing up.
LTE(Box<LteInformationElement>),
FiveG,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LteInformationElement {
DlCcch(lte_rrc::DL_CCCH_Message),
DlDcch(lte_rrc::DL_DCCH_Message),
// This element of the enum is substantially larger than the others,
// so we box it to prevent the size of the enum (any variant) from blowing up.
DlDcch(Box<lte_rrc::DL_DCCH_Message>),
UlCcch(lte_rrc::UL_CCCH_Message),
UlDcch(lte_rrc::UL_DCCH_Message),
BcchBch(lte_rrc::BCCH_BCH_Message),
@@ -40,6 +44,8 @@ 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(),
@@ -58,11 +64,11 @@ impl TryFrom<&GsmtapMessage> for InformationElement {
fn try_from(gsmtap_msg: &GsmtapMessage) -> Result<Self, Self::Error> {
match gsmtap_msg.header.gsmtap_type {
GsmtapType::LteRrc(lte_rrc_subtype) => {
use LteRrcSubtype as L;
use LteInformationElement as R;
use LteRrcSubtype as L;
let lte = match lte_rrc_subtype {
L::DlCcch => R::DlCcch(decode(&gsmtap_msg.payload)?),
L::DlDcch => R::DlDcch(decode(&gsmtap_msg.payload)?),
L::DlDcch => R::DlDcch(Box::new(decode(&gsmtap_msg.payload)?)),
L::UlCcch => R::UlCcch(decode(&gsmtap_msg.payload)?),
L::UlDcch => R::UlDcch(decode(&gsmtap_msg.payload)?),
L::BcchBch => R::BcchBch(decode(&gsmtap_msg.payload)?),
@@ -75,11 +81,20 @@ impl TryFrom<&GsmtapMessage> for InformationElement {
L::BcchDlSchMbms => R::BcchDlSchMbms(decode(&gsmtap_msg.payload)?),
L::SbcchSlBch => R::SbcchSlBch(decode(&gsmtap_msg.payload)?),
L::SbcchSlBchV2x => R::SbcchSlBchV2x(decode(&gsmtap_msg.payload)?),
_ => return Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)),
_ => {
return Err(InformationElementError::UnsupportedGsmtapType(
gsmtap_msg.header.gsmtap_type,
))
}
};
Ok(InformationElement::LTE(lte))
},
_ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)),
Ok(InformationElement::LTE(Box::new(lte)))
}
GsmtapType::LteNas(LteNasSubtype::Plain) => Ok(InformationElement::LTE(Box::new(
LteInformationElement::NAS(gsmtap_msg.payload.clone()),
))),
_ => Err(InformationElementError::UnsupportedGsmtapType(
gsmtap_msg.header.gsmtap_type,
)),
}
}
}

View File

@@ -1,5 +1,8 @@
pub mod analyzer;
pub mod information_element;
pub mod lte_downgrade;
pub mod connection_redirect_downgrade;
pub mod imsi_provided;
pub mod imsi_requested;
pub mod information_element;
pub mod null_cipher;
pub mod priority_2g_downgrade;
pub mod util;

View File

@@ -1,25 +1,41 @@
use std::borrow::Cow;
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 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};
pub struct NullCipherAnalyzer {
}
pub struct NullCipherAnalyzer {}
impl NullCipherAnalyzer {
fn check_rrc_connection_reconfiguration_cipher(&self, reconfiguration: &RRCConnectionReconfiguration) -> bool {
let RRCConnectionReconfigurationCriticalExtensions::C1(c1) = &reconfiguration.critical_extensions else {
fn check_rrc_connection_reconfiguration_cipher(
&self,
reconfiguration: &RRCConnectionReconfiguration,
) -> bool {
let RRCConnectionReconfigurationCriticalExtensions::C1(c1) =
&reconfiguration.critical_extensions
else {
return false;
};
let RRCConnectionReconfigurationCriticalExtensions_c1::RrcConnectionReconfiguration_r8(c1) = c1 else {
let RRCConnectionReconfigurationCriticalExtensions_c1::RrcConnectionReconfiguration_r8(c1) =
c1
else {
return false;
};
if let Some(handover) = &c1.security_config_ho {
let maybe_security_config = match &handover.handover_type {
telcom_parser::lte_rrc::SecurityConfigHOHandoverType::IntraLTE(lte) => lte.security_algorithm_config.as_ref(),
telcom_parser::lte_rrc::SecurityConfigHOHandoverType::InterRAT(rat) => Some(&rat.security_algorithm_config),
telcom_parser::lte_rrc::SecurityConfigHOHandoverType::IntraLTE(lte) => {
lte.security_algorithm_config.as_ref()
}
telcom_parser::lte_rrc::SecurityConfigHOHandoverType::InterRAT(rat) => {
Some(&rat.security_algorithm_config)
}
};
if let Some(security_config) = maybe_security_config {
if security_config.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 {
@@ -28,19 +44,24 @@ impl NullCipherAnalyzer {
}
}
// Use map/flatten to dig into a long chain of nested Option types
let maybe_v1250 = c1.non_critical_extension.as_ref()
.map(|v890| v890.non_critical_extension.as_ref()).flatten()
.map(|v920| v920.non_critical_extension.as_ref()).flatten()
.map(|v1020| v1020.non_critical_extension.as_ref()).flatten()
.map(|v1130| v1130.non_critical_extension.as_ref()).flatten();
let maybe_v1250 = c1
.non_critical_extension
.as_ref()
.and_then(|v890| v890.non_critical_extension.as_ref())
.and_then(|v920| v920.non_critical_extension.as_ref())
.and_then(|v1020| v1020.non_critical_extension.as_ref())
.and_then(|v1130| v1130.non_critical_extension.as_ref());
let Some(v1250) = maybe_v1250 else {
return false;
};
if let Some(SCG_Configuration_r12::Setup(scg_setup)) = v1250.scg_configuration_r12.as_ref() {
let maybe_cipher = scg_setup.scg_config_part_scg_r12.as_ref()
.map(|scg| scg.mobility_control_info_scg_r12.as_ref()).flatten()
.map(|mci| mci.ciphering_algorithm_scg_r12.as_ref()).flatten();
if let Some(SCG_Configuration_r12::Setup(scg_setup)) = v1250.scg_configuration_r12.as_ref()
{
let maybe_cipher = scg_setup
.scg_config_part_scg_r12
.as_ref()
.and_then(|scg| scg.mobility_control_info_scg_r12.as_ref())
.and_then(|mci| mci.ciphering_algorithm_scg_r12.as_ref());
if let Some(cipher) = maybe_cipher {
if cipher.0 == CipheringAlgorithm_r12::EEA0 {
return true;
@@ -48,18 +69,26 @@ impl NullCipherAnalyzer {
}
}
let maybe_v1530_security_config = v1250.non_critical_extension.as_ref()
.map(|v1310| v1310.non_critical_extension.as_ref()).flatten()
.map(|v1430| v1430.non_critical_extension.as_ref()).flatten()
.map(|v1510| v1510.non_critical_extension.as_ref()).flatten()
.map(|v1530| v1530.security_config_ho_v1530.as_ref()).flatten();
let maybe_v1530_security_config = v1250
.non_critical_extension
.as_ref()
.and_then(|v1310| v1310.non_critical_extension.as_ref())
.and_then(|v1430| v1430.non_critical_extension.as_ref())
.and_then(|v1510| v1510.non_critical_extension.as_ref())
.and_then(|v1530| v1530.security_config_ho_v1530.as_ref());
let Some(v1530_security_config) = maybe_v1530_security_config else {
return false;
};
let maybe_security_algorithm = match &v1530_security_config.handover_type_v1530 {
SecurityConfigHO_v1530HandoverType_v1530::Intra5GC(intra_5gc) => intra_5gc.security_algorithm_config_r15.as_ref(),
SecurityConfigHO_v1530HandoverType_v1530::Fivegc_ToEPC(to_epc) => Some(&to_epc.security_algorithm_config_r15),
SecurityConfigHO_v1530HandoverType_v1530::Epc_To5GC(to_5gc) => Some(&to_5gc.security_algorithm_config_r15),
SecurityConfigHO_v1530HandoverType_v1530::Intra5GC(intra_5gc) => {
intra_5gc.security_algorithm_config_r15.as_ref()
}
SecurityConfigHO_v1530HandoverType_v1530::Fivegc_ToEPC(to_epc) => {
Some(&to_epc.security_algorithm_config_r15)
}
SecurityConfigHO_v1530HandoverType_v1530::Epc_To5GC(to_5gc) => {
Some(&to_5gc.security_algorithm_config_r15)
}
};
if let Some(security_algorithm) = maybe_security_algorithm {
if security_algorithm.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 {
@@ -76,7 +105,13 @@ impl NullCipherAnalyzer {
let SecurityModeCommandCriticalExtensions_c1::SecurityModeCommand_r8(r8) = &c1 else {
return false;
};
if r8.security_config_smc.security_algorithm_config.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 {
if r8
.security_config_smc
.security_algorithm_config
.ciphering_algorithm
.0
== CipheringAlgorithm_r12::EEA0
{
return true;
}
false
@@ -93,20 +128,30 @@ impl Analyzer for NullCipherAnalyzer {
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
let InformationElement::LTE(LteInformationElement::DlDcch(dcch_msg)) = ie else {
return None;
let dcch_msg = match ie {
InformationElement::LTE(lte_ie) => match &**lte_ie {
LteInformationElement::DlDcch(dcch_msg) => dcch_msg,
_ => return None,
},
_ => return None,
};
let DL_DCCH_MessageType::C1(c1) = &dcch_msg.message else {
return None;
};
let null_cipher_detected = match c1 {
DL_DCCH_MessageType_c1::RrcConnectionReconfiguration(reconfiguration) => self.check_rrc_connection_reconfiguration_cipher(reconfiguration),
DL_DCCH_MessageType_c1::SecurityModeCommand(command) => self.check_security_mode_command_cipher(command),
DL_DCCH_MessageType_c1::RrcConnectionReconfiguration(reconfiguration) => {
self.check_rrc_connection_reconfiguration_cipher(reconfiguration)
}
DL_DCCH_MessageType_c1::SecurityModeCommand(command) => {
self.check_security_mode_command_cipher(command)
}
_ => return None,
};
if null_cipher_detected {
return Some(Event {
event_type: EventType::QualitativeWarning { severity: Severity::High },
event_type: EventType::QualitativeWarning {
severity: Severity::High,
},
message: "Cell suggested use of null cipher".to_string(),
});
}

View File

@@ -2,18 +2,31 @@ use std::borrow::Cow;
use super::analyzer::{Analyzer, Event, EventType, Severity};
use super::information_element::{InformationElement, LteInformationElement};
use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1, CellReselectionPriority, SystemInformationBlockType7, SystemInformationCriticalExtensions, SystemInformation_r8_IEsSib_TypeAndInfo, SystemInformation_r8_IEsSib_TypeAndInfo_Entry};
use telcom_parser::lte_rrc::{
BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1, CellReselectionPriority,
SystemInformationBlockType7, SystemInformationCriticalExtensions,
SystemInformation_r8_IEsSib_TypeAndInfo, SystemInformation_r8_IEsSib_TypeAndInfo_Entry,
};
/// Based on heuristic T7 from Shinjo Park's "Why We Cannot Win".
pub struct LteSib6And7DowngradeAnalyzer {
}
pub struct LteSib6And7DowngradeAnalyzer {}
impl LteSib6And7DowngradeAnalyzer {
fn unpack_system_information<'a>(&self, ie: &'a InformationElement) -> Option<&'a SystemInformation_r8_IEsSib_TypeAndInfo> {
if let InformationElement::LTE(LteInformationElement::BcchDlSch(bcch_dl_sch_message)) = ie {
if let BCCH_DL_SCH_MessageType::C1(BCCH_DL_SCH_MessageType_c1::SystemInformation(system_information)) = &bcch_dl_sch_message.message {
if let SystemInformationCriticalExtensions::SystemInformation_r8(sib) = &system_information.critical_extensions {
return Some(&sib.sib_type_and_info);
fn unpack_system_information<'a>(
&self,
ie: &'a InformationElement,
) -> Option<&'a SystemInformation_r8_IEsSib_TypeAndInfo> {
if let InformationElement::LTE(lte_ie) = ie {
if let LteInformationElement::BcchDlSch(bcch_dl_sch_message) = &**lte_ie {
if let BCCH_DL_SCH_MessageType::C1(BCCH_DL_SCH_MessageType_c1::SystemInformation(
system_information,
)) = &bcch_dl_sch_message.message
{
if let SystemInformationCriticalExtensions::SystemInformation_r8(sib) =
&system_information.critical_extensions
{
return Some(&sib.sib_type_and_info);
}
}
}
}
@@ -31,14 +44,19 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
Cow::from("Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.")
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<super::analyzer::Event> {
fn analyze_information_element(
&mut self,
ie: &InformationElement,
) -> Option<super::analyzer::Event> {
let sibs = &self.unpack_system_information(ie)?.0;
for sib in sibs {
match sib {
SystemInformation_r8_IEsSib_TypeAndInfo_Entry::Sib6(sib6) => {
if let Some(carrier_info_list) = sib6.carrier_freq_list_utra_fdd.as_ref() {
for carrier_info in &carrier_info_list.0 {
if let Some(CellReselectionPriority(p)) = carrier_info.cell_reselection_priority {
if let Some(CellReselectionPriority(p)) =
carrier_info.cell_reselection_priority
{
if p == 0 {
return Some(Event {
event_type: EventType::QualitativeWarning { severity: Severity::High },
@@ -50,7 +68,9 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
}
if let Some(carrier_info_list) = sib6.carrier_freq_list_utra_tdd.as_ref() {
for carrier_info in &carrier_info_list.0 {
if let Some(CellReselectionPriority(p)) = carrier_info.cell_reselection_priority {
if let Some(CellReselectionPriority(p)) =
carrier_info.cell_reselection_priority
{
if p == 0 {
return Some(Event {
event_type: EventType::QualitativeWarning { severity: Severity::High },
@@ -60,20 +80,31 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
}
}
}
},
SystemInformation_r8_IEsSib_TypeAndInfo_Entry::Sib7(SystemInformationBlockType7 { carrier_freqs_info_list: Some(carrier_info_list), .. }) => {
}
SystemInformation_r8_IEsSib_TypeAndInfo_Entry::Sib7(
SystemInformationBlockType7 {
carrier_freqs_info_list: Some(carrier_info_list),
..
},
) => {
for carrier_info in &carrier_info_list.0 {
if let Some(CellReselectionPriority(p)) = carrier_info.common_info.cell_reselection_priority {
if let Some(CellReselectionPriority(p)) =
carrier_info.common_info.cell_reselection_priority
{
if p == 0 {
return Some(Event {
event_type: EventType::QualitativeWarning { severity: Severity::High },
message: "LTE cell advertised a 2G cell for priority 0 reselection".to_string(),
event_type: EventType::QualitativeWarning {
severity: Severity::High,
},
message:
"LTE cell advertised a 2G cell for priority 0 reselection"
.to_string(),
});
}
}
}
},
_ => {},
}
_ => {}
}
}
None

33
lib/src/analysis/util.rs Normal file
View File

@@ -0,0 +1,33 @@
// 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;

View File

@@ -5,7 +5,7 @@ use crc::{Algorithm, Crc};
use deku::prelude::*;
use crate::hdlc::{self, hdlc_decapsulate};
use log::{warn, error};
use log::{error, warn};
use thiserror::Error;
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
@@ -42,7 +42,7 @@ pub enum LogConfigRequest {
log_type: u32,
log_mask_bitsize: u32,
log_mask: Vec<u8>,
}
},
}
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
@@ -93,13 +93,19 @@ impl MessagesContainer {
Ok(data) => match Message::from_bytes((&data, 0)) {
Ok(((leftover_bytes, _), res)) => {
if !leftover_bytes.is_empty() {
warn!("warning: {} leftover bytes when parsing Message", leftover_bytes.len());
warn!(
"warning: {} leftover bytes when parsing Message",
leftover_bytes.len()
);
}
result.push(Ok(res));
},
}
Err(e) => result.push(Err(DiagParsingError::MessageParsingError(e, data))),
},
Err(err) => result.push(Err(DiagParsingError::HdlcDecapsulationError(err, sub_msg.to_vec()))),
Err(err) => result.push(Err(DiagParsingError::HdlcDecapsulationError(
err,
sub_msg.to_vec(),
))),
}
}
}
@@ -171,7 +177,7 @@ pub enum LogBody {
msg: Vec<u8>,
},
#[deku(id = "0xb0c0")]
LteRrcOtaMessage{
LteRrcOtaMessage {
ext_header_version: u8,
#[deku(ctx = "*ext_header_version")]
packet: LteRrcOtaPacket,
@@ -183,6 +189,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,
@@ -208,7 +216,20 @@ pub enum LogBody {
NrRrcOtaMessage {
#[deku(count = "hdr_len")]
msg: Vec<u8>,
}
},
}
#[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)]
@@ -349,15 +370,17 @@ pub enum ResponsePayload {
#[deku(ctx = "subopcode: u32", id = "subopcode")]
pub enum LogConfigResponse {
#[deku(id = "1")]
RetrieveIdRanges {
log_mask_sizes: [u32; 16],
},
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
#[deku(id = "3")]
SetMask,
}
pub fn build_log_mask_request(log_type: u32, log_mask_bitsize: u32, accepted_log_codes: &[u32]) -> Request {
pub fn build_log_mask_request(
log_type: u32,
log_mask_bitsize: u32,
accepted_log_codes: &[u32],
) -> Request {
let mut current_byte: u8 = 0;
let mut num_bits_written: u8 = 0;
let mut log_mask: Vec<u8> = vec![];
@@ -398,31 +421,35 @@ mod test {
log_mask_bitsize: 0,
log_mask: vec![],
});
assert_eq!(req.to_bytes().unwrap(), vec![
115, 0, 0, 0,
3, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
]);
assert_eq!(
req.to_bytes().unwrap(),
vec![115, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,]
);
}
#[test]
fn test_build_log_mask_request() {
let log_type = 11;
let bitsize = 513;
let req = build_log_mask_request(log_type, bitsize, &crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING);
assert_eq!(req, Request::LogConfig(LogConfigRequest::SetMask {
log_type: log_type,
log_mask_bitsize: bitsize,
log_mask: vec![
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0,
0x0, 0x0, 0xc, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0,
],
}));
let req = build_log_mask_request(
log_type,
bitsize,
&crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING,
);
assert_eq!(
req,
Request::LogConfig(LogConfigRequest::SetMask {
log_type,
log_mask_bitsize: bitsize,
log_mask: vec![
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xc, 0x30, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0,
],
})
);
}
#[test]
@@ -433,53 +460,53 @@ mod test {
mdm_field: -1,
hdlc_encapsulated_request: vec![1, 2, 3, 4],
};
assert_eq!(req.to_bytes().unwrap(), vec![
32, 0, 0, 0,
1, 2, 3, 4,
]);
assert_eq!(req.to_bytes().unwrap(), vec![32, 0, 0, 0, 1, 2, 3, 4,]);
let req = RequestContainer {
data_type: DataType::UserSpace,
use_mdm: true,
mdm_field: -1,
hdlc_encapsulated_request: vec![1, 2, 3, 4],
};
assert_eq!(req.to_bytes().unwrap(), vec![
32, 0, 0, 0,
255, 255, 255, 255,
1, 2, 3, 4,
]);
assert_eq!(
req.to_bytes().unwrap(),
vec![32, 0, 0, 0, 255, 255, 255, 255, 1, 2, 3, 4,]
);
}
#[test]
fn test_logs() {
let data = vec![
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20,
14, 48, 0, 160, 0, 2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1,
238, 173, 213, 77, 208
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0, 160, 0,
2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1, 238, 173, 213, 77, 208,
];
let msg = Message::from_bytes((&data, 0)).unwrap().1;
assert_eq!(msg, Message::Log {
pending_msgs: 0,
outer_length: 38,
inner_length: 38,
log_type: 0xb0c0,
timestamp: Timestamp { ts: 72659535985485082 },
body: LogBody::LteRrcOtaMessage {
ext_header_version: 20,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 48,
bearer_id: 0,
phy_cell_id: 160,
earfcn: 2050,
sfn_subfn: 4057,
pdu_num: 5,
sib_mask: 0,
len: 7,
packet: vec![0x40, 0x1, 0xee, 0xad, 0xd5, 0x4d, 0xd0],
assert_eq!(
msg,
Message::Log {
pending_msgs: 0,
outer_length: 38,
inner_length: 38,
log_type: 0xb0c0,
timestamp: Timestamp {
ts: 72659535985485082
},
},
});
body: LogBody::LteRrcOtaMessage {
ext_header_version: 20,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 48,
bearer_id: 0,
phy_cell_id: 160,
earfcn: 2050,
sfn_subfn: 4057,
pdu_num: 5,
sib_mask: 0,
len: 7,
packet: vec![0x40, 0x1, 0xee, 0xad, 0xd5, 0x4d, 0xd0],
},
},
}
);
}
fn make_container(data_type: DataType, message: HdlcEncapsulatedMessage) -> MessagesContainer {
@@ -500,7 +527,9 @@ mod test {
outer_length: length_with_payload,
inner_length: length_with_payload,
log_type: 0xb0c0,
timestamp: Timestamp { ts: 72659535985485082 },
timestamp: Timestamp {
ts: 72659535985485082,
},
body: LogBody::LteRrcOtaMessage {
ext_header_version: 20,
packet: LteRrcOtaPacket::V8 {
@@ -517,7 +546,9 @@ mod test {
},
},
};
let serialized = message.to_bytes().expect("failed to serialize test message");
let serialized = message
.to_bytes()
.expect("failed to serialize test message");
let encapsulated_data = hdlc::hdlc_encapsulate(&serialized, &CRC_CCITT);
let encapsulated = HdlcEncapsulatedMessage {
len: encapsulated_data.len() as u32,
@@ -559,7 +590,10 @@ mod test {
container.num_messages += 1;
let result = container.into_messages();
assert_eq!(result[0], Ok(message1));
assert!(matches!(result[1], Err(DiagParsingError::MessageParsingError(_, _))));
assert!(matches!(
result[1],
Err(DiagParsingError::MessageParsingError(_, _))
));
}
#[test]
@@ -574,6 +608,9 @@ mod test {
container.num_messages += 1;
let result = container.into_messages();
assert_eq!(result[0], Ok(message1));
assert!(matches!(result[1], Err(DiagParsingError::HdlcDecapsulationError(_, _))));
assert!(matches!(
result[1],
Err(DiagParsingError::HdlcDecapsulationError(_, _))
));
}
}

View File

@@ -1,13 +1,16 @@
use crate::diag::{
build_log_mask_request, DataType, DiagParsingError, LogConfigRequest, LogConfigResponse,
Message, MessagesContainer, Request, RequestContainer, ResponsePayload, CRC_CCITT,
};
use crate::hdlc::hdlc_encapsulate;
use crate::diag::{build_log_mask_request, DataType, DiagParsingError, LogConfigRequest, LogConfigResponse, Message, MessagesContainer, Request, RequestContainer, ResponsePayload, CRC_CCITT};
use crate::log_codes;
use deku::prelude::*;
use futures_core::TryStream;
use log::{error, info};
use std::io::ErrorKind;
use std::os::fd::AsRawFd;
use futures_core::TryStream;
use thiserror::Error;
use log::{info, warn, error};
use deku::prelude::*;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -38,26 +41,23 @@ pub enum DiagDeviceError {
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
// Layer 2:
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
// Layer 3:
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C, // 0x512f
log_codes::WCDMA_SIGNALLING_MESSAGE, // 0x412f
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C, // 0xb0c0
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C, // 0xb821
log_codes::WCDMA_SIGNALLING_MESSAGE, // 0x412f
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C, // 0xb0c0
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C, // 0xb821
// NAS:
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C, // 0x713a
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C, // 0xb0e2
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C, // 0xb0e3
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C, // 0xb0ec
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
log_codes::LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C, // 0xb0e2
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C, // 0xb0e3
log_codes::LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C, // 0xb0ec
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
// User IP traffic:
log_codes::LOG_DATA_PROTOCOL_LOGGING_C // 0x11eb
log_codes::LOG_DATA_PROTOCOL_LOGGING_C, // 0x11eb
];
const BUFFER_LEN: usize = 1024 * 1024 * 10;
const MEMORY_DEVICE_MODE: i32 = 2;
const MEMORY_DEVICE_MODE: u32 = 2;
#[cfg(target_arch = "arm")]
const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
@@ -68,9 +68,9 @@ 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")]
#[cfg(target_arch = "aarch64")]
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
pub struct DiagDevice {
@@ -99,7 +99,9 @@ impl DiagDevice {
})
}
pub fn as_stream(&mut self) -> impl TryStream<Ok = MessagesContainer, Error = DiagDeviceError> + '_ {
pub fn as_stream(
&mut self,
) -> impl TryStream<Ok = MessagesContainer, Error = DiagDeviceError> + '_ {
futures::stream::try_unfold(self, |dev| async {
let container = dev.get_next_messages_container().await?;
Ok(Some((container, dev)))
@@ -108,16 +110,25 @@ impl DiagDevice {
async fn get_next_messages_container(&mut self) -> Result<MessagesContainer, DiagDeviceError> {
let mut bytes_read = 0;
while bytes_read == 0 {
bytes_read = self.file.read(&mut self.read_buf).await
// TP-Link M7350 sometimes sends too small messages, we need to be able to deal with short reads.
while bytes_read <= 8 {
bytes_read = self
.file
.read(&mut self.read_buf)
.await
.map_err(DiagDeviceError::DeviceReadFailed)?;
}
let ((leftover_bytes, _), container) = MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0))
.map_err(DiagDeviceError::ParseMessagesContainerError)?;
if !leftover_bytes.is_empty() {
warn!("warning: {} leftover bytes when parsing MessagesContainer", leftover_bytes.len());
info!(
"Parsing messages container size = {:?} [{:?}]",
bytes_read,
&self.read_buf[0..bytes_read]
);
match MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0)) {
Ok((_, container)) => return Ok(container),
Err(err) => return Err(DiagDeviceError::ParseMessagesContainerError(err)),
}
Ok(container)
}
async fn write_request(&mut self, req: &Request) -> DiagResult<()> {
@@ -127,7 +138,9 @@ impl DiagDevice {
use_mdm: self.use_mdm > 0,
mdm_field: -1,
hdlc_encapsulated_request: hdlc_encapsulate(req_bytes, &CRC_CCITT),
}.to_bytes().expect("Failed to serialize RequestContainer");
}
.to_bytes()
.expect("Failed to serialize RequestContainer");
if let Err(err) = self.file.write(&buf).await {
// For reasons I don't entirely understand, calls to write(2) on
// /dev/diag always return 0 bytes written, though the written
@@ -162,13 +175,17 @@ impl DiagDevice {
for msg in self.read_response().await? {
match msg {
Ok(Message::Log { .. }) => info!("skipping log response..."),
Ok(Message::Response { payload, status, .. }) => match payload {
ResponsePayload::LogConfig(LogConfigResponse::RetrieveIdRanges { log_mask_sizes }) => {
Ok(Message::Response {
payload, status, ..
}) => match payload {
ResponsePayload::LogConfig(LogConfigResponse::RetrieveIdRanges {
log_mask_sizes,
}) => {
if status != 0 {
return Err(DiagDeviceError::RequestFailed(status, req));
}
return Ok(log_mask_sizes);
},
}
_ => info!("skipping non-LogConfigResponse response..."),
},
Err(e) => error!("error parsing message: {:?}", e),
@@ -179,20 +196,26 @@ impl DiagDevice {
}
async fn set_log_mask(&mut self, log_type: u32, log_mask_bitsize: u32) -> DiagResult<()> {
let req = build_log_mask_request(log_type, log_mask_bitsize, &LOG_CODES_FOR_RAW_PACKET_LOGGING);
let req = build_log_mask_request(
log_type,
log_mask_bitsize,
&LOG_CODES_FOR_RAW_PACKET_LOGGING,
);
self.write_request(&req).await?;
for msg in self.read_response().await? {
match msg {
Ok(Message::Log { .. }) => info!("skipping log response..."),
Ok(Message::Response { payload, status, .. }) => {
Ok(Message::Response {
payload, status, ..
}) => {
if let ResponsePayload::LogConfig(LogConfigResponse::SetMask) = payload {
if status != 0 {
return Err(DiagDeviceError::RequestFailed(status, req));
}
return Ok(());
}
},
}
Err(e) => error!("error parsing message: {:?}", e),
}
}
@@ -215,19 +238,55 @@ impl DiagDevice {
}
}
// also found in: https://android.googlesource.com/kernel/msm.git/+/android-7.1.0_r0.3/drivers/char/diag/diagchar.h#399
//
// the code on
// https://github.com/P1sec/QCSuper/blob/master/docs/The%20Diag%20protocol.md#the-diag-protocol-over-devdiag
// is misleading, mode_param is only 8 bits. sending the larger [u32; 3] payload will cause the
// IOCTL to be rejected by TPLINK M7350 HW rev 5
//
// TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada
#[repr(C)]
struct diag_logging_mode_param_t {
req_mode: u32,
peripheral_mask: u32,
mode_param: u8,
}
// Triggers the diag device's debug logging mode
fn enable_frame_readwrite(fd: i32, mode: i32) -> DiagResult<()> {
fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> {
unsafe {
if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 {
let mut params = if cfg!(feature = "tplink") {
diag_logging_mode_param_t {
req_mode: mode,
peripheral_mask: 0,
mode_param: 1,
}
} else {
diag_logging_mode_param_t {
req_mode: mode,
peripheral_mask: u32::MAX,
mode_param: 0,
}
};
let ret = libc::ioctl(
fd,
DIAG_IOCTL_SWITCH_LOGGING,
&mut [mode, -1, 0] as *mut _, // diag_logging_mode_param_t
std::mem::size_of::<[i32; 3]>(), 0, 0, 0, 0
&mut params as *mut _,
std::mem::size_of::<diag_logging_mode_param_t>(),
0,
0,
0,
0,
);
if ret < 0 {
let msg = format!("DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}", ret);
return Err(DiagDeviceError::InitializationFailed(msg))
let msg = format!(
"DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}",
ret
);
return Err(DiagDeviceError::InitializationFailed(msg));
}
}
}
@@ -241,7 +300,7 @@ fn determine_use_mdm(fd: i32) -> DiagResult<i32> {
unsafe {
if libc::ioctl(fd, DIAG_IOCTL_REMOTE_DEV, &use_mdm as *const i32) < 0 {
let msg = format!("DIAG_IOCTL_REMOTE_DEV ioctl failed with error code {}", 0);
return Err(DiagDeviceError::InitializationFailed(msg))
return Err(DiagDeviceError::InitializationFailed(msg));
}
}
Ok(use_mdm)

View File

@@ -6,24 +6,24 @@ use deku::prelude::*;
pub enum GsmtapType {
Um(UmSubtype),
Abis,
UmBurst, /* raw burst bits */
SIM, /* ISO 7816 smart card interface */
TetraI1, /* tetra air interface */
UmBurst, /* raw burst bits */
SIM, /* ISO 7816 smart card interface */
TetraI1, /* tetra air interface */
TetraI1Burst, /* tetra air interface */
WmxBurst, /* WiMAX burst */
GbLlc, /* GPRS Gb interface: LLC */
GbSndcp, /* GPRS Gb interface: SNDCP */
Gmr1Um, /* GMR-1 L2 packets */
WmxBurst, /* WiMAX burst */
GbLlc, /* GPRS Gb interface: LLC */
GbSndcp, /* GPRS Gb interface: SNDCP */
Gmr1Um, /* GMR-1 L2 packets */
UmtsRlcMac,
UmtsRrc(UmtsRrcSubtype),
LteRrc(LteRrcSubtype), /* LTE interface */
LteMac, /* LTE MAC interface */
LteMacFramed, /* LTE MAC with context hdr */
OsmocoreLog, /* libosmocore logging */
QcDiag, /* Qualcomm DIAG frame */
LteMac, /* LTE MAC interface */
LteMacFramed, /* LTE MAC with context hdr */
OsmocoreLog, /* libosmocore logging */
QcDiag, /* Qualcomm DIAG frame */
LteNas(LteNasSubtype), /* LTE Non-Access Stratum */
E1T1, /* E1/T1 Lines */
GsmRlp, /* GSM RLP frames as per 3GPP TS 24.022 */
E1T1, /* E1/T1 Lines */
GsmRlp, /* GSM RLP frames as per 3GPP TS 24.022 */
}
// based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/src/scat/parsers/qualcomm/diagltelogparser.py#L1337
@@ -119,7 +119,7 @@ pub enum UmtsRrcSubtype {
SysInfoTypeSB1 = 58,
SysInfoTypeSB2 = 59,
ToTargetRNCContainer = 60,
TargetRNCToSourceRNCContainer = 61
TargetRNCToSourceRNCContainer = 61,
}
#[repr(u8)]
@@ -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,
@@ -213,15 +218,15 @@ pub struct GsmtapHeader {
}
impl GsmtapHeader {
pub fn new(
gsmtap_type: GsmtapType,
) -> Self {
pub fn new(gsmtap_type: GsmtapType) -> Self {
GsmtapHeader {
gsmtap_type,
version: 2,
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,

View File

@@ -13,7 +13,10 @@ pub enum GsmtapParserError {
}
pub fn parse(msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, GsmtapParserError> {
if let Message::Log { timestamp, body, .. } = msg {
if let Message::Log {
timestamp, body, ..
} = msg
{
match log_to_gsmtap(body)? {
Some(msg) => Ok(Some((timestamp, msg))),
None => Ok(None),
@@ -25,9 +28,13 @@ pub fn parse(msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, GsmtapP
fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserError> {
match value {
LogBody::LteRrcOtaMessage { ext_header_version, packet } => {
LogBody::LteRrcOtaMessage {
ext_header_version,
packet,
} => {
let gsmtap_type = match ext_header_version {
0x02 | 0x03 | 0x04 | 0x06 | 0x07 | 0x08 | 0x0d | 0x16 => match packet.get_pdu_num() {
0x02 | 0x03 | 0x04 | 0x06 | 0x07 | 0x08 | 0x0d | 0x16 => match packet.get_pdu_num()
{
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch),
3 => GsmtapType::LteRrc(LteRrcSubtype::MCCH),
@@ -36,7 +43,12 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
6 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
7 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
8 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch),
pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)),
pdu => {
return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(
ext_header_version,
pdu,
))
}
},
0x09 | 0x0c => match packet.get_pdu_num() {
8 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
@@ -47,7 +59,12 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
13 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
14 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
15 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch),
pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)),
pdu => {
return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(
ext_header_version,
pdu,
))
}
},
0x0e..=0x10 => match packet.get_pdu_num() {
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
@@ -58,7 +75,12 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
7 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
8 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
9 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch),
pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)),
pdu => {
return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(
ext_header_version,
pdu,
))
}
},
0x13 | 0x1a | 0x1b => match packet.get_pdu_num() {
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
@@ -76,8 +98,13 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
49 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb),
50 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb),
52 => GsmtapType::LteRrc(LteRrcSubtype::UlDcchNb),
pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)),
}
pdu => {
return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(
ext_header_version,
pdu,
))
}
},
0x14 | 0x18 | 0x19 => match packet.get_pdu_num() {
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch),
@@ -94,12 +121,20 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
58 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb),
59 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb),
61 => GsmtapType::LteRrc(LteRrcSubtype::UlDcchNb),
pdu => return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(ext_header_version, pdu)),
pdu => {
return Err(GsmtapParserError::InvalidLteRrcOtaHeaderPduNum(
ext_header_version,
pdu,
))
}
},
_ => return Err(GsmtapParserError::InvalidLteRrcOtaExtHeaderVersion(ext_header_version)),
_ => {
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();
@@ -107,18 +142,19 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
header,
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,
}))
},
}
_ => {
error!("gsmtap_sink: ignoring unhandled log type: {:?}", value);
Ok(None)
},
}
}
}

View File

@@ -3,11 +3,14 @@
//! here:
//! https://github.com/P1sec/QCSuper/blob/master/docs/The%20Diag%20protocol.md#the-diag-protocol-over-usb
use crc::Crc;
use bytes::Buf;
use crc::Crc;
use thiserror::Error;
use crate::diag::{MESSAGE_ESCAPE_CHAR, MESSAGE_TERMINATOR, ESCAPED_MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR};
use crate::diag::{
ESCAPED_MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR, MESSAGE_ESCAPE_CHAR,
MESSAGE_TERMINATOR,
};
#[derive(Debug, Clone, Error, PartialEq)]
pub enum HdlcError {
@@ -29,7 +32,9 @@ pub fn hdlc_encapsulate(data: &[u8], crc: &Crc<u16>) -> Vec<u8> {
for &b in data {
match b {
MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]),
MESSAGE_ESCAPE_CHAR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR]),
MESSAGE_ESCAPE_CHAR => {
result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR])
}
_ => result.push(b),
}
}
@@ -37,7 +42,9 @@ pub fn hdlc_encapsulate(data: &[u8], crc: &Crc<u16>) -> Vec<u8> {
for b in crc.checksum(data).to_le_bytes() {
match b {
MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]),
MESSAGE_ESCAPE_CHAR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR]),
MESSAGE_ESCAPE_CHAR => {
result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR])
}
_ => result.push(b),
}
}
@@ -77,7 +84,10 @@ pub fn hdlc_decapsulate(data: &[u8], crc: &Crc<u16>) -> Result<Vec<u8>, HdlcErro
let checksum_lo = unescaped.pop().ok_or(HdlcError::MissingChecksum)?;
let checksum = [checksum_lo, checksum_hi].as_slice().get_u16_le();
if checksum != crc.checksum(&unescaped) {
return Err(HdlcError::InvalidChecksum(checksum, crc.checksum(&unescaped)));
return Err(HdlcError::InvalidChecksum(
checksum,
crc.checksum(&unescaped),
));
}
Ok(unescaped)

View File

@@ -1,12 +1,16 @@
pub mod hdlc;
pub mod analysis;
pub mod diag;
pub mod diag_device;
pub mod qmdl;
pub mod log_codes;
pub mod gsmtap;
pub mod gsmtap_parser;
pub mod hdlc;
pub mod log_codes;
pub mod pcap;
pub mod analysis;
pub mod qmdl;
pub mod util;
// bin/check.rs may target windows and does not use this mod
#[cfg(target_family = "unix")]
pub mod diag_device;
// re-export telcom_parser, since we use its types in our API
pub use telcom_parser;

View File

@@ -1,6 +1,5 @@
//! Enumerates some relevant diag log codes. Copied from QCSuper
// These are 2G-related log types.
pub const LOG_GSM_RR_SIGNALING_MESSAGE_C: u32 = 0x512f;
@@ -95,12 +94,10 @@ pub const RRCLOG_SIG_DL_MSCH: u32 = 8;
pub const RRCLOG_EXTENSION_SIB: u32 = 9;
pub const RRCLOG_SIB_CONTAINER: u32 = 10;
// 3G layer 3 packets:
pub const WCDMA_SIGNALLING_MESSAGE: u32 = 0x412f;
// Upper layers
pub const LOG_DATA_PROTOCOL_LOGGING_C: u32 = 0x11eb;

View File

@@ -1,17 +1,18 @@
//! Parse QMDL files and create a pcap file.
//! Creates a plausible IP header and [GSMtap](https://osmocom.org/projects/baseband/wiki/GSMTAP) header and then puts the rest of the data under that for wireshark to parse.
use crate::gsmtap::GsmtapMessage;
//! Parse QMDL files and create a pcap file.
//! Creates a plausible IP header and [GSMtap](https://osmocom.org/projects/baseband/wiki/GSMTAP) header and then puts the rest of the data under that for wireshark to parse.
use crate::diag::Timestamp;
use crate::gsmtap::GsmtapMessage;
use tokio::io::AsyncWrite;
use std::borrow::Cow;
use chrono::prelude::*;
use deku::prelude::*;
use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
use pcap_file_tokio::pcapng::blocks::interface_description::InterfaceDescriptionBlock;
use pcap_file_tokio::pcapng::blocks::section_header::{SectionHeaderBlock, SectionHeaderOption};
use pcap_file_tokio::pcapng::PcapNgWriter;
use pcap_file_tokio::PcapError;
use pcap_file_tokio::{Endianness, PcapError};
use std::borrow::Cow;
use thiserror::Error;
use tokio::io::AsyncWrite;
#[derive(Error, Debug)]
pub enum GsmtapPcapError {
@@ -25,7 +26,10 @@ pub enum GsmtapPcapError {
Deku(#[from] DekuError),
}
pub struct GsmtapPcapWriter<T> where T: AsyncWrite {
pub struct GsmtapPcapWriter<T>
where
T: AsyncWrite,
{
writer: PcapNgWriter<T>,
ip_id: u16,
}
@@ -58,9 +62,29 @@ struct UdpHeader {
checksum: u16,
}
impl<T> GsmtapPcapWriter<T> where T: AsyncWrite + Unpin + Send {
impl<T> GsmtapPcapWriter<T>
where
T: AsyncWrite + Unpin + Send,
{
pub async fn new(writer: T) -> Result<Self, GsmtapPcapError> {
let writer = PcapNgWriter::new(writer).await?;
let metadata = crate::util::RuntimeMetadata::new();
let package = format!(
"{} {}",
env!("CARGO_PKG_NAME").to_owned(),
metadata.rayhunter_version
);
let section = SectionHeaderBlock {
endianness: Endianness::Big,
major_version: 1,
minor_version: 0,
section_length: -1,
options: vec![
SectionHeaderOption::Hardware(Cow::from(metadata.arch)),
SectionHeaderOption::OS(Cow::from(metadata.system_os)),
SectionHeaderOption::UserApplication(Cow::from(package)),
],
};
let writer = PcapNgWriter::with_section_header(writer, section).await?;
Ok(GsmtapPcapWriter { writer, ip_id: 0 })
}
@@ -74,8 +98,13 @@ impl<T> GsmtapPcapWriter<T> where T: AsyncWrite + Unpin + Send {
Ok(())
}
pub async fn write_gsmtap_message(&mut self, msg: GsmtapMessage, timestamp: Timestamp) -> Result<(), GsmtapPcapError> {
let duration = timestamp.to_datetime()
pub async fn write_gsmtap_message(
&mut self,
msg: GsmtapMessage,
timestamp: Timestamp,
) -> Result<(), GsmtapPcapError> {
let duration = timestamp
.to_datetime()
.signed_duration_since(DateTime::UNIX_EPOCH)
.to_std()?;

View File

@@ -3,18 +3,24 @@
//! QmdlReader and QmdlWriter can read and write MessagesContainers to and from
//! QMDL files.
use crate::diag::{MessagesContainer, MESSAGE_TERMINATOR, HdlcEncapsulatedMessage, DataType};
use crate::diag::{DataType, HdlcEncapsulatedMessage, MessagesContainer, MESSAGE_TERMINATOR};
use futures::TryStream;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, AsyncBufReadExt};
use log::error;
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
pub struct QmdlWriter<T> where T: AsyncWrite + Unpin {
pub struct QmdlWriter<T>
where
T: AsyncWrite + Unpin,
{
writer: T,
pub total_written: usize,
}
impl<T> QmdlWriter<T> where T: AsyncWrite + Unpin {
impl<T> QmdlWriter<T>
where
T: AsyncWrite + Unpin,
{
pub fn new(writer: T) -> Self {
QmdlWriter::new_with_existing_size(writer, 0)
}
@@ -35,13 +41,19 @@ impl<T> QmdlWriter<T> where T: AsyncWrite + Unpin {
}
}
pub struct QmdlReader<T> where T: AsyncRead {
pub struct QmdlReader<T>
where
T: AsyncRead,
{
reader: BufReader<T>,
bytes_read: usize,
max_bytes: Option<usize>,
}
impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
impl<T> QmdlReader<T>
where
T: AsyncRead + Unpin,
{
pub fn new(reader: T, max_bytes: Option<usize>) -> Self {
QmdlReader {
reader: BufReader::new(reader),
@@ -50,21 +62,28 @@ impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
}
}
pub fn as_stream(&mut self) -> impl TryStream<Ok = MessagesContainer, Error = std::io::Error> + '_ {
pub fn as_stream(
&mut self,
) -> impl TryStream<Ok = MessagesContainer, Error = std::io::Error> + '_ {
futures::stream::try_unfold(self, |reader| async {
let maybe_container = reader.get_next_messages_container().await?;
match maybe_container {
Some(container) => Ok(Some((container, reader))),
None => Ok(None)
None => Ok(None),
}
})
}
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 {
error!("warning: {} bytes read, but max_bytes was {}", self.bytes_read, max_bytes);
error!(
"warning: {} bytes read, but max_bytes was {}",
self.bytes_read, max_bytes
);
}
return Ok(None);
}
@@ -82,12 +101,10 @@ impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
Ok(Some(MessagesContainer {
data_type: DataType::UserSpace,
num_messages: 1,
messages: vec![
HdlcEncapsulatedMessage {
len: bytes_read as u32,
data: buf,
},
]
messages: vec![HdlcEncapsulatedMessage {
len: bytes_read as u32,
data: buf,
}],
}))
}
}
@@ -96,26 +113,29 @@ impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
mod test {
use std::io::Cursor;
use crate::hdlc::hdlc_encapsulate;
use crate::diag::CRC_CCITT;
use crate::hdlc::hdlc_encapsulate;
use super::*;
fn get_test_messages() -> Vec<HdlcEncapsulatedMessage> {
let messages: Vec<HdlcEncapsulatedMessage> = (10..20).map(|i| {
let data = hdlc_encapsulate(&vec![i as u8; i], &CRC_CCITT);
HdlcEncapsulatedMessage {
len: data.len() as u32,
data,
}
}).collect();
let messages: Vec<HdlcEncapsulatedMessage> = (10..20)
.map(|i| {
let data = hdlc_encapsulate(&vec![i as u8; i], &CRC_CCITT);
HdlcEncapsulatedMessage {
len: data.len() as u32,
data,
}
})
.collect();
messages
}
// returns a byte array consisting of concatenated HDLC encapsulated
// test messages
fn get_test_message_bytes() -> Vec<u8> {
get_test_messages().iter()
get_test_messages()
.iter()
.flat_map(|msg| msg.data.clone())
.collect()
}
@@ -132,7 +152,7 @@ mod test {
MessagesContainer {
data_type: DataType::UserSpace,
num_messages: messages2.len() as u32,
messages: messages2.to_vec()
messages: messages2.to_vec(),
},
]
}
@@ -148,7 +168,10 @@ mod test {
num_messages: 1,
messages: vec![message],
};
assert_eq!(expected_container, reader.get_next_messages_container().await.unwrap().unwrap());
assert_eq!(
expected_container,
reader.get_next_messages_container().await.unwrap().unwrap()
);
}
}
@@ -167,9 +190,15 @@ mod test {
num_messages: 1,
messages: vec![message],
};
assert_eq!(expected_container, reader.get_next_messages_container().await.unwrap().unwrap());
assert_eq!(
expected_container,
reader.get_next_messages_container().await.unwrap().unwrap()
);
}
assert!(matches!(reader.get_next_messages_container().await, Ok(None)));
assert!(matches!(
reader.get_next_messages_container().await,
Ok(None)
));
}
#[tokio::test]
@@ -202,8 +231,14 @@ mod test {
num_messages: 1,
messages: vec![message],
};
assert_eq!(expected_container, reader.get_next_messages_container().await.unwrap().unwrap());
assert_eq!(
expected_container,
reader.get_next_messages_container().await.unwrap().unwrap()
);
}
assert!(matches!(reader.get_next_messages_container().await, Ok(None)));
assert!(matches!(
reader.get_next_messages_container().await,
Ok(None)
));
}
}

51
lib/src/util.rs Normal file
View File

@@ -0,0 +1,51 @@
use serde::Serialize;
#[cfg(target_family = "unix")]
use nix::sys::utsname::uname;
/// Expose binary and system information.
#[derive(Serialize, Debug)]
pub struct RuntimeMetadata {
/// The cargo package version from this library's cargo.toml, e.g., "1.2.3".
pub rayhunter_version: String,
/// The operating system `sysname` and optionally `release`. e.g., "Linux 3.18.48" or "linux".
pub system_os: String,
/// The CPU architecture in use. e.g., "armv7l" or "arm".
pub arch: String,
}
impl Default for RuntimeMetadata {
fn default() -> Self {
Self::new()
}
}
impl RuntimeMetadata {
/// Return the binary and system information, attempting to retrieve
/// attributes from `uname(2)` and falling back to values from
/// `std::env::consts`.
pub fn new() -> Self {
let build_target = RuntimeMetadata {
rayhunter_version: env!("CARGO_PKG_VERSION").to_owned(),
arch: std::env::consts::ARCH.to_string(),
system_os: std::env::consts::OS.to_string(),
};
#[cfg(target_family = "windows")]
return build_target;
#[cfg(target_family = "unix")]
match uname() {
Ok(utsname) => RuntimeMetadata {
rayhunter_version: env!("CARGO_PKG_VERSION").to_owned(),
arch: format!("{}", utsname.machine().to_string_lossy()),
system_os: format!(
"{} {}",
utsname.sysname().to_string_lossy(),
utsname.release().to_string_lossy(),
),
},
Err(_) => build_target,
}
}
}

View File

@@ -1,42 +1,46 @@
use rayhunter::{diag::{
LogBody, LteRrcOtaPacket, Message, Timestamp
}, gsmtap_parser};
use deku::prelude::*;
use rayhunter::{
diag::{LogBody, LteRrcOtaPacket, Message, Timestamp},
gsmtap_parser,
};
// Tests here are based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/tests/test_diagltelogparser.py
#[test]
fn test_lte_rrc_ota() {
let v26_binary = &[
0x10, 0x0, 0x23, 0x0, 0x23, 0x0, 0xc0, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x1a, 0xf, 0x40, 0xf, 0x40, 0x1, 0xe, 0x1, 0x13, 0x7,
0x0, 0x0, 0x0, 0x0, 0xb, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x10, 0x15
0x10, 0x0, 0x23, 0x0, 0x23, 0x0, 0xc0, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a,
0xf, 0x40, 0xf, 0x40, 0x1, 0xe, 0x1, 0x13, 0x7, 0x0, 0x0, 0x0, 0x0, 0xb, 0x0, 0x0, 0x0,
0x0, 0x2, 0x0, 0x10, 0x15,
];
let (_, parsed) = Message::from_bytes((v26_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 0x23,
inner_length: 0x23,
timestamp: Timestamp { ts: 0 },
log_type: 0xb0c0,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 26,
packet: LteRrcOtaPacket::V25 {
rrc_rel_maj: 15,
rrc_rel_min: 64,
nr_rrc_rel_maj: 15,
nr_rrc_rel_min: 64,
bearer_id: 1,
phy_cell_id: 270,
earfcn: 1811,
sfn_subfn: 0,
pdu_num: 11,
sib_mask: 0,
len: 2,
packet: vec![0x10, 0x15],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 0x23,
inner_length: 0x23,
timestamp: Timestamp { ts: 0 },
log_type: 0xb0c0,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 26,
packet: LteRrcOtaPacket::V25 {
rrc_rel_maj: 15,
rrc_rel_min: 64,
nr_rrc_rel_maj: 15,
nr_rrc_rel_min: 64,
bearer_id: 1,
phy_cell_id: 270,
earfcn: 1811,
sfn_subfn: 0,
pdu_num: 11,
sib_mask: 0,
len: 2,
packet: vec![0x10, 0x15],
}
}
}
});
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[0x10, 0x15]);
assert_eq!(gsmtap_msg.header.packet_type, 13);
@@ -49,41 +53,40 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 0);
let v26_binary = &[
0x10, 0x00, 0x23, 0x00, 0x23, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1a, 0x0f, 0x40, 0x0f, 0x40, 0x01, 0x0e, 0x01,
0x13, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00,
0x00, 0x00, 0x00, 0x02, 0x00, 0x10, 0x15,
0x10, 0x00, 0x23, 0x00, 0x23, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x1a, 0x0f, 0x40, 0x0f, 0x40, 0x01, 0x0e, 0x01, 0x13, 0x07, 0x00, 0x00, 0x00, 0x00,
0x0b, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x10, 0x15,
];
let (_, parsed) = Message::from_bytes((v26_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 35,
inner_length: 35,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 26,
packet: LteRrcOtaPacket::V25 {
rrc_rel_maj: 15,
rrc_rel_min: 64,
nr_rrc_rel_maj: 15,
nr_rrc_rel_min: 64,
bearer_id: 1,
phy_cell_id: 270,
earfcn: 1811,
sfn_subfn: 0,
pdu_num: 11,
sib_mask: 0,
len: 2,
packet: vec![0x10, 0x15],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 35,
inner_length: 35,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 26,
packet: LteRrcOtaPacket::V25 {
rrc_rel_maj: 15,
rrc_rel_min: 64,
nr_rrc_rel_maj: 15,
nr_rrc_rel_min: 64,
bearer_id: 1,
phy_cell_id: 270,
earfcn: 1811,
sfn_subfn: 0,
pdu_num: 11,
sib_mask: 0,
len: 2,
packet: vec![0x10, 0x15],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[
0x10, 0x15,
]);
assert_eq!(&gsmtap_msg.payload, &[0x10, 0x15,]);
assert_eq!(gsmtap_msg.header.packet_type, 13);
assert_eq!(gsmtap_msg.header.timeslot, 0);
assert_eq!(gsmtap_msg.header.arfcn, 1811);
@@ -94,44 +97,44 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 0);
let v24_binary = &[
0x10, 0x00, 0x2c, 0x00, 0x2c, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x18, 0x0f, 0x22, 0x00, 0x68, 0x00, 0xe4, 0x0c,
0x00, 0x00, 0x09, 0xdc, 0x05, 0x00, 0x00, 0x00,
0x00, 0x0d, 0x00, 0x40, 0x85, 0x8e, 0xc4, 0xe5,
0xbf, 0xe0, 0x50, 0xdc, 0x29, 0x15, 0x16, 0x00,
0x10, 0x00, 0x2c, 0x00, 0x2c, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x18, 0x0f, 0x22, 0x00, 0x68, 0x00, 0xe4, 0x0c, 0x00, 0x00, 0x09, 0xdc, 0x05, 0x00,
0x00, 0x00, 0x00, 0x0d, 0x00, 0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29,
0x15, 0x16, 0x00,
];
let (_, parsed) = Message::from_bytes((v24_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 44,
inner_length: 44,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 24,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 15,
rrc_rel_min: 34,
bearer_id: 0,
phy_cell_id: 104,
earfcn: 3300,
sfn_subfn: 56329,
pdu_num: 5,
sib_mask: 0,
len: 13,
packet: vec![
0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29,
0x15, 0x16, 0x0
],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 44,
inner_length: 44,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 24,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 15,
rrc_rel_min: 34,
bearer_id: 0,
phy_cell_id: 104,
earfcn: 3300,
sfn_subfn: 56329,
pdu_num: 5,
sib_mask: 0,
len: 13,
packet: vec![
0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29, 0x15, 0x16, 0x0
],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[
0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50,
0xdc, 0x29, 0x15, 0x16, 0x00,
]);
assert_eq!(
&gsmtap_msg.payload,
&[0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29, 0x15, 0x16, 0x00,]
);
assert_eq!(gsmtap_msg.header.packet_type, 13);
assert_eq!(gsmtap_msg.header.timeslot, 0);
assert_eq!(gsmtap_msg.header.arfcn, 3300);
@@ -142,48 +145,48 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 9);
let v20_binary = &[
0x10, 0x00, 0x37, 0x00, 0x37, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x14, 0x0e, 0x30, 0x01, 0x09, 0x01, 0x9c, 0x18,
0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
0x00, 0x18, 0x00, 0x08, 0x10, 0xa7, 0x14, 0x53,
0x59, 0xa6, 0x05, 0x43, 0x68, 0xc0, 0x3b, 0xda,
0x30, 0x04, 0xa6, 0x88, 0x02, 0x8d, 0xa2, 0x00,
0x9a, 0x68, 0x40,
0x10, 0x00, 0x37, 0x00, 0x37, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x14, 0x0e, 0x30, 0x01, 0x09, 0x01, 0x9c, 0x18, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00,
0x00, 0x00, 0x00, 0x18, 0x00, 0x08, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x05, 0x43, 0x68,
0xc0, 0x3b, 0xda, 0x30, 0x04, 0xa6, 0x88, 0x02, 0x8d, 0xa2, 0x00, 0x9a, 0x68, 0x40,
];
let (_, parsed) = Message::from_bytes((v20_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 55,
inner_length: 55,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 20,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 48,
bearer_id: 1,
phy_cell_id: 265,
earfcn: 6300,
sfn_subfn: 0,
pdu_num: 9,
sib_mask: 0,
len: 24,
packet: vec![
0x8, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x5, 0x43, 0x68,
0xc0, 0x3b, 0xda, 0x30, 0x4, 0xa6, 0x88, 0x2, 0x8d, 0xa2,
0x0, 0x9a, 0x68, 0x40
],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 55,
inner_length: 55,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 20,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 48,
bearer_id: 1,
phy_cell_id: 265,
earfcn: 6300,
sfn_subfn: 0,
pdu_num: 9,
sib_mask: 0,
len: 24,
packet: vec![
0x8, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x5, 0x43, 0x68, 0xc0, 0x3b, 0xda,
0x30, 0x4, 0xa6, 0x88, 0x2, 0x8d, 0xa2, 0x0, 0x9a, 0x68, 0x40
],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[
0x08, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x05,
0x43, 0x68, 0xc0, 0x3b, 0xda, 0x30, 0x04, 0xa6,
0x88, 0x02, 0x8d, 0xa2, 0x00, 0x9a, 0x68, 0x40,
]);
assert_eq!(
&gsmtap_msg.payload,
&[
0x08, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x05, 0x43, 0x68, 0xc0, 0x3b, 0xda, 0x30,
0x04, 0xa6, 0x88, 0x02, 0x8d, 0xa2, 0x00, 0x9a, 0x68, 0x40,
]
);
assert_eq!(gsmtap_msg.header.packet_type, 13);
assert_eq!(gsmtap_msg.header.timeslot, 0);
assert_eq!(gsmtap_msg.header.arfcn, 6300);
@@ -194,41 +197,41 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 0);
let v19_binary = &[
0x10, 0x00, 0x28, 0x00, 0x28, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x13, 0x0e, 0x22, 0x00, 0x0b, 0x00, 0xfa, 0x09,
0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00,
0x00, 0x09, 0x00, 0x28, 0x18, 0x40, 0x16, 0x08,
0x08, 0x80, 0x00, 0x00,
0x10, 0x00, 0x28, 0x00, 0x28, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x13, 0x0e, 0x22, 0x00, 0x0b, 0x00, 0xfa, 0x09, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00,
0x00, 0x00, 0x00, 0x09, 0x00, 0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00, 0x00,
];
let (_, parsed) = Message::from_bytes((v19_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 40,
inner_length: 40,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 19,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 34,
bearer_id: 0,
phy_cell_id: 11,
earfcn: 2554,
sfn_subfn: 0,
pdu_num: 50,
sib_mask: 0,
len: 9,
packet: vec![0x28, 0x18, 0x40, 0x16, 0x8, 0x8, 0x80, 0x0, 0x0],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 40,
inner_length: 40,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 19,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 14,
rrc_rel_min: 34,
bearer_id: 0,
phy_cell_id: 11,
earfcn: 2554,
sfn_subfn: 0,
pdu_num: 50,
sib_mask: 0,
len: 9,
packet: vec![0x28, 0x18, 0x40, 0x16, 0x8, 0x8, 0x80, 0x0, 0x0],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[
0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00,
0x00,
]);
assert_eq!(
&gsmtap_msg.payload,
&[0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00, 0x00,]
);
assert_eq!(gsmtap_msg.header.packet_type, 13);
assert_eq!(gsmtap_msg.header.timeslot, 0);
assert_eq!(gsmtap_msg.header.arfcn, 2554);
@@ -239,40 +242,41 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 0);
let v15_binary = &[
0x10, 0x00, 0x26, 0x00, 0x26, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0f, 0x0d, 0x21, 0x00, 0x9e, 0x00, 0x14, 0x05,
0x00, 0x00, 0x49, 0x8c, 0x05, 0x00, 0x00, 0x00,
0x00, 0x07, 0x00, 0x40, 0x0c, 0x8e, 0xc9, 0x42,
0x89, 0xe0,
0x10, 0x00, 0x26, 0x00, 0x26, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0f, 0x0d, 0x21, 0x00, 0x9e, 0x00, 0x14, 0x05, 0x00, 0x00, 0x49, 0x8c, 0x05, 0x00,
0x00, 0x00, 0x00, 0x07, 0x00, 0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0,
];
let (_, parsed) = Message::from_bytes((v15_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 38,
inner_length: 38,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 15,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 13,
rrc_rel_min: 33,
bearer_id: 0,
phy_cell_id: 158,
earfcn: 1300,
sfn_subfn: 35913,
pdu_num: 5,
sib_mask: 0,
len: 7,
packet: vec![0x40, 0xc, 0x8e, 0xc9, 0x42, 0x89, 0xe0],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 38,
inner_length: 38,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 15,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 13,
rrc_rel_min: 33,
bearer_id: 0,
phy_cell_id: 158,
earfcn: 1300,
sfn_subfn: 35913,
pdu_num: 5,
sib_mask: 0,
len: 7,
packet: vec![0x40, 0xc, 0x8e, 0xc9, 0x42, 0x89, 0xe0],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[
0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0,
]);
assert_eq!(
&gsmtap_msg.payload,
&[0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0,]
);
assert_eq!(gsmtap_msg.header.packet_type, 13);
assert_eq!(gsmtap_msg.header.timeslot, 0);
assert_eq!(gsmtap_msg.header.arfcn, 1300);
@@ -283,49 +287,50 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 9);
let v15_binary = &[
0x10, 0x00, 0x3b, 0x00, 0x3b, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0f, 0x0d, 0x21, 0x01, 0x9e, 0x00, 0x14, 0x05,
0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
0x00, 0x1c, 0x00, 0x08, 0x10, 0xa5, 0x34, 0x61,
0x41, 0xa3, 0x1c, 0x31, 0x68, 0x04, 0x40, 0x1a,
0x00, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f, 0x00,
0x10, 0x67, 0xc1, 0x06, 0xd9, 0xe0, 0x00,
0x10, 0x00, 0x3b, 0x00, 0x3b, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0f, 0x0d, 0x21, 0x01, 0x9e, 0x00, 0x14, 0x05, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00,
0x00, 0x00, 0x00, 0x1c, 0x00, 0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68,
0x04, 0x40, 0x1a, 0x00, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f, 0x00, 0x10, 0x67, 0xc1, 0x06,
0xd9, 0xe0, 0x00,
];
let (_, parsed) = Message::from_bytes((v15_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 59,
inner_length: 59,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 15,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 13,
rrc_rel_min: 33,
bearer_id: 1,
phy_cell_id: 158,
earfcn: 1300,
sfn_subfn: 0,
pdu_num: 9,
sib_mask: 0,
len: 28,
packet: vec![
0x8, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68,
0x4, 0x40, 0x1a, 0x0, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f,
0x0, 0x10, 0x67, 0xc1, 0x6, 0xd9, 0xe0, 0x0
],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 59,
inner_length: 59,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 15,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 13,
rrc_rel_min: 33,
bearer_id: 1,
phy_cell_id: 158,
earfcn: 1300,
sfn_subfn: 0,
pdu_num: 9,
sib_mask: 0,
len: 28,
packet: vec![
0x8, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68, 0x4, 0x40, 0x1a,
0x0, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f, 0x0, 0x10, 0x67, 0xc1, 0x6, 0xd9,
0xe0, 0x0
],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[
0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c,
0x31, 0x68, 0x04, 0x40, 0x1a, 0x00, 0x49, 0x16,
0x7c, 0x23, 0x15, 0x9f, 0x00, 0x10, 0x67, 0xc1,
0x06, 0xd9, 0xe0, 0x00,
]);
assert_eq!(
&gsmtap_msg.payload,
&[
0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68, 0x04, 0x40, 0x1a, 0x00,
0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f, 0x00, 0x10, 0x67, 0xc1, 0x06, 0xd9, 0xe0, 0x00,
]
);
assert_eq!(gsmtap_msg.header.packet_type, 13);
assert_eq!(gsmtap_msg.header.timeslot, 0);
assert_eq!(gsmtap_msg.header.arfcn, 1300);
@@ -336,35 +341,36 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 0);
let v13_binary = &[
0x10, 0x00, 0x21, 0x00, 0x21, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0d, 0x0c, 0x74, 0x01, 0x32, 0x00, 0x38, 0x18,
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x00, 0x02, 0x00, 0x2c, 0x00,
0x10, 0x00, 0x21, 0x00, 0x21, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0d, 0x0c, 0x74, 0x01, 0x32, 0x00, 0x38, 0x18, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00,
0x00, 0x00, 0x00, 0x02, 0x00, 0x2c, 0x00,
];
let (_, parsed) = Message::from_bytes((v13_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 33,
inner_length: 33,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 13,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 12,
rrc_rel_min: 116,
bearer_id: 1,
phy_cell_id: 50,
earfcn: 6200,
sfn_subfn: 0,
pdu_num: 8,
sib_mask: 0,
len: 2,
packet: vec![0x2c, 0x0],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 33,
inner_length: 33,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 13,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 12,
rrc_rel_min: 116,
bearer_id: 1,
phy_cell_id: 50,
earfcn: 6200,
sfn_subfn: 0,
pdu_num: 8,
sib_mask: 0,
len: 2,
packet: vec![0x2c, 0x0],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[0x2c, 0x00]);
assert_eq!(gsmtap_msg.header.packet_type, 13);
@@ -377,40 +383,41 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 0);
let v9_binary = &[
0x10, 0x00, 0x26, 0x00, 0x26, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x09, 0x0b, 0x70, 0x00, 0x00, 0x01, 0x14, 0x05,
0x00, 0x00, 0x09, 0x91, 0x0b, 0x00, 0x00, 0x00,
0x00, 0x07, 0x00, 0x40, 0x0b, 0x8e, 0xc1, 0xdd,
0x13, 0xb0,
0x10, 0x00, 0x26, 0x00, 0x26, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x09, 0x0b, 0x70, 0x00, 0x00, 0x01, 0x14, 0x05, 0x00, 0x00, 0x09, 0x91, 0x0b, 0x00,
0x00, 0x00, 0x00, 0x07, 0x00, 0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0,
];
let (_, parsed) = Message::from_bytes((v9_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 38,
inner_length: 38,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 9,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 11,
rrc_rel_min: 112,
bearer_id: 0,
phy_cell_id: 256,
earfcn: 1300,
sfn_subfn: 37129,
pdu_num: 11,
sib_mask: 0,
len: 7,
packet: vec![0x40, 0xb, 0x8e, 0xc1, 0xdd, 0x13, 0xb0],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 38,
inner_length: 38,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 9,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 11,
rrc_rel_min: 112,
bearer_id: 0,
phy_cell_id: 256,
earfcn: 1300,
sfn_subfn: 37129,
pdu_num: 11,
sib_mask: 0,
len: 7,
packet: vec![0x40, 0xb, 0x8e, 0xc1, 0xdd, 0x13, 0xb0],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[
0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0,
]);
assert_eq!(
&gsmtap_msg.payload,
&[0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0,]
);
assert_eq!(gsmtap_msg.header.packet_type, 13);
assert_eq!(gsmtap_msg.header.timeslot, 0);
assert_eq!(gsmtap_msg.header.arfcn, 1300);
@@ -421,35 +428,36 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 9);
let v8_binary = &[
0x10, 0x00, 0x21, 0x00, 0x21, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x08, 0x0a, 0x72, 0x01, 0x0e, 0x00, 0x9c, 0x18,
0x00, 0x00, 0xa9, 0x33, 0x06, 0x00, 0x00, 0x00,
0x00, 0x02, 0x00, 0x2e, 0x02,
0x10, 0x00, 0x21, 0x00, 0x21, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x08, 0x0a, 0x72, 0x01, 0x0e, 0x00, 0x9c, 0x18, 0x00, 0x00, 0xa9, 0x33, 0x06, 0x00,
0x00, 0x00, 0x00, 0x02, 0x00, 0x2e, 0x02,
];
let (_, parsed) = Message::from_bytes((v8_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 33,
inner_length: 33,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 8,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 10,
rrc_rel_min: 114,
bearer_id: 1,
phy_cell_id: 14,
earfcn: 6300,
sfn_subfn: 13225,
pdu_num: 6,
sib_mask: 0,
len: 2,
packet: vec![0x2e, 0x2],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 33,
inner_length: 33,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 8,
packet: LteRrcOtaPacket::V8 {
rrc_rel_maj: 10,
rrc_rel_min: 114,
bearer_id: 1,
phy_cell_id: 14,
earfcn: 6300,
sfn_subfn: 13225,
pdu_num: 6,
sib_mask: 0,
len: 2,
packet: vec![0x2e, 0x2],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[0x2e, 0x02]);
assert_eq!(gsmtap_msg.header.packet_type, 13);
@@ -462,46 +470,48 @@ fn test_lte_rrc_ota() {
assert_eq!(gsmtap_msg.header.subslot, 9);
let v6_binary = &[
0x10, 0x00, 0x2f, 0x00, 0x2f, 0x00, 0xc0, 0xb0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x06, 0x09, 0xb1, 0x00, 0x07, 0x01, 0x2c, 0x07,
0x25, 0x34, 0x02, 0x02, 0x00, 0x00, 0x00, 0x12,
0x00, 0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02,
0xd3, 0xb0, 0x98, 0x1c, 0x20, 0xa0, 0x81, 0x8c,
0x43, 0x26, 0xd0,
0x10, 0x00, 0x2f, 0x00, 0x2f, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x06, 0x09, 0xb1, 0x00, 0x07, 0x01, 0x2c, 0x07, 0x25, 0x34, 0x02, 0x02, 0x00, 0x00,
0x00, 0x12, 0x00, 0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02, 0xd3, 0xb0, 0x98, 0x1c, 0x20,
0xa0, 0x81, 0x8c, 0x43, 0x26, 0xd0,
];
let (_, parsed) = Message::from_bytes((v6_binary, 0)).unwrap();
assert_eq!(&parsed, &Message::Log {
pending_msgs: 0,
outer_length: 47,
inner_length: 47,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 6,
packet: LteRrcOtaPacket::V5 {
rrc_rel_maj: 9,
rrc_rel_min: 177,
bearer_id: 0,
phy_cell_id: 263,
earfcn: 1836,
sfn_subfn: 13349,
pdu_num: 2,
sib_mask: 2,
len: 18,
packet: vec![
0x40, 0x49, 0x88, 0x5, 0xc0, 0x97, 0x2, 0xd3, 0xb0, 0x98,
0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43, 0x26, 0xd0
],
assert_eq!(
&parsed,
&Message::Log {
pending_msgs: 0,
outer_length: 47,
inner_length: 47,
timestamp: Timestamp { ts: 0 },
log_type: 45248,
body: LogBody::LteRrcOtaMessage {
ext_header_version: 6,
packet: LteRrcOtaPacket::V5 {
rrc_rel_maj: 9,
rrc_rel_min: 177,
bearer_id: 0,
phy_cell_id: 263,
earfcn: 1836,
sfn_subfn: 13349,
pdu_num: 2,
sib_mask: 2,
len: 18,
packet: vec![
0x40, 0x49, 0x88, 0x5, 0xc0, 0x97, 0x2, 0xd3, 0xb0, 0x98, 0x1c, 0x20, 0xa0,
0x81, 0x8c, 0x43, 0x26, 0xd0
],
},
},
},
});
}
);
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
assert_eq!(&gsmtap_msg.payload, &[
0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02, 0xd3,
0xb0, 0x98, 0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43,
0x26, 0xd0,
]);
assert_eq!(
&gsmtap_msg.payload,
&[
0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02, 0xd3, 0xb0, 0x98, 0x1c, 0x20, 0xa0, 0x81,
0x8c, 0x43, 0x26, 0xd0,
]
);
assert_eq!(gsmtap_msg.header.packet_type, 13);
assert_eq!(gsmtap_msg.header.timeslot, 0);
assert_eq!(gsmtap_msg.header.arfcn, 1836);

View File

@@ -1,4 +1,6 @@
#!/bin/sh
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"'
echo "rebooting the device..."
adb shell '/bin/rootshell -c "reboot"'

View File

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

View File

@@ -1,33 +1,33 @@
//! a simple shell for uploading to the orbic device.
//!
//!
//! It literally just runs bash as UID/GID 0, with special Android GIDs 3003
//! (AID_INET) and 3004 (AID_NET_RAW).
use std::process::Command;
use std::os::unix::process::CommandExt;
use std::env;
use std::os::unix::process::CommandExt;
use std::process::Command;
#[cfg(target_arch = "arm")]
use nix::unistd::Gid;
fn main() {
let mut args = env::args();
let mut args = env::args();
// 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
#[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");
}
// 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
#[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();
Command::new("/bin/bash")
.args(args)
.uid(0)
.gid(0)
.exec();
// discard argv[0]
let _ = args.next();
// This call will only return if there is an error
let error = Command::new("/bin/bash").args(args).uid(0).gid(0).exec();
eprintln!("Error running command: {error}");
std::process::exit(1);
}

View File

@@ -1,9 +1,11 @@
[package]
name = "serial"
version = "0.1.0"
version = "0.2.6"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rusb = { version = "0.9.3", features = ["vendored"] }
anyhow = "1.0.97"
nusb = "0.1.13"
tokio = { version = "1.44.2", features = ["macros", "rt", "time"] }

View File

@@ -3,152 +3,171 @@
//! 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
//! # Errors
//!
//! 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() {
//! 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),
//! ````
use std::str;
use std::time::Duration;
use rusb::{Context, DeviceHandle, UsbContext};
use anyhow::{bail, Context, Result};
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use nusb::{Device, Interface};
fn main() {
const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
Make sure your device is plugged in and turned on.
If it's possible you have a device with a different usb id:
please file a bug with the output of `lsusb` attached."#;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
if args.len() != 2 || args[1] == "-h" || args[1] == "--help" {
println!("usage: {0} [<command> | --root]", args[0]);
return;
std::process::exit(1);
}
match Context::new() {
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),
if args[1] == "--root" {
enable_command_mode()
} else {
match open_orbic()? {
Some(interface) => send_command(interface, &args[1]).await,
None => bail!(ORBIC_NOT_FOUND),
}
}
}
/// 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) {
async fn send_command(interface: Interface, command: &str) -> Result<()> {
let mut data = String::new();
data.push_str("\r\n");
data.push_str(command);
data.push_str("\r\n");
let timeout = Duration::from_secs(1);
let mut response = [0; 256];
let enable_serial_port = Control {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: 0x22,
value: 3,
index: 1,
};
// Set up the serial port appropriately
handle
.write_control(0x21, 0x22, 3, 1, &[], timeout)
.expect("Failed to send control request");
interface
.control_out_blocking(enable_serial_port, &[], timeout)
.context("Failed to send control request")?;
// Send the command
handle
.write_bulk(0x2, data.as_bytes(), timeout)
.expect("Failed to write command");
tokio::time::timeout(timeout, interface.bulk_out(0x2, data.as_bytes().to_vec()))
.await
.context("Timed out writing command")?
.into_result()
.context("Failed to write command")?;
// Consume the echoed command
handle
.read_bulk(0x82, &mut response, timeout)
.expect("Failed to read submitted command");
tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
.await
.context("Timed out reading submitted command")?
.into_result()
.context("Failed to read submitted command")?;
// Read the actual response
handle
.read_bulk(0x82, &mut response, timeout)
.expect("Failed to read response");
let response = tokio::time::timeout(timeout, interface.bulk_in(0x82, RequestBuffer::new(256)))
.await
.context("Timed out reading response")?
.into_result()
.context("Failed to read response")?;
let responsestr = str::from_utf8(&response).expect("Failed to parse response");
// For some reason, on macOS the response buffer gets filled with garbage data that's
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
// the garbage with `from_utf8_lossy` and look for our expected success string.
let responsestr = String::from_utf8_lossy(&response);
if !responsestr.contains("\r\nOK\r\n") {
println!("Received unexpected response{0}", responsestr);
println!("Received unexpected response: {0}", responsestr);
std::process::exit(1);
}
Ok(())
}
/// 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 enable_command_mode<T: UsbContext>(context: &mut T) {
if open_orbic(context).is_some() {
fn enable_command_mode() -> Result<()> {
if open_orbic()?.is_some() {
println!("Device already in command mode. Doing nothing...");
return;
return Ok(());
}
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 let Some(device) = open_device(0x05c6, 0xf626)? {
let enable_command_mode = Control {
control_type: ControlType::Vendor,
recipient: Recipient::Device,
request: 0xa0,
value: 0,
index: 0,
};
let interface = device
.detach_and_claim_interface(1)
.context("detach_and_claim_interface(1) failed")?;
if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) {
// If the device reboots while the command is still executing we
// may get a pipe error here
if e == rusb::Error::Pipe {
return;
if e == nusb::transfer::TransferError::Stall {
return Ok(());
}
panic!("Failed to send device switch control request: {0}", e)
bail!("Failed to send device switch control request: {0}", e)
}
return;
return Ok(());
}
panic!("No Orbic device found");
bail!(ORBIC_NOT_FOUND);
}
/// Get a handle and contet for the orbic device
fn open_orbic<T: UsbContext>(context: &mut T) -> Option<DeviceHandle<T>> {
/// Get an Interface for the orbic device
fn open_orbic() -> Result<Option<Interface>> {
// Device after initial mode switch
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);
if let Some(device) = open_device(0x05c6, 0xf601)? {
let interface = device
.detach_and_claim_interface(1) // will reattach drivers on release
.context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface));
}
// Device with rndis enabled as well
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);
if let Some(device) = open_device(0x05c6, 0xf622)? {
let interface = device
.detach_and_claim_interface(1) // will reattach drivers on release
.context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface));
}
None
Ok(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() {
/// General function to open a USB device
fn open_device(vid: u16, pid: u16) -> Result<Option<Device>> {
let devices = match nusb::list_devices() {
Ok(d) => d,
Err(_) => return None,
Err(_) => return Ok(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 {
for device in devices {
if device.vendor_id() == vid && device.product_id() == pid {
match device.open() {
Ok(handle) => return Some(handle),
Err(e) => panic!("device found but failed to open: {}", e),
Ok(d) => return Ok(Some(d)),
Err(e) => bail!("device found but failed to open: {}", e),
}
}
}
None
Ok(None)
}

View File

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

View File

@@ -10,9 +10,9 @@ pub enum ParsingError {
}
pub fn decode<T>(data: &[u8]) -> Result<T, ParsingError>
where T: UperCodec<Output = T>
where
T: UperCodec<Output = T>,
{
let mut asn_data = PerCodecData::from_slice_uper(data);
T::uper_decode(&mut asn_data)
.map_err(ParsingError::UperDecodeError)
T::uper_decode(&mut asn_data).map_err(ParsingError::UperDecodeError)
}

View File

@@ -1,4 +1,4 @@
/*
/*
This file was autogenerated using hampi (https://github.com/ystero-dev/hampi), do not modify!
This place is not a place of honor...

View File

@@ -1,10 +1,10 @@
use telcom_parser::lte_rrc::BCCH_DL_SCH_Message;
use asn1_codecs::{uper::UperCodec, PerCodecData};
use telcom_parser::lte_rrc::BCCH_DL_SCH_Message;
fn hex_to_bin(hex: &str) -> Vec<u8> {
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i+2], 16).unwrap())
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
.collect()
}

5
tools/devenv.dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM rust:1.86-bullseye
RUN apt-get update
RUN apt-get install -y build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf
RUN rustup target add armv7-unknown-linux-gnueabihf

60
tools/nasparse.py Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/python3
import pycrate_mobile
from pycrate_mobile import NASLTE
import pycrate_core
import binascii
import sys
import pprint
from enum import Enum
import pycrate_mobile.TS24301_EMM
EPS_IMSI_ATTACH = 2
def parse_nas_message(buffer, uplink=None):
if isinstance(buffer, str): #handle string argument or raw bytes
bin = binascii.unhexlify(buffer)
else:
bin = buffer
if uplink:
parsed = NASLTE.parse_NASLTE_MO(bin)
elif uplink == None: #We don't know if its an up or downlink
parsed = NASLTE.parse_NASLTE_MO(bin)
if parsed[0] == None:
parsed = NASLTE.parse_NASLTE_MT(bin)
else:
parsed = NASLTE.parse_NASLTE_MT(bin)
if parsed[0] is None: # Not a NAS Packet
raise TypeError("Not a nas packet")
return parsed[0]
def heur_ue_imsi_sent(msg):
output = "device transmitted IMSI to base station!"
if type(msg) not in [pycrate_mobile.TS24301_EMM.EMMAttachRequest, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage]:
return (False, None)
if isinstance(msg, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage):
try:
msg = msg['EMMAttachRequest']
except pycrate_core.elt.EltErr:
return (False, None)
if msg['EPSAttachType']['V'].to_int() == EPS_IMSI_ATTACH: #EPSAttachType Value is 'Combined EPS/IMSI Attach (2)'
return (True, output)
return (False, None)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: nasparse.py [hex encoded nas message]")
exit(1)
buffer = sys.argv[1]
msg = parse_nas_message(buffer)
pprint.pprint(msg)
triggered, message = heur_ue_imsi_sent(msg)
if triggered:
print(message)
exit(1)

38
tools/nasparse_test.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/python3
import unittest
import nasparse
class TestNasparse(unittest.TestCase):
imsi_sent_msg = '07412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1000000000000'
sec_imsi_sent_msg = '1727db4b7c0207412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1'
non_nas_msg = 'deadbeefcafe'
other_nas_msg = '074413780004023fd121'
other_nas_mt_msg = "023fd12100000000000000000000000000000000000000000000000000000000"
ciphered_nas_msg = "27ed6146bd0162a5d62d62e1ce501720dc8bd84f1167fd"
def run_heur(self, msg):
buf = nasparse.parse_nas_message(msg)
return nasparse.heur_ue_imsi_sent(buf)[0]
def test_imsi_sent(self):
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "imsi_sent_msg should trigger heuristic")
def test_sec_imsi_sent(self):
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "sec_imsi_sent_msg should trigger heuristic")
def test_non_nas_msg(self):
with self.assertRaises(TypeError):
self.run_heur(self.non_nas_msg)
def test_other_nas(self):
self.assertEqual(self.run_heur(self.other_nas_msg), False, "other_nas_msg should not trigger heuristic")
def test_other_nas_mt(self):
self.assertEqual(self.run_heur(self.other_nas_mt_msg), False, "other_nas_mt_msg should not trigger heuristic")
def test_ciphered_nas(self):
self.assertEqual(self.run_heur(self.ciphered_nas_msg), False, "ciphered_nas_msg should not trigger heuristic")
if __name__ == '__main__':
unittest.main()

38
tools/pcap_check.py Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/python3
import nasparse
from scapy.utils import RawPcapNgReader
import sys
TYPE_LTE_NAS = 0x12
UDP_LEN = 28
def process_pcap(pcap_path):
print('Opening {}...'.format(pcap_path))
count = 0
for pkt_data, pkt_metadata in RawPcapNgReader(pcap_path):
count += 1
gsmtap_len = pkt_data[UDP_LEN+1] * 4 # gsmtap header length is stored in the 2nd byte of GSMTAP as a number of 32 bit words
header_end = gsmtap_len + UDP_LEN #length of UDP/IP header plus GSMTAP header
gsmtap_hdr = pkt_data[UDP_LEN:header_end]
if gsmtap_hdr[2] != TYPE_LTE_NAS:
continue
# uplink status is the 7th bit of the 5th byte of the GSMTAP header.
# Uplink (Mobile originated) = 0 Downlink (mobile terminated) = 1
uplink = (gsmtap_hdr[4] & 0b01000000) >> 6
buffer = pkt_data[header_end:]
msg = nasparse.parse_nas_message(buffer, uplink)
triggered, message = nasparse.heur_ue_imsi_sent(msg)
if triggered:
print(f"Frame {count} triggered heuristic: {message}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: pcap_check.py [path/to/pcap/file]")
exit(1)
pcap_path = sys.argv[1]
process_pcap(pcap_path)

View File

@@ -1,4 +1,5 @@
asn1tools==0.166.0
bitstruct==8.19.0
diskcache==5.6.3
pycrate==0.7.8
pyparsing==3.1.2

17
tools/run-docker-devenv Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
# Tool to cross-compile outside of Linux.
#
# On MacOS, OrbStack is recommended, but other docker distributions will work
# too.
#
# Usage:
# ./tools/run-docker-devenv
#
# Inside the shell:
# cargo build --bin rayhunter-daemon --target armv7-unknown-linux-gnueabihf --release
#
# Your output binary is in ./target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon
docker build -t rayhunter-devenv -f tools/devenv.dockerfile .
exec docker run --user $UID:$GID -v ./:/workdir -w /workdir -it rayhunter-devenv "$@"