mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 10:13:35 -07:00
Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 941ea59e11 | |||
| 8082e013f4 | |||
| a7ce1ad4d3 | |||
| 531e9aa6fb | |||
| 833d0e41b4 | |||
| 056cdac546 | |||
| 6ea2b0a4e6 | |||
| 1cc5eb4c4c | |||
| 99676f1590 | |||
| 9fe75ac961 | |||
| 151e186ef9 | |||
| 06c4dd468e | |||
| 740f979293 | |||
| 700258b0f2 | |||
| f661e2e318 | |||
| b12a159f0a | |||
| 4e40994577 | |||
| 1b29cf0dee | |||
| aafd83d636 | |||
| dd67fbf645 | |||
| e440dab736 | |||
| 30e543898b | |||
| 01e762a3d6 | |||
| fa9e9319c2 | |||
| b317200307 | |||
| 55f78cf749 | |||
| cb9e8254a8 | |||
| a9afa347f0 | |||
| 75944a7d16 | |||
| e11bb2518e | |||
| 31076ec8b2 | |||
| 5e22b5c6a8 | |||
| 3dc373f0d3 | |||
| bccdcf36e1 | |||
| fb9c4ab85b | |||
| e864ce0a51 | |||
| 7f990ae4bd | |||
| 3ac4acd83c | |||
| 5c5333f0c7 | |||
| 60934e593b | |||
| 4099eb30a5 | |||
| f81adad897 | |||
| 775468f037 | |||
| 91e825adff | |||
| 499b86aca6 | |||
| 7b897c335d | |||
| c47be1074b | |||
| 326d4106bd | |||
| df8a1f5606 | |||
| b0f5296c20 | |||
| 4e792b1402 | |||
| 9144259202 | |||
| 58f0071864 | |||
| 3c0716c877 | |||
| bf8f1fb8eb | |||
| 2a808245fb | |||
| 208ccbafaa | |||
| b150f9dc4f | |||
| b6ef48e0f6 | |||
| fddb18546c | |||
| 2911838b1c | |||
| adbe3991dd | |||
| fbc47187c5 | |||
| 5f601a209e | |||
| 04652d2097 | |||
| 034e0632e4 | |||
| 4edf001ca4 | |||
| b41f61bfa6 | |||
| 46a5bf8a84 | |||
| 2ee45382fc | |||
| f507cc0269 | |||
| 0780b527b9 | |||
| b0a1b14160 | |||
| b7243dae62 | |||
| 0c4a0123aa | |||
| 9bc8a7892b | |||
| 431a97ca65 | |||
| 0364bfbc98 | |||
| 996e47684c | |||
| 266f2b2e53 | |||
| 2080cd7845 | |||
| 9af8e006b0 | |||
| e841e22774 | |||
| 0d9f53f602 | |||
| c9dcbbe5d6 | |||
| 61d6ff6510 | |||
| e79dc4a8f0 | |||
| 6204bc0195 | |||
| 65b9843e39 | |||
| d0d01089dd | |||
| 9c26e89b24 | |||
| 1f4786db19 | |||
| 88f81d86fa | |||
| 0b3c0de481 | |||
| 188e9f436b | |||
| f2b5aa2743 | |||
| b785a7f21c | |||
| 09d35ccec7 | |||
| 5ae186bc73 | |||
| c765a40426 | |||
| 93cfbea361 | |||
| 8e6bed97b7 | |||
| 4214b27c0f | |||
| f69487853a | |||
| 7eb61748d7 | |||
| ca4e560e92 | |||
| 2ffb1d4620 | |||
| 77944dd17c | |||
| 50301076f0 | |||
| 21c839678b | |||
| 332a7ffbd0 | |||
| 8d250553b7 | |||
| fa897e73fa | |||
| c3494e338f | |||
| f9b2cd6a59 | |||
| eb072fb38c | |||
| 91f82fc71d | |||
| 6fda8450dc | |||
| bbfe5877fe | |||
| 75d3740f66 | |||
| 94c576fd96 | |||
| ee83613757 | |||
| 840f8ad8b0 | |||
| c9ac834ca7 | |||
| 8629aacf6b | |||
| a3fd1479f9 | |||
| 049c563f02 | |||
| a33b5a3418 | |||
| 107ba58296 | |||
| d016279172 | |||
| 5a084f1abb | |||
| 3619df32ab | |||
| 34d87d1fd7 | |||
| da4952e70f | |||
| 30323b8329 | |||
| 28b0f409db | |||
| 12640cc878 | |||
| 26eda5904f | |||
| 3e26e61b05 | |||
| 565c0f1e67 | |||
| 6bd36921d8 | |||
| c83ae30be8 | |||
| fa612241a5 | |||
| 10592bbd9d | |||
| 327eaddcd7 | |||
| 32149c3b37 | |||
| e47d4dacc4 | |||
| 4009e3d1ed | |||
| b2cd735a07 | |||
| 94e9a88a91 | |||
| f4a6c834d2 | |||
| 95e8f846d3 | |||
| 15f128add1 | |||
| 87f9cc403b | |||
| 7addf3a67f | |||
| 4d8cc9b738 | |||
| b0d797d206 | |||
| 1ae3b5020b | |||
| a23df84848 | |||
| 4e862841b3 | |||
| 2cc8404b13 | |||
| 35ae2962f2 | |||
| 1134361cca | |||
| bec680f93d | |||
| 968af93b69 | |||
| ee75326912 | |||
| 3b9a001e88 | |||
| 78d33b2cff | |||
| 6c237e884c | |||
| f3e4091e1d | |||
| 16f705f29c | |||
| a6fce6d568 | |||
| fcac6fdf16 | |||
| df84faa1f9 | |||
| c59fb7c013 | |||
| ca4f49b15f | |||
| 861aaedd47 | |||
| f6681a3703 | |||
| d6bc307a81 | |||
| 7cbb3369d8 | |||
| cb3dbff54a | |||
| 65e1cd4967 | |||
| d6fb54afb3 | |||
| bc93c01890 | |||
| be2d70325d | |||
| 5c4bd161fa |
@@ -1,3 +1,11 @@
|
|||||||
[target.armv7-unknown-linux-gnueabihf]
|
[target.armv7-unknown-linux-gnueabihf]
|
||||||
linker = "arm-linux-gnueabihf-gcc"
|
linker = "arm-linux-gnueabihf-gcc"
|
||||||
rustflags = ["-C", "target-feature=+crt-static"]
|
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"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
c5bbaabe15d4ccfee97b9997a13569fbfea13c45
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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..."
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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..."
|
||||||
@@ -3,32 +3,54 @@ name: Build Release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, "release-*"]
|
branches: [main, "release-*"]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_serial:
|
build_serial_and_check:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- os: ubuntu-latest
|
- name: ubuntu-24
|
||||||
build_name: serial
|
os: ubuntu-latest
|
||||||
- os: windows-latest
|
target: x86_64-unknown-linux-musl
|
||||||
build_name: serial.exe
|
- name: ubuntu-24-aarch64
|
||||||
- os: macos-latest
|
os: ubuntu-24.04-arm
|
||||||
build_name: serial
|
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 }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.platform.target }}
|
||||||
- name: Build serial
|
- name: Build serial
|
||||||
run: cargo build --bin serial --release
|
run: cargo build --bin serial --release --target ${{ matrix.platform.target }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: serial-${{ matrix.platform.os }}
|
name: serial-${{ matrix.platform.name }}
|
||||||
path: ./target/release/${{ matrix.platform.build_name }}
|
path: target/${{ matrix.platform.target }}/release/serial${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
build_rootshell_and_rayhunter:
|
- 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.name }}
|
||||||
|
path: target/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
||||||
|
if-no-files-found: error
|
||||||
|
build_rootshell:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -47,25 +69,44 @@ jobs:
|
|||||||
name: rootshell
|
name: rootshell
|
||||||
path: target/armv7-unknown-linux-gnueabihf/release/rootshell
|
path: target/armv7-unknown-linux-gnueabihf/release/rootshell
|
||||||
if-no-files-found: error
|
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)
|
- 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
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: rayhunter-daemon
|
name: rayhunter-daemon-${{ matrix.device.name }}
|
||||||
path: target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon
|
path: target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
build_release_zip:
|
build_release_zip:
|
||||||
needs:
|
needs:
|
||||||
- build_serial
|
- build_serial_and_check
|
||||||
- build_rootshell_and_rayhunter
|
- build_rootshell
|
||||||
|
- build_rayhunter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
- name: Fix executable permissions on binaries
|
- name: Fix executable permissions on binaries
|
||||||
run: chmod +x serial-*/serial rayhunter-daemon/rayhunter-daemon
|
run: chmod +x serial-*/serial rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
|
||||||
- name: Setup release directory
|
- name: Setup release directory
|
||||||
run: mv rayhunter-daemon/rayhunter-daemon rootshell/rootshell serial-* dist
|
run: mv rayhunter-daemon-* rootshell/rootshell serial-* dist
|
||||||
- name: Archive release directory
|
- name: Archive release directory
|
||||||
run: tar -cvf release.tar -C dist .
|
run: tar -cvf release.tar -C dist .
|
||||||
# TODO: have this create a release directly
|
# TODO: have this create a release directly
|
||||||
|
|||||||
@@ -11,10 +11,31 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_and_test:
|
check_and_test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
device:
|
||||||
|
- name: tplink
|
||||||
|
- name: orbic
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Check
|
- name: Check
|
||||||
run: cargo check --verbose
|
run: cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||||
- name: Run tests
|
- 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 }}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
This project is governed by [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode).
|
||||||
Generated
+221
-710
File diff suppressed because it is too large
Load Diff
@@ -1,92 +1,130 @@
|
|||||||
|

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

|

|
||||||
|
|
||||||
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.
|
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.
|
||||||
|
|
||||||
**THIS CODE IS PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS**
|
**THIS CODE IS 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
|
## The Hardware
|
||||||
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/)
|
|
||||||
|
|
||||||
## 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).
|
||||||
|
|
||||||
1. Install the Android Debug Bridge (ADB) on your computer (don't worry about instructions for installing it on a phone/device yet). You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer).
|
## Setup (Mac, Linux)
|
||||||
2. Download the latest [rayhunter release bundle](https://github.com/EFForg/rayhunter/releases) and unzip it.
|
|
||||||
3. Run the install script inside the bundle corresponding to your platform (`install-linux.sh`, `install-mac.sh`).
|
|
||||||
4. Once finished, rayhunter should be running! You can verify this by visiting the web UI as described below.
|
|
||||||
|
|
||||||
## Usage
|
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
|
||||||
|
|
||||||
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:
|
```bash
|
||||||
|
cd ~/Downloads/release
|
||||||
|
```
|
||||||
|
|
||||||
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!)
|
3. Turn on the Orbic device by holding the power button for 3 seconds. Plug it into your computer using a USB-C Cable.
|
||||||
* 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.
|
4. Run the install script for your operating system:
|
||||||
2. Over usb: Connect the Orbic device to your laptop via usb. Run `adb forward tcp:8080 tcp:8080`, then visit `http://localhost:8080`.
|
|
||||||
|
```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
|
## Development
|
||||||
* Install ADB on your computer using the instructions above.
|
|
||||||
|
|
||||||
### If your are on x86 linux
|
Follow these instructions if you need to build Rayhunter from source rather than using our [compiled builds](https://github.com/EFForg/rayhunter/releases).
|
||||||
* on your linux laptop install rust the usual way and then install cross compiling dependences.
|
|
||||||
* run `sudo apt install build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf`
|
|
||||||
|
|
||||||
* set up cross compliing for rust:
|
* 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 you're on x86 linux
|
||||||
|
|
||||||
|
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 x86_64-unknown-linux-gnu
|
||||||
rustup target add armv7-unknown-linux-gnueabihf
|
rustup target add armv7-unknown-linux-gnueabihf
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you can root your device and install rayhunter by running `./install.sh` - **Note:** You will have to install the cross compile tooling below before running this.
|
Now you can root your device and install Rayhunter by running `./tools/install-dev.sh`
|
||||||
|
|
||||||
### If you aren't on linux or can't run the install scripts
|
## Support and Discussion
|
||||||
* Root your device on windows using the instructions here: https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183
|
|
||||||
|
|
||||||
* Build for arm using `cargo build`
|
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`
|
**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.
|
||||||
|
|
||||||
* 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.
|
|
||||||
|
|
||||||
*Good Hunting!*
|
*Good Hunting!*
|
||||||
|
|||||||
@@ -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).
|
||||||
+15
-6
@@ -1,8 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter-daemon"
|
name = "rayhunter-daemon"
|
||||||
version = "0.1.0"
|
version = "0.2.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# These feature flags are mutually exclusive, and exactly one must be enabled.
|
||||||
|
orbic = ["rayhunter/orbic"]
|
||||||
|
tplink = ["rayhunter/tplink"]
|
||||||
|
|
||||||
|
default = ["orbic"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rayhunter-daemon"
|
name = "rayhunter-daemon"
|
||||||
path = "src/daemon.rs"
|
path = "src/daemon.rs"
|
||||||
@@ -15,20 +22,22 @@ path = "src/check.rs"
|
|||||||
rayhunter = { path = "../lib" }
|
rayhunter = { path = "../lib" }
|
||||||
toml = "0.8.8"
|
toml = "0.8.8"
|
||||||
serde = { version = "1.0.193", features = ["derive"] }
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
tokio = { version = "1.35.1", features = ["full"] }
|
tokio = { version = "1.44.2", features = ["full"] }
|
||||||
axum = "0.7.3"
|
axum = "0.8"
|
||||||
futures-core = "0.3.30"
|
futures-core = "0.3.30"
|
||||||
thiserror = "1.0.52"
|
thiserror = "1.0.52"
|
||||||
|
libc = "0.2.150"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
env_logger = "0.10.1"
|
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"
|
futures-macro = "0.3.30"
|
||||||
include_dir = "0.7.3"
|
include_dir = "0.7.3"
|
||||||
mime_guess = "2.0.4"
|
mime_guess = "2.0.4"
|
||||||
tempdir = "0.3.7"
|
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
tokio-stream = "0.1.14"
|
tokio-stream = "0.1.14"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
clap = { version = "4.5.2", features = ["derive"] }
|
clap = { version = "4.5.2", features = ["derive"] }
|
||||||
serde_json = "1.0.114"
|
serde_json = "1.0.114"
|
||||||
image = "0.25.1"
|
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
||||||
|
tempfile = "3.10.1"
|
||||||
|
simple_logger = "5.0.0"
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::{future, pin};
|
||||||
|
|
||||||
|
use axum::Json;
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use rayhunter::analysis::analyzer::Harness;
|
||||||
|
use rayhunter::diag::{DataType, MessagesContainer};
|
||||||
|
use rayhunter::qmdl::QmdlReader;
|
||||||
|
use serde::Serialize;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::io::{AsyncWriteExt, BufWriter};
|
||||||
|
use tokio::sync::mpsc::Receiver;
|
||||||
|
use tokio::sync::{RwLock, RwLockWriteGuard};
|
||||||
|
use tokio_util::task::TaskTracker;
|
||||||
|
|
||||||
|
use crate::dummy_analyzer::TestAnalyzer;
|
||||||
|
use crate::qmdl_store::RecordingStore;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub struct AnalysisWriter {
|
||||||
|
writer: BufWriter<File>,
|
||||||
|
harness: Harness,
|
||||||
|
bytes_written: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We write our analysis results to a file immediately to minimize the amount of
|
||||||
|
// state Rayhunter has to keep track of in memory. The analysis file's format is
|
||||||
|
// Newline Delimited JSON
|
||||||
|
// (https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson), which
|
||||||
|
// lets us simply append new rows to the end without parsing the entire JSON
|
||||||
|
// object beforehand.
|
||||||
|
impl AnalysisWriter {
|
||||||
|
pub async fn new(file: File, enable_dummy_analyzer: bool) -> Result<Self, std::io::Error> {
|
||||||
|
let mut harness = Harness::new_with_all_analyzers();
|
||||||
|
if enable_dummy_analyzer {
|
||||||
|
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = Self {
|
||||||
|
writer: BufWriter::new(file),
|
||||||
|
bytes_written: 0,
|
||||||
|
harness,
|
||||||
|
};
|
||||||
|
let metadata = result.harness.get_metadata();
|
||||||
|
result.write(&metadata).await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the analysis harness on the given container, serializing the results
|
||||||
|
// to the analysis file and returning the file's new length.
|
||||||
|
pub async fn analyze(
|
||||||
|
&mut self,
|
||||||
|
container: MessagesContainer,
|
||||||
|
) -> Result<(usize, bool), std::io::Error> {
|
||||||
|
let row = self.harness.analyze_qmdl_messages(container);
|
||||||
|
if !row.is_empty() {
|
||||||
|
self.write(&row).await?;
|
||||||
|
}
|
||||||
|
Ok((self.bytes_written, row.contains_warnings()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
||||||
|
let mut value_str = serde_json::to_string(value).unwrap();
|
||||||
|
value_str.push('\n');
|
||||||
|
self.bytes_written += value_str.len();
|
||||||
|
self.writer.write_all(value_str.as_bytes()).await?;
|
||||||
|
self.writer.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flushes any pending I/O to disk before dropping the writer
|
||||||
|
pub async fn close(mut self) -> Result<(), std::io::Error> {
|
||||||
|
self.writer.flush().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone, Default)]
|
||||||
|
pub struct AnalysisStatus {
|
||||||
|
queued: Vec<String>,
|
||||||
|
running: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AnalysisCtrlMessage {
|
||||||
|
NewFilesQueued,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn queued_len(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -> usize {
|
||||||
|
analysis_status_lock.read().await.queued.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dequeue_to_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -> String {
|
||||||
|
let mut analysis_status = analysis_status_lock.write().await;
|
||||||
|
let name = analysis_status.queued.remove(0);
|
||||||
|
assert!(analysis_status.running.is_none());
|
||||||
|
analysis_status.running = Some(name.clone());
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) {
|
||||||
|
let mut analysis_status = analysis_status_lock.write().await;
|
||||||
|
analysis_status.running = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn perform_analysis(
|
||||||
|
name: &str,
|
||||||
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
info!("Opening QMDL and analysis file for {}...", name);
|
||||||
|
let (analysis_file, qmdl_file, entry_index) = {
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
let (entry_index, _) = qmdl_store
|
||||||
|
.entry_for_name(name)
|
||||||
|
.ok_or(format!("failed to find QMDL store entry for {}", name))?;
|
||||||
|
let analysis_file = qmdl_store
|
||||||
|
.clear_and_open_entry_analysis(entry_index)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
let qmdl_file = qmdl_store
|
||||||
|
.open_entry_qmdl(entry_index)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
|
||||||
|
(analysis_file, qmdl_file, entry_index)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut analysis_writer = AnalysisWriter::new(analysis_file, enable_dummy_analyzer)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
let file_size = qmdl_file
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
|
.expect("failed to get QMDL file metadata")
|
||||||
|
.len();
|
||||||
|
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||||
|
let mut qmdl_stream = pin::pin!(qmdl_reader
|
||||||
|
.as_stream()
|
||||||
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||||
|
|
||||||
|
info!("Starting analysis for {}...", name);
|
||||||
|
while let Some(container) = qmdl_stream
|
||||||
|
.try_next()
|
||||||
|
.await
|
||||||
|
.expect("failed getting QMDL container")
|
||||||
|
{
|
||||||
|
let (size_bytes, _) = analysis_writer
|
||||||
|
.analyze(container)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
debug!("{} analysis: {} bytes written", name, size_bytes);
|
||||||
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
|
qmdl_store
|
||||||
|
.update_entry_analysis_size(entry_index, size_bytes)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis_writer
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:?}", e))?;
|
||||||
|
info!("Analysis for {} complete!", name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_analysis_thread(
|
||||||
|
task_tracker: &TaskTracker,
|
||||||
|
mut analysis_rx: Receiver<AnalysisCtrlMessage>,
|
||||||
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
|
) {
|
||||||
|
task_tracker.spawn(async move {
|
||||||
|
loop {
|
||||||
|
match analysis_rx.recv().await {
|
||||||
|
Some(AnalysisCtrlMessage::NewFilesQueued) => {
|
||||||
|
let count = queued_len(analysis_status_lock.clone()).await;
|
||||||
|
for _ in 0..count {
|
||||||
|
let name = dequeue_to_running(analysis_status_lock.clone()).await;
|
||||||
|
if let Err(err) =
|
||||||
|
perform_analysis(&name, qmdl_store_lock.clone(), enable_dummy_analyzer)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("failed to analyze {}: {}", name, err);
|
||||||
|
}
|
||||||
|
clear_running(analysis_status_lock.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(AnalysisCtrlMessage::Exit) | None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_analysis_status(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
) -> Result<Json<AnalysisStatus>, (StatusCode, String)> {
|
||||||
|
Ok(Json(state.analysis_status_lock.read().await.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_qmdl(name: &str, analysis_status: &mut RwLockWriteGuard<AnalysisStatus>) -> bool {
|
||||||
|
if analysis_status.queued.iter().any(|n| n == name)
|
||||||
|
|| analysis_status.running.iter().any(|n| n == name)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
analysis_status.queued.push(name.to_string());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_analysis(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
Path(qmdl_name): Path<String>,
|
||||||
|
) -> Result<(StatusCode, Json<AnalysisStatus>), (StatusCode, String)> {
|
||||||
|
let mut analysis_status = state.analysis_status_lock.write().await;
|
||||||
|
let store = state.qmdl_store_lock.read().await;
|
||||||
|
let queued = if qmdl_name.is_empty() {
|
||||||
|
let mut entry_names: Vec<&str> = store
|
||||||
|
.manifest
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.name.as_str())
|
||||||
|
.collect();
|
||||||
|
if let Some(current_entry) = store.current_entry {
|
||||||
|
entry_names.remove(current_entry);
|
||||||
|
}
|
||||||
|
entry_names
|
||||||
|
.iter()
|
||||||
|
.any(|name| queue_qmdl(name, &mut analysis_status))
|
||||||
|
} else {
|
||||||
|
queue_qmdl(&qmdl_name, &mut analysis_status)
|
||||||
|
};
|
||||||
|
if queued {
|
||||||
|
state
|
||||||
|
.analysis_sender
|
||||||
|
.send(AnalysisCtrlMessage::NewFilesQueued)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("failed to queue new analysis files: {:?}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok((StatusCode::ACCEPTED, Json(analysis_status.clone())))
|
||||||
|
}
|
||||||
+155
-14
@@ -1,31 +1,172 @@
|
|||||||
use std::{future, path::PathBuf, pin::pin};
|
|
||||||
use rayhunter::{analysis::analyzer::Harness, diag::DataType, qmdl::QmdlReader};
|
|
||||||
use tokio::fs::File;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::TryStreamExt;
|
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)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about)]
|
#[command(version, about)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(short, long)]
|
#[arg(short = 'p', long)]
|
||||||
qmdl_path: PathBuf,
|
qmdl_path: PathBuf,
|
||||||
|
|
||||||
|
#[arg(short = 'c', long)]
|
||||||
|
pcapify: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
show_skipped: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
verbose: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
|
||||||
|
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||||
|
let file_size = qmdl_file
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
|
.expect("failed to get QMDL file metadata")
|
||||||
|
.len();
|
||||||
|
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
||||||
|
let mut qmdl_stream = pin!(qmdl_reader
|
||||||
|
.as_stream()
|
||||||
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||||
|
let mut skipped_reasons: HashMap<String, i32> = HashMap::new();
|
||||||
|
let mut total_messages = 0;
|
||||||
|
let mut warnings = 0;
|
||||||
|
let mut skipped = 0;
|
||||||
|
while let Some(container) = qmdl_stream
|
||||||
|
.try_next()
|
||||||
|
.await
|
||||||
|
.expect("failed getting QMDL container")
|
||||||
|
{
|
||||||
|
let row = harness.analyze_qmdl_messages(container);
|
||||||
|
total_messages += 1;
|
||||||
|
for reason in row.skipped_message_reasons {
|
||||||
|
*skipped_reasons.entry(reason).or_insert(0) += 1;
|
||||||
|
skipped += 1;
|
||||||
|
}
|
||||||
|
for analysis in row.analysis {
|
||||||
|
for maybe_event in analysis.events {
|
||||||
|
let Some(event) = maybe_event else { continue };
|
||||||
|
match event.event_type {
|
||||||
|
EventType::Informational => {
|
||||||
|
info!(
|
||||||
|
"{}: INFO - {} {}",
|
||||||
|
qmdl_path, analysis.timestamp, event.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EventType::QualitativeWarning { severity } => {
|
||||||
|
warn!(
|
||||||
|
"{}: WARNING (Severity: {:?}) - {} {}",
|
||||||
|
qmdl_path, severity, analysis.timestamp, event.message,
|
||||||
|
);
|
||||||
|
warnings += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if show_skipped && skipped > 0 {
|
||||||
|
info!("{}: messages skipped:", qmdl_path);
|
||||||
|
for (reason, count) in skipped_reasons.iter() {
|
||||||
|
info!(" - {}: \"{}\"", count, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"{}: {} messages analyzed, {} warnings, {} messages skipped",
|
||||||
|
qmdl_path, total_messages, warnings, skipped
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pcapify(qmdl_path: &PathBuf) {
|
||||||
|
let qmdl_file = &mut File::open(&qmdl_path)
|
||||||
|
.await
|
||||||
|
.expect("failed to open qmdl file");
|
||||||
|
let qmdl_file_size = qmdl_file.metadata().await.unwrap().len();
|
||||||
|
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize));
|
||||||
|
let mut pcap_path = qmdl_path.clone();
|
||||||
|
pcap_path.set_extension("pcap");
|
||||||
|
let pcap_file = &mut File::create(&pcap_path)
|
||||||
|
.await
|
||||||
|
.expect("failed to open pcap file");
|
||||||
|
let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap();
|
||||||
|
pcap_writer.write_iface_header().await.unwrap();
|
||||||
|
while let Some(container) = qmdl_reader
|
||||||
|
.get_next_messages_container()
|
||||||
|
.await
|
||||||
|
.expect("failed to get container")
|
||||||
|
{
|
||||||
|
for 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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
|
||||||
let args = Args::parse();
|
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();
|
let mut harness = Harness::new_with_all_analyzers();
|
||||||
|
if args.enable_dummy_analyzer {
|
||||||
|
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
||||||
|
}
|
||||||
|
info!("Analyzers:");
|
||||||
|
for analyzer in harness.get_metadata().analyzers {
|
||||||
|
info!(" - {}: {}", analyzer.name, analyzer.description);
|
||||||
|
}
|
||||||
|
|
||||||
let qmdl_file = File::open(args.qmdl_path).await.expect("failed to open QMDL file");
|
let metadata = metadata(&args.qmdl_path)
|
||||||
let file_size = qmdl_file.metadata().await.expect("failed to get QMDL file metadata").len();
|
.await
|
||||||
let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize));
|
.expect("failed to get metadata");
|
||||||
let mut qmdl_stream = pin!(qmdl_reader.as_stream()
|
if metadata.is_dir() {
|
||||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
let mut dir = read_dir(&args.qmdl_path).await.expect("failed to read dir");
|
||||||
println!("{}\n", serde_json::to_string(&harness.get_metadata()).expect("failed to serialize report metadata"));
|
while let Some(entry) = dir.next_entry().await.expect("failed to get entry") {
|
||||||
while let Some(container) = qmdl_stream.try_next().await.expect("failed getting QMDL container") {
|
let name = entry.file_name();
|
||||||
let row = harness.analyze_qmdl_messages(container);
|
let name_str = name.to_str().unwrap();
|
||||||
println!("{}\n", serde_json::to_string(&row).expect("failed to serialize row"));
|
if name_str.ends_with(".qmdl") {
|
||||||
|
let path = entry.path();
|
||||||
|
let path_str = path.to_str().unwrap();
|
||||||
|
analyze_file(&mut harness, path_str, args.show_skipped).await;
|
||||||
|
if args.pcapify {
|
||||||
|
pcapify(&path).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let path = args.qmdl_path.to_str().unwrap();
|
||||||
|
analyze_file(&mut harness, path, args.show_skipped).await;
|
||||||
|
if args.pcapify {
|
||||||
|
pcapify(&args.qmdl_path).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-20
@@ -2,20 +2,15 @@ use crate::error::RayhunterError;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ConfigFile {
|
#[serde(default)]
|
||||||
qmdl_store_path: Option<String>,
|
|
||||||
port: Option<u16>,
|
|
||||||
readonly_mode: Option<bool>,
|
|
||||||
ui_level: Option<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub qmdl_store_path: String,
|
pub qmdl_store_path: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub readonly_mode: bool,
|
pub debug_mode: bool,
|
||||||
pub ui_level: u8,
|
pub ui_level: u8,
|
||||||
|
pub enable_dummy_analyzer: bool,
|
||||||
|
pub colorblind_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -23,23 +18,23 @@ impl Default for Config {
|
|||||||
Config {
|
Config {
|
||||||
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
|
qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
|
||||||
port: 8080,
|
port: 8080,
|
||||||
readonly_mode: false,
|
debug_mode: false,
|
||||||
ui_level: 1,
|
ui_level: 1,
|
||||||
|
enable_dummy_analyzer: false,
|
||||||
|
colorblind_mode: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError> where P: AsRef<std::path::Path> {
|
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
|
||||||
let mut config = Config::default();
|
where
|
||||||
|
P: AsRef<std::path::Path>,
|
||||||
|
{
|
||||||
if let Ok(config_file) = std::fs::read_to_string(&path) {
|
if let Ok(config_file) = std::fs::read_to_string(&path) {
|
||||||
let parsed_config: ConfigFile = toml::from_str(&config_file)
|
Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
|
||||||
.map_err(RayhunterError::ConfigFileParsingError)?;
|
} else {
|
||||||
if let Some(path) = parsed_config.qmdl_store_path { config.qmdl_store_path = path }
|
Ok(Config::default())
|
||||||
if let Some(port) = parsed_config.port { config.port = port }
|
|
||||||
if let Some(readonly_mode) = parsed_config.readonly_mode { config.readonly_mode = readonly_mode }
|
|
||||||
if let Some(ui_level) = parsed_config.ui_level { config.ui_level = ui_level }
|
|
||||||
}
|
}
|
||||||
Ok(config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
|||||||
+150
-111
@@ -1,39 +1,62 @@
|
|||||||
|
mod analysis;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod diag;
|
||||||
|
mod display;
|
||||||
|
mod dummy_analyzer;
|
||||||
mod error;
|
mod error;
|
||||||
mod pcap;
|
mod pcap;
|
||||||
|
mod qmdl_store;
|
||||||
mod server;
|
mod server;
|
||||||
mod stats;
|
mod stats;
|
||||||
mod qmdl_store;
|
|
||||||
mod diag;
|
|
||||||
mod framebuffer;
|
|
||||||
|
|
||||||
use crate::config::{parse_config, parse_args};
|
use crate::config::{parse_args, parse_config};
|
||||||
use crate::diag::run_diag_read_thread;
|
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::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 axum::response::Redirect;
|
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::routing::{get, post};
|
||||||
use axum::Router;
|
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 stats::get_qmdl_manifest;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
use tokio::sync::mpsc::{self, Sender};
|
use tokio::sync::mpsc::{self, Sender};
|
||||||
use tokio::sync::oneshot::error::TryRecvError;
|
use tokio::sync::{oneshot, RwLock};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::thread::sleep;
|
type AppRouter = Router<Arc<ServerState>>;
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::net::TcpListener;
|
fn get_router() -> AppRouter {
|
||||||
use tokio::sync::{RwLock, oneshot};
|
Router::new()
|
||||||
use std::sync::Arc;
|
.route("/api/pcap/{name}", get(get_pcap))
|
||||||
use include_dir::{include_dir, Dir};
|
.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
|
// 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
|
// ServerState and a oneshot Receiver that'll fire when it's time to shutdown
|
||||||
@@ -41,34 +64,19 @@ use include_dir::{include_dir, Dir};
|
|||||||
async fn run_server(
|
async fn run_server(
|
||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
state: Arc<ServerState>,
|
||||||
server_shutdown_rx: oneshot::Receiver<()>,
|
server_shutdown_rx: oneshot::Receiver<()>,
|
||||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>
|
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
let state = Arc::new(ServerState {
|
info!("spinning up server");
|
||||||
qmdl_store_lock,
|
let app = get_router().with_state(state);
|
||||||
diag_device_ctrl_sender: diag_device_sender,
|
|
||||||
readonly_mode: config.readonly_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", get(get_analysis_report))
|
|
||||||
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
|
||||||
.route("/*path", get(serve_static))
|
|
||||||
.with_state(state);
|
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
|
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
|
||||||
let listener = TcpListener::bind(&addr).await.unwrap();
|
let listener = TcpListener::bind(&addr).await.unwrap();
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
info!("The orca is hunting for stingrays...");
|
info!("The orca is hunting for stingrays...");
|
||||||
axum::serve(listener, app)
|
axum::serve(listener, app)
|
||||||
.with_graceful_shutdown(server_shutdown_signal(server_shutdown_rx))
|
.with_graceful_shutdown(server_shutdown_signal(server_shutdown_rx))
|
||||||
.await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,13 +85,31 @@ async fn server_shutdown_signal(server_shutdown_rx: oneshot::Receiver<()>) {
|
|||||||
info!("Server received shutdown signal, exiting...");
|
info!("Server received shutdown signal, exiting...");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads a QmdlStore if one exists, and if not, only create one if we're not in
|
// Loads a RecordingStore if one exists, and if not, only create one if we're
|
||||||
// readonly mode.
|
// 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> {
|
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
|
||||||
match (RecordingStore::exists(&config.qmdl_store_path).await?, config.readonly_mode) {
|
let store_exists = RecordingStore::exists(&config.qmdl_store_path).await?;
|
||||||
(true, _) => Ok(RecordingStore::load(&config.qmdl_store_path).await?),
|
if config.debug_mode {
|
||||||
(false, false) => Ok(RecordingStore::create(&config.qmdl_store_path).await?),
|
if store_exists {
|
||||||
(false, true) => Err(RayhunterError::NoStoreReadonlyMode(config.qmdl_store_path.clone())),
|
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?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +120,9 @@ fn run_ctrl_c_thread(
|
|||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
server_shutdown_tx: oneshot::Sender<()>,
|
server_shutdown_tx: oneshot::Sender<()>,
|
||||||
ui_shutdown_tx: oneshot::Sender<()>,
|
maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
analysis_tx: Sender<AnalysisCtrlMessage>,
|
||||||
) -> JoinHandle<Result<(), RayhunterError>> {
|
) -> JoinHandle<Result<(), RayhunterError>> {
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
match tokio::signal::ctrl_c().await {
|
match tokio::signal::ctrl_c().await {
|
||||||
@@ -107,14 +134,24 @@ fn run_ctrl_c_thread(
|
|||||||
info!("Done!");
|
info!("Done!");
|
||||||
}
|
}
|
||||||
|
|
||||||
server_shutdown_tx.send(())
|
server_shutdown_tx
|
||||||
|
.send(())
|
||||||
.expect("couldn't send server shutdown signal");
|
.expect("couldn't send server shutdown signal");
|
||||||
info!("sending UI shutdown");
|
info!("sending UI shutdown");
|
||||||
ui_shutdown_tx.send(())
|
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
|
||||||
.expect("couldn't send ui shutdown signal");
|
ui_shutdown_tx
|
||||||
diag_device_sender.send(DiagDeviceCtrlMessage::Exit).await
|
.send(())
|
||||||
|
.expect("couldn't send ui shutdown signal");
|
||||||
|
}
|
||||||
|
diag_device_sender
|
||||||
|
.send(DiagDeviceCtrlMessage::Exit)
|
||||||
|
.await
|
||||||
.expect("couldn't send Exit message to diag thread");
|
.expect("couldn't send Exit message to diag thread");
|
||||||
},
|
analysis_tx
|
||||||
|
.send(AnalysisCtrlMessage::Exit)
|
||||||
|
.await
|
||||||
|
.expect("couldn't send Exit message to analysis thread");
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Unable to listen for shutdown signal: {}", err);
|
error!("Unable to listen for shutdown signal: {}", err);
|
||||||
}
|
}
|
||||||
@@ -123,56 +160,6 @@ fn run_ctrl_c_thread(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>){
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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(framebuffer::Color565::Green, 2);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
sleep(Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
}).await.unwrap();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), RayhunterError> {
|
async fn main() -> Result<(), RayhunterError> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
@@ -183,25 +170,77 @@ async fn main() -> Result<(), RayhunterError> {
|
|||||||
// TaskTrackers give us an interface to spawn tokio threads, and then
|
// TaskTrackers give us an interface to spawn tokio threads, and then
|
||||||
// eventually await all of them ending
|
// eventually await all of them ending
|
||||||
let task_tracker = TaskTracker::new();
|
let task_tracker = TaskTracker::new();
|
||||||
|
println!("R A Y H U N T E R 🐳");
|
||||||
|
|
||||||
let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
|
let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
|
||||||
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
||||||
if !config.readonly_mode {
|
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
|
||||||
let mut dev = DiagDevice::new().await
|
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
|
||||||
|
let mut maybe_ui_shutdown_tx = None;
|
||||||
|
if !config.debug_mode {
|
||||||
|
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
|
||||||
|
maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
|
||||||
|
let mut dev = DiagDevice::new()
|
||||||
|
.await
|
||||||
.map_err(RayhunterError::DiagInitError)?;
|
.map_err(RayhunterError::DiagInitError)?;
|
||||||
dev.config_logs().await
|
dev.config_logs()
|
||||||
|
.await
|
||||||
.map_err(RayhunterError::DiagInitError)?;
|
.map_err(RayhunterError::DiagInitError)?;
|
||||||
|
|
||||||
run_diag_read_thread(&task_tracker, dev, rx, qmdl_store_lock.clone());
|
info!("Starting Diag Thread");
|
||||||
|
run_diag_read_thread(
|
||||||
|
&task_tracker,
|
||||||
|
dev,
|
||||||
|
rx,
|
||||||
|
ui_update_tx.clone(),
|
||||||
|
qmdl_store_lock.clone(),
|
||||||
|
config.enable_dummy_analyzer,
|
||||||
|
);
|
||||||
|
info!("Starting UI");
|
||||||
|
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
|
||||||
}
|
}
|
||||||
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
|
|
||||||
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
|
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
|
||||||
run_ctrl_c_thread(&task_tracker, tx.clone(), server_shutdown_tx, ui_shutdown_tx, qmdl_store_lock.clone());
|
info!("create shutdown thread");
|
||||||
run_server(&task_tracker, &config, qmdl_store_lock.clone(), server_shutdown_rx, tx).await;
|
let analysis_status_lock = Arc::new(RwLock::new(AnalysisStatus::default()));
|
||||||
update_ui(&task_tracker, &config, ui_shutdown_rx).await;
|
run_analysis_thread(
|
||||||
|
&task_tracker,
|
||||||
|
analysis_rx,
|
||||||
|
qmdl_store_lock.clone(),
|
||||||
|
analysis_status_lock.clone(),
|
||||||
|
config.enable_dummy_analyzer,
|
||||||
|
);
|
||||||
|
run_ctrl_c_thread(
|
||||||
|
&task_tracker,
|
||||||
|
tx.clone(),
|
||||||
|
server_shutdown_tx,
|
||||||
|
maybe_ui_shutdown_tx,
|
||||||
|
qmdl_store_lock.clone(),
|
||||||
|
analysis_tx.clone(),
|
||||||
|
);
|
||||||
|
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.close();
|
||||||
task_tracker.wait().await;
|
task_tracker.wait().await;
|
||||||
|
|
||||||
|
info!("see you space cowboy...");
|
||||||
Ok(())
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+189
-86
@@ -2,25 +2,24 @@ use std::pin::pin;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::State;
|
use axum::extract::{Path, State};
|
||||||
use axum::http::header::CONTENT_TYPE;
|
use axum::http::header::CONTENT_TYPE;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use rayhunter::analysis::analyzer::Harness;
|
use futures::{StreamExt, TryStreamExt};
|
||||||
use rayhunter::diag::{DataType, MessagesContainer};
|
|
||||||
use rayhunter::diag_device::DiagDevice;
|
|
||||||
use serde::Serialize;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use tokio::sync::mpsc::Receiver;
|
|
||||||
use rayhunter::qmdl::QmdlWriter;
|
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
|
use rayhunter::diag::DataType;
|
||||||
|
use rayhunter::diag_device::DiagDevice;
|
||||||
|
use rayhunter::qmdl::QmdlWriter;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{BufWriter, AsyncWriteExt};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use tokio_util::task::TaskTracker;
|
use tokio_util::task::TaskTracker;
|
||||||
use futures::{StreamExt, TryStreamExt};
|
|
||||||
|
|
||||||
use crate::qmdl_store::RecordingStore;
|
use crate::analysis::AnalysisWriter;
|
||||||
|
use crate::display;
|
||||||
|
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
|
|
||||||
pub enum DiagDeviceCtrlMessage {
|
pub enum DiagDeviceCtrlMessage {
|
||||||
@@ -29,67 +28,19 @@ pub enum DiagDeviceCtrlMessage {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AnalysisWriter {
|
|
||||||
writer: BufWriter<File>,
|
|
||||||
harness: Harness,
|
|
||||||
bytes_written: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
// We write our analysis results to a file immediately to minimize the amount of
|
|
||||||
// state Rayhunter has to keep track of in memory. The analysis file's format is
|
|
||||||
// Newline Delimited JSON
|
|
||||||
// (https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson), which
|
|
||||||
// lets us simply append new rows to the end without parsing the entire JSON
|
|
||||||
// object beforehand.
|
|
||||||
impl AnalysisWriter {
|
|
||||||
pub async fn new(file: File) -> Result<Self, std::io::Error> {
|
|
||||||
let mut result = Self {
|
|
||||||
writer: BufWriter::new(file),
|
|
||||||
harness: Harness::new_with_all_analyzers(),
|
|
||||||
bytes_written: 0,
|
|
||||||
};
|
|
||||||
let metadata = result.harness.get_metadata();
|
|
||||||
result.write(&metadata).await?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs the analysis harness on the given container, serializing the results
|
|
||||||
// to the analysis file and returning the file's new length.
|
|
||||||
pub async fn analyze(&mut self, container: MessagesContainer) -> Result<usize, std::io::Error> {
|
|
||||||
let row = self.harness.analyze_qmdl_messages(container);
|
|
||||||
if !row.is_empty() {
|
|
||||||
self.write(&row).await?;
|
|
||||||
}
|
|
||||||
Ok(self.bytes_written)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
|
||||||
let mut value_str = serde_json::to_string(value).unwrap();
|
|
||||||
value_str.push('\n');
|
|
||||||
self.bytes_written += value_str.len();
|
|
||||||
self.writer.write_all(value_str.as_bytes()).await?;
|
|
||||||
self.writer.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flushes any pending I/O to disk before dropping the writer
|
|
||||||
pub async fn close(mut self) -> Result<(), std::io::Error> {
|
|
||||||
self.writer.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_diag_read_thread(
|
pub fn run_diag_read_thread(
|
||||||
task_tracker: &TaskTracker,
|
task_tracker: &TaskTracker,
|
||||||
mut dev: DiagDevice,
|
mut dev: DiagDevice,
|
||||||
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>
|
ui_update_sender: Sender<display::DisplayState>,
|
||||||
|
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
|
enable_dummy_analyzer: bool,
|
||||||
) {
|
) {
|
||||||
task_tracker.spawn(async move {
|
task_tracker.spawn(async move {
|
||||||
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
||||||
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
|
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
|
||||||
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
||||||
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file).await
|
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer).await
|
||||||
.expect("failed to create analysis writer"));
|
.expect("failed to create analysis writer"));
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -100,7 +51,7 @@ pub fn run_diag_read_thread(
|
|||||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||||
}
|
}
|
||||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file).await
|
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
|
||||||
.expect("failed to write to analysis file"));
|
.expect("failed to write to analysis file"));
|
||||||
},
|
},
|
||||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||||
@@ -143,11 +94,17 @@ pub fn run_diag_read_thread(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(analysis_writer) = maybe_analysis_writer.as_mut() {
|
if let Some(analysis_writer) = maybe_analysis_writer.as_mut() {
|
||||||
let analysis_file_len = analysis_writer.analyze(container).await
|
let analysis_output = analysis_writer.analyze(container).await
|
||||||
.expect("failed to analyze container");
|
.expect("failed to analyze container");
|
||||||
|
let (analysis_file_len, heuristic_warning) = analysis_output;
|
||||||
|
if heuristic_warning {
|
||||||
|
info!("a heuristic triggered on this run!");
|
||||||
|
ui_update_sender.send(display::DisplayState::WarningDetected).await
|
||||||
|
.expect("couldn't send ui update message: {}");
|
||||||
|
}
|
||||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||||
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
||||||
qmdl_store.update_entry_analysis_size(index, analysis_file_len as usize).await
|
qmdl_store.update_entry_analysis_size(index, analysis_file_len).await
|
||||||
.expect("failed to update analysis file size");
|
.expect("failed to update analysis file size");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -162,40 +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(
|
||||||
if state.readonly_mode {
|
State(state): State<Arc<ServerState>>,
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_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;
|
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||||
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await
|
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await.map_err(|e| {
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't create new qmdl entry: {}", e)))?;
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't create new qmdl entry: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||||
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StartRecording((qmdl_writer, analysis_file))).await
|
state
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
|
.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 = 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()))
|
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(
|
||||||
if state.readonly_mode {
|
State(state): State<Arc<ServerState>>,
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in readonly mode".to_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;
|
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||||
qmdl_store.close_current_entry().await
|
qmdl_store.close_current_entry().await.map_err(|e| {
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't close current qmdl entry: {}", e)))?;
|
(
|
||||||
state.diag_device_ctrl_sender.send(DiagDeviceCtrlMessage::StopRecording).await
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?;
|
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()))
|
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_analysis_report(State(state): State<Arc<ServerState>>) -> 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 qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let Some(entry) = qmdl_store.get_current_entry() else {
|
let (entry_index, _) = if qmdl_name == "live" {
|
||||||
return Err((
|
qmdl_store.get_current_entry().ok_or((
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
"No QMDL data's being recorded to analyze, try starting a new recording!".to_string()
|
"No QMDL data's being recorded to analyze, try starting a new recording!".to_string(),
|
||||||
));
|
))?
|
||||||
|
} else {
|
||||||
|
qmdl_store.entry_for_name(&qmdl_name).ok_or((
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
format!("Couldn't find QMDL entry with name \"{}\"", qmdl_name),
|
||||||
|
))?
|
||||||
};
|
};
|
||||||
let analysis_file = qmdl_store.open_entry_analysis(entry).await
|
let analysis_file = qmdl_store
|
||||||
|
.open_entry_analysis(entry_index)
|
||||||
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
||||||
let analysis_stream = ReaderStream::new(analysis_file);
|
let analysis_stream = ReaderStream::new(analysis_file);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use rayhunter::telcom_parser::lte_rrc::{PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Identity};
|
||||||
|
|
||||||
|
use rayhunter::analysis::analyzer::{Analyzer, Event, EventType, Severity};
|
||||||
|
use rayhunter::analysis::information_element::{InformationElement, LteInformationElement};
|
||||||
|
|
||||||
|
pub struct TestAnalyzer {
|
||||||
|
pub count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Analyzer for TestAnalyzer {
|
||||||
|
fn get_name(&self) -> Cow<str> {
|
||||||
|
Cow::from("Example Analyzer")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_description(&self) -> Cow<str> {
|
||||||
|
Cow::from("Always returns true, if you are seeing this you are either a developer or you are about to have problems.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||||
|
self.count += 1;
|
||||||
|
if self.count % 100 == 0 {
|
||||||
|
return Some(Event {
|
||||||
|
event_type: EventType::Informational,
|
||||||
|
message: "multiple of 100 events processed".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let 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;
|
||||||
|
};
|
||||||
|
for record in &paging.paging_record_list.as_ref()?.0 {
|
||||||
|
if let PagingUE_Identity::S_TMSI(_) = record.ue_identity {
|
||||||
|
return Some(Event {
|
||||||
|
event_type: EventType::QualitativeWarning {
|
||||||
|
severity: Severity::Low,
|
||||||
|
},
|
||||||
|
message: "TMSI was provided to cell".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
use thiserror::Error;
|
|
||||||
use rayhunter::diag_device::DiagDeviceError;
|
use rayhunter::diag_device::DiagDeviceError;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::qmdl_store::RecordingStoreError;
|
use crate::qmdl_store::RecordingStoreError;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum RayhunterError{
|
pub enum RayhunterError {
|
||||||
#[error("Config file parsing error: {0}")]
|
#[error("Config file parsing error: {0}")]
|
||||||
ConfigFileParsingError(#[from] toml::de::Error),
|
ConfigFileParsingError(#[from] toml::de::Error),
|
||||||
#[error("Diag intialization error: {0}")]
|
#[error("Diag intialization error: {0}")]
|
||||||
@@ -13,6 +13,6 @@ pub enum RayhunterError{
|
|||||||
TokioError(#[from] tokio::io::Error),
|
TokioError(#[from] tokio::io::Error),
|
||||||
#[error("QmdlStore error: {0}")]
|
#[error("QmdlStore error: {0}")]
|
||||||
QmdlStoreError(#[from] RecordingStoreError),
|
QmdlStoreError(#[from] RecordingStoreError),
|
||||||
#[error("No QMDL store found at path {0}, but can't create a new one due to readonly mode")]
|
#[error("No QMDL store found at path {0}, but can't create a new one due to debug mode")]
|
||||||
NoStoreReadonlyMode(String),
|
NoStoreDebugMode(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +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)]
|
|
||||||
pub enum Color565 {
|
|
||||||
Red = 0b1111100000000000,
|
|
||||||
Green = 0b0000011111100000,
|
|
||||||
Blue = 0b0000000000011111,
|
|
||||||
White = 0b1111111111111111,
|
|
||||||
Black = 0b0000000000000000,
|
|
||||||
Cyan = 0b0000011111111111,
|
|
||||||
Yellow = 0b1111111111100000,
|
|
||||||
Pink = 0b1111010010011111,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+36
-22
@@ -1,36 +1,43 @@
|
|||||||
use crate::ServerState;
|
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::diag::DataType;
|
||||||
use rayhunter::gsmtap_parser;
|
use rayhunter::gsmtap_parser;
|
||||||
use rayhunter::pcap::GsmtapPcapWriter;
|
use rayhunter::pcap::GsmtapPcapWriter;
|
||||||
use rayhunter::qmdl::QmdlReader;
|
use rayhunter::qmdl::QmdlReader;
|
||||||
use axum::body::Body;
|
use std::sync::Arc;
|
||||||
use axum::http::header::CONTENT_TYPE;
|
use std::{future, pin::pin};
|
||||||
use axum::extract::{State, Path};
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::response::{Response, IntoResponse};
|
|
||||||
use tokio::io::duplex;
|
use tokio::io::duplex;
|
||||||
use tokio_util::io::ReaderStream;
|
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
|
// 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
|
// 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.
|
// pcap data to a channel that's piped to the client.
|
||||||
pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
pub async fn get_pcap(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
Path(qmdl_name): Path<String>,
|
||||||
|
) -> Result<Response, (StatusCode, String)> {
|
||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let entry = qmdl_store.entry_for_name(&qmdl_name)
|
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name).ok_or((
|
||||||
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
|
StatusCode::NOT_FOUND,
|
||||||
|
format!("couldn't find qmdl file with name {}", qmdl_name),
|
||||||
|
))?;
|
||||||
if entry.qmdl_size_bytes == 0 {
|
if entry.qmdl_size_bytes == 0 {
|
||||||
return Err((
|
return Err((
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
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).await
|
let qmdl_file = qmdl_store
|
||||||
|
.open_entry_qmdl(entry_index)
|
||||||
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)))?;
|
||||||
// the QMDL reader should stop at the last successfully written data chunk
|
// the QMDL reader should stop at the last successfully written data chunk
|
||||||
// (entry.size_bytes)
|
// (entry.size_bytes)
|
||||||
@@ -39,21 +46,28 @@ pub async fn get_pcap(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
|
|||||||
pcap_writer.write_iface_header().await.unwrap();
|
pcap_writer.write_iface_header().await.unwrap();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut reader = QmdlReader::new(qmdl_file, Some(entry.qmdl_size_bytes));
|
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
|
||||||
let mut messages_stream = pin!(reader.as_stream()
|
let mut messages_stream = pin!(reader
|
||||||
|
.as_stream()
|
||||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||||
|
|
||||||
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() {
|
for maybe_msg in container.into_messages() {
|
||||||
match maybe_msg {
|
match maybe_msg {
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)
|
let maybe_gsmtap_msg =
|
||||||
.expect("error parsing gsmtap message");
|
gsmtap_parser::parse(msg).expect("error parsing gsmtap message");
|
||||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
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");
|
.expect("error writing pcap packet");
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => error!("error parsing message: {:?}", e),
|
Err(e) => error!("error parsing message: {:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+230
-62
@@ -1,17 +1,25 @@
|
|||||||
use std::path::{PathBuf, Path};
|
|
||||||
use thiserror::Error;
|
|
||||||
use tokio::{fs::{self, File, try_exists}, io::AsyncWriteExt};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
|
use rayhunter::util::RuntimeMetadata;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::{
|
||||||
|
fs::{self, try_exists, File, OpenOptions},
|
||||||
|
io::AsyncWriteExt,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum RecordingStoreError {
|
pub enum RecordingStoreError {
|
||||||
#[error("Can't close an entry when there's no current entry")]
|
#[error("Can't close an entry when there's no current entry")]
|
||||||
NoCurrentEntry,
|
NoCurrentEntry,
|
||||||
|
#[error("An entry with that name doesn't exist")]
|
||||||
|
NoSuchEntryError,
|
||||||
#[error("Couldn't create file: {0}")]
|
#[error("Couldn't create file: {0}")]
|
||||||
CreateFileError(tokio::io::Error),
|
CreateFileError(tokio::io::Error),
|
||||||
#[error("Couldn't read file: {0}")]
|
#[error("Couldn't read file: {0}")]
|
||||||
ReadFileError(tokio::io::Error),
|
ReadFileError(tokio::io::Error),
|
||||||
|
#[error("Couldn't delete file: {0}")]
|
||||||
|
DeleteFileError(tokio::io::Error),
|
||||||
#[error("Couldn't open directory at path: {0}")]
|
#[error("Couldn't open directory at path: {0}")]
|
||||||
OpenDirError(tokio::io::Error),
|
OpenDirError(tokio::io::Error),
|
||||||
#[error("Couldn't read manifest file: {0}")]
|
#[error("Couldn't read manifest file: {0}")]
|
||||||
@@ -19,7 +27,7 @@ pub enum RecordingStoreError {
|
|||||||
#[error("Couldn't write manifest file: {0}")]
|
#[error("Couldn't write manifest file: {0}")]
|
||||||
WriteManifestError(tokio::io::Error),
|
WriteManifestError(tokio::io::Error),
|
||||||
#[error("Couldn't parse QMDL store manifest file: {0}")]
|
#[error("Couldn't parse QMDL store manifest file: {0}")]
|
||||||
ParseManifestError(toml::de::Error)
|
ParseManifestError(toml::de::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RecordingStore {
|
pub struct RecordingStore {
|
||||||
@@ -40,17 +48,24 @@ pub struct ManifestEntry {
|
|||||||
pub last_message_time: Option<DateTime<Local>>,
|
pub last_message_time: Option<DateTime<Local>>,
|
||||||
pub qmdl_size_bytes: usize,
|
pub qmdl_size_bytes: usize,
|
||||||
pub analysis_size_bytes: usize,
|
pub analysis_size_bytes: usize,
|
||||||
|
pub rayhunter_version: Option<String>,
|
||||||
|
pub system_os: Option<String>,
|
||||||
|
pub arch: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManifestEntry {
|
impl ManifestEntry {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
|
let metadata = RuntimeMetadata::new();
|
||||||
ManifestEntry {
|
ManifestEntry {
|
||||||
name: format!("{}", now.timestamp()),
|
name: format!("{}", now.timestamp()),
|
||||||
start_time: now,
|
start_time: now,
|
||||||
last_message_time: None,
|
last_message_time: None,
|
||||||
qmdl_size_bytes: 0,
|
qmdl_size_bytes: 0,
|
||||||
analysis_size_bytes: 0,
|
analysis_size_bytes: 0,
|
||||||
|
rayhunter_version: Some(metadata.rayhunter_version),
|
||||||
|
system_os: Some(metadata.system_os),
|
||||||
|
arch: Some(metadata.arch),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,16 +85,26 @@ impl ManifestEntry {
|
|||||||
impl RecordingStore {
|
impl RecordingStore {
|
||||||
// Returns whether a directory with a "manifest.toml" exists at the given
|
// Returns whether a directory with a "manifest.toml" exists at the given
|
||||||
// path (though doesn't check if that manifest is valid)
|
// path (though doesn't check if that manifest is valid)
|
||||||
pub async fn exists<P>(path: P) -> Result<bool, RecordingStoreError> where P: AsRef<Path> {
|
pub async fn exists<P>(path: P) -> Result<bool, RecordingStoreError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
let manifest_path = path.as_ref().join("manifest.toml");
|
let manifest_path = path.as_ref().join("manifest.toml");
|
||||||
let dir_exists = try_exists(path).await.map_err(RecordingStoreError::OpenDirError)?;
|
let dir_exists = try_exists(path)
|
||||||
let manifest_exists = try_exists(manifest_path).await.map_err(RecordingStoreError::ReadManifestError)?;
|
.await
|
||||||
|
.map_err(RecordingStoreError::OpenDirError)?;
|
||||||
|
let manifest_exists = try_exists(manifest_path)
|
||||||
|
.await
|
||||||
|
.map_err(RecordingStoreError::ReadManifestError)?;
|
||||||
Ok(dir_exists && manifest_exists)
|
Ok(dir_exists && manifest_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads an existing RecordingStore at the given path. Errors if no store exists,
|
// Loads an existing RecordingStore at the given path. Errors if no store exists,
|
||||||
// or if it's malformed.
|
// or if it's malformed.
|
||||||
pub async fn load<P>(path: P) -> Result<Self, RecordingStoreError> where P: AsRef<Path> {
|
pub async fn load<P>(path: P) -> Result<Self, RecordingStoreError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
let path: PathBuf = path.as_ref().to_path_buf();
|
let path: PathBuf = path.as_ref().to_path_buf();
|
||||||
let manifest = RecordingStore::read_manifest(&path).await?;
|
let manifest = RecordingStore::read_manifest(&path).await?;
|
||||||
Ok(RecordingStore {
|
Ok(RecordingStore {
|
||||||
@@ -91,26 +116,35 @@ impl RecordingStore {
|
|||||||
|
|
||||||
// Creates a new RecordingStore at the given path. This involves creating a dir
|
// Creates a new RecordingStore at the given path. This involves creating a dir
|
||||||
// and writing an empty manifest.
|
// and writing an empty manifest.
|
||||||
pub async fn create<P>(path: P) -> Result<Self, RecordingStoreError> where P: AsRef<Path> {
|
pub async fn create<P>(path: P) -> Result<Self, RecordingStoreError>
|
||||||
let manifest_path = path.as_ref().join("manifest.toml");
|
where
|
||||||
fs::create_dir_all(&path).await
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
fs::create_dir_all(&path)
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::OpenDirError)?;
|
.map_err(RecordingStoreError::OpenDirError)?;
|
||||||
let mut manifest_file = File::create(&manifest_path).await
|
|
||||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
let mut store = RecordingStore {
|
||||||
let empty_manifest = Manifest { entries: Vec::new() };
|
path: path.as_ref().to_owned(),
|
||||||
let empty_manifest_contents = toml::to_string_pretty(&empty_manifest)
|
manifest: Manifest {
|
||||||
.expect("failed to serialize manifest");
|
entries: Vec::new(),
|
||||||
manifest_file.write_all(empty_manifest_contents.as_bytes()).await
|
},
|
||||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
current_entry: None,
|
||||||
RecordingStore::load(path).await
|
};
|
||||||
|
|
||||||
|
store.write_manifest().await?;
|
||||||
|
Ok(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError> where P: AsRef<Path> {
|
async fn read_manifest<P>(path: P) -> Result<Manifest, RecordingStoreError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
let manifest_path = path.as_ref().join("manifest.toml");
|
let manifest_path = path.as_ref().join("manifest.toml");
|
||||||
let file_contents = fs::read_to_string(&manifest_path).await
|
let file_contents = fs::read_to_string(&manifest_path)
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::ReadManifestError)?;
|
.map_err(RecordingStoreError::ReadManifestError)?;
|
||||||
toml::from_str(&file_contents)
|
toml::from_str(&file_contents).map_err(RecordingStoreError::ParseManifestError)
|
||||||
.map_err(RecordingStoreError::ParseManifestError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closes the current entry (if needed), creates a new entry based on the
|
// Closes the current entry (if needed), creates a new entry based on the
|
||||||
@@ -123,16 +157,12 @@ impl RecordingStore {
|
|||||||
}
|
}
|
||||||
let new_entry = ManifestEntry::new();
|
let new_entry = ManifestEntry::new();
|
||||||
let qmdl_filepath = new_entry.get_qmdl_filepath(&self.path);
|
let qmdl_filepath = new_entry.get_qmdl_filepath(&self.path);
|
||||||
let qmdl_file = File::options()
|
let qmdl_file = File::create(&qmdl_filepath)
|
||||||
.create(true)
|
.await
|
||||||
.write(true)
|
|
||||||
.open(&qmdl_filepath).await
|
|
||||||
.map_err(RecordingStoreError::CreateFileError)?;
|
.map_err(RecordingStoreError::CreateFileError)?;
|
||||||
let analysis_filepath = new_entry.get_analysis_filepath(&self.path);
|
let analysis_filepath = new_entry.get_analysis_filepath(&self.path);
|
||||||
let analysis_file = File::options()
|
let analysis_file = File::create(&analysis_filepath)
|
||||||
.create(true)
|
.await
|
||||||
.write(true)
|
|
||||||
.open(&analysis_filepath).await
|
|
||||||
.map_err(RecordingStoreError::CreateFileError)?;
|
.map_err(RecordingStoreError::CreateFileError)?;
|
||||||
self.manifest.entries.push(new_entry);
|
self.manifest.entries.push(new_entry);
|
||||||
self.current_entry = Some(self.manifest.entries.len() - 1);
|
self.current_entry = Some(self.manifest.entries.len() - 1);
|
||||||
@@ -141,74 +171,166 @@ impl RecordingStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns the corresponding QMDL file for a given entry
|
// Returns the corresponding QMDL file for a given entry
|
||||||
pub async fn open_entry_qmdl(&self, entry: &ManifestEntry) -> Result<File, RecordingStoreError> {
|
pub async fn open_entry_qmdl(&self, entry_index: usize) -> Result<File, RecordingStoreError> {
|
||||||
File::open(entry.get_qmdl_filepath(&self.path)).await
|
let entry = &self.manifest.entries[entry_index];
|
||||||
|
File::open(entry.get_qmdl_filepath(&self.path))
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::ReadFileError)
|
.map_err(RecordingStoreError::ReadFileError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the corresponding QMDL file for a given entry
|
// Returns the corresponding QMDL file for a given entry
|
||||||
pub async fn open_entry_analysis(&self, entry: &ManifestEntry) -> Result<File, RecordingStoreError> {
|
pub async fn open_entry_analysis(
|
||||||
File::open(entry.get_analysis_filepath(&self.path)).await
|
&self,
|
||||||
|
entry_index: usize,
|
||||||
|
) -> Result<File, RecordingStoreError> {
|
||||||
|
let entry = &self.manifest.entries[entry_index];
|
||||||
|
File::open(entry.get_analysis_filepath(&self.path))
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::ReadFileError)
|
.map_err(RecordingStoreError::ReadFileError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn clear_and_open_entry_analysis(
|
||||||
|
&mut self,
|
||||||
|
entry_index: usize,
|
||||||
|
) -> Result<File, RecordingStoreError> {
|
||||||
|
let entry = &self.manifest.entries[entry_index];
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(entry.get_analysis_filepath(&self.path))
|
||||||
|
.await
|
||||||
|
.map_err(RecordingStoreError::ReadFileError)?;
|
||||||
|
self.update_entry_analysis_size(entry_index, 0).await?;
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
|
||||||
// Unsets the current entry
|
// Unsets the current entry
|
||||||
pub async fn close_current_entry(&mut self) -> Result<(), RecordingStoreError> {
|
pub async fn close_current_entry(&mut self) -> Result<(), RecordingStoreError> {
|
||||||
match self.current_entry {
|
match self.current_entry {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
self.current_entry = None;
|
self.current_entry = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
None => Err(RecordingStoreError::NoCurrentEntry)
|
None => Err(RecordingStoreError::NoCurrentEntry),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the given entry's size and updates the last_message_time to now, updating the manifest
|
// Sets the given entry's size and updates the last_message_time to now, updating the manifest
|
||||||
pub async fn update_entry_qmdl_size(&mut self, entry_index: usize, size_bytes: usize) -> Result<(), RecordingStoreError> {
|
pub async fn update_entry_qmdl_size(
|
||||||
|
&mut self,
|
||||||
|
entry_index: usize,
|
||||||
|
size_bytes: usize,
|
||||||
|
) -> Result<(), RecordingStoreError> {
|
||||||
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
|
self.manifest.entries[entry_index].qmdl_size_bytes = size_bytes;
|
||||||
self.manifest.entries[entry_index].last_message_time = Some(Local::now());
|
self.manifest.entries[entry_index].last_message_time = Some(Local::now());
|
||||||
self.write_manifest().await
|
self.write_manifest().await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the given entry's analysis file size
|
// Sets the given entry's analysis file size
|
||||||
pub async fn update_entry_analysis_size(&mut self, entry_index: usize, size_bytes: usize) -> Result<(), RecordingStoreError> {
|
pub async fn update_entry_analysis_size(
|
||||||
|
&mut self,
|
||||||
|
entry_index: usize,
|
||||||
|
size_bytes: usize,
|
||||||
|
) -> Result<(), RecordingStoreError> {
|
||||||
self.manifest.entries[entry_index].analysis_size_bytes = size_bytes;
|
self.manifest.entries[entry_index].analysis_size_bytes = size_bytes;
|
||||||
self.write_manifest().await
|
self.write_manifest().await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
|
async fn write_manifest(&mut self) -> Result<(), RecordingStoreError> {
|
||||||
let mut manifest_file = File::options()
|
let tmp_path = self.path.join("manifest.toml.new");
|
||||||
.write(true)
|
let mut manifest_tmp_file = File::create(&tmp_path)
|
||||||
.open(self.path.join("manifest.toml")).await
|
.await
|
||||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||||
let manifest_contents = toml::to_string_pretty(&self.manifest)
|
|
||||||
.expect("failed to serialize manifest");
|
let manifest_contents =
|
||||||
manifest_file.write_all(manifest_contents.as_bytes()).await
|
toml::to_string_pretty(&self.manifest).expect("failed to serialize manifest");
|
||||||
|
manifest_tmp_file
|
||||||
|
.write_all(manifest_contents.as_bytes())
|
||||||
|
.await
|
||||||
.map_err(RecordingStoreError::WriteManifestError)?;
|
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||||
|
|
||||||
|
fs::rename(tmp_path, self.path.join("manifest.toml"))
|
||||||
|
.await
|
||||||
|
.map_err(RecordingStoreError::WriteManifestError)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds an entry by filename
|
// Finds an entry by filename
|
||||||
pub fn entry_for_name(&self, name: &str) -> Option<ManifestEntry> {
|
pub fn entry_for_name(&self, name: &str) -> Option<(usize, &ManifestEntry)> {
|
||||||
self.manifest.entries.iter()
|
let entry_index = self
|
||||||
.find(|entry| entry.name == name)
|
.manifest
|
||||||
.cloned()
|
.entries
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.name == name)?;
|
||||||
|
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_entry(&self) -> Option<&ManifestEntry> {
|
pub fn get_current_entry(&self) -> Option<(usize, &ManifestEntry)> {
|
||||||
let entry_index = self.current_entry?;
|
let entry_index = self.current_entry?;
|
||||||
self.manifest.entries.get(entry_index)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use tempdir::TempDir;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use tempfile::{Builder, TempDir};
|
||||||
|
|
||||||
|
fn make_temp_dir() -> TempDir {
|
||||||
|
Builder::new().prefix("qmdl_store_test").tempdir().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_load_from_empty_dir() {
|
async fn test_load_from_empty_dir() {
|
||||||
let dir = TempDir::new("qmdl_store_test").unwrap();
|
let dir = make_temp_dir();
|
||||||
assert!(!RecordingStore::exists(dir.path()).await.unwrap());
|
assert!(!RecordingStore::exists(dir.path()).await.unwrap());
|
||||||
let _created_store = RecordingStore::create(dir.path()).await.unwrap();
|
let _created_store = RecordingStore::create(dir.path()).await.unwrap();
|
||||||
assert!(RecordingStore::exists(dir.path()).await.unwrap());
|
assert!(RecordingStore::exists(dir.path()).await.unwrap());
|
||||||
@@ -218,26 +340,56 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_creating_updating_and_closing_entries() {
|
async fn test_creating_updating_and_closing_entries() {
|
||||||
let dir = TempDir::new("qmdl_store_test").unwrap();
|
let dir = make_temp_dir();
|
||||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||||
let _ = store.new_entry().await.unwrap();
|
let _ = store.new_entry().await.unwrap();
|
||||||
let entry_index = store.current_entry.unwrap();
|
let entry_index = store.current_entry.unwrap();
|
||||||
assert_eq!(RecordingStore::read_manifest(dir.path()).await.unwrap(), store.manifest);
|
assert_eq!(
|
||||||
assert!(store.manifest.entries[entry_index].last_message_time.is_none());
|
RecordingStore::read_manifest(dir.path()).await.unwrap(),
|
||||||
|
store.manifest
|
||||||
|
);
|
||||||
|
assert!(store.manifest.entries[entry_index]
|
||||||
|
.last_message_time
|
||||||
|
.is_none());
|
||||||
|
|
||||||
store.update_entry_qmdl_size(entry_index, 1000).await.unwrap();
|
store
|
||||||
let entry = store.entry_for_name(&store.manifest.entries[entry_index].name).unwrap();
|
.update_entry_qmdl_size(entry_index, 1000)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let (entry_index, entry) = store
|
||||||
|
.entry_for_name(&store.manifest.entries[entry_index].name)
|
||||||
|
.unwrap();
|
||||||
assert!(entry.last_message_time.is_some());
|
assert!(entry.last_message_time.is_some());
|
||||||
assert_eq!(store.manifest.entries[entry_index].qmdl_size_bytes, 1000);
|
assert_eq!(store.manifest.entries[entry_index].qmdl_size_bytes, 1000);
|
||||||
assert_eq!(RecordingStore::read_manifest(dir.path()).await.unwrap(), store.manifest);
|
assert_eq!(
|
||||||
|
RecordingStore::read_manifest(dir.path()).await.unwrap(),
|
||||||
|
store.manifest
|
||||||
|
);
|
||||||
|
|
||||||
store.close_current_entry().await.unwrap();
|
store.close_current_entry().await.unwrap();
|
||||||
assert!(matches!(store.close_current_entry().await, Err(RecordingStoreError::NoCurrentEntry)));
|
assert!(matches!(
|
||||||
|
store.close_current_entry().await,
|
||||||
|
Err(RecordingStoreError::NoCurrentEntry)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
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]
|
#[tokio::test]
|
||||||
async fn test_repeated_new_entries() {
|
async fn test_repeated_new_entries() {
|
||||||
let dir = TempDir::new("qmdl_store_test").unwrap();
|
let dir = make_temp_dir();
|
||||||
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
let mut store = RecordingStore::create(dir.path()).await.unwrap();
|
||||||
let _ = store.new_entry().await.unwrap();
|
let _ = store.new_entry().await.unwrap();
|
||||||
let entry_index = store.current_entry.unwrap();
|
let entry_index = store.current_entry.unwrap();
|
||||||
@@ -246,4 +398,20 @@ mod tests {
|
|||||||
assert_ne!(entry_index, new_entry_index);
|
assert_ne!(entry_index, new_entry_index);
|
||||||
assert_eq!(store.manifest.entries.len(), 2);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-15
@@ -1,35 +1,53 @@
|
|||||||
use axum::body::Body;
|
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::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::io::AsyncReadExt;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use include_dir::{include_dir, Dir};
|
|
||||||
|
|
||||||
use crate::DiagDeviceCtrlMessage;
|
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||||
use crate::qmdl_store::RecordingStore;
|
use crate::qmdl_store::RecordingStore;
|
||||||
|
use crate::{display, DiagDeviceCtrlMessage};
|
||||||
|
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||||
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
||||||
pub readonly_mode: bool
|
pub ui_update_sender: Sender<display::DisplayState>,
|
||||||
|
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||||
|
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||||
|
pub debug_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Path<String>) -> Result<Response, (StatusCode, String)> {
|
pub async fn get_qmdl(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
Path(qmdl_name): Path<String>,
|
||||||
|
) -> Result<Response, (StatusCode, String)> {
|
||||||
|
let qmdl_idx = qmdl_name.trim_end_matches(".qmdl");
|
||||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let entry = qmdl_store.entry_for_name(&qmdl_name)
|
let (entry_index, entry) = qmdl_store.entry_for_name(qmdl_idx).ok_or((
|
||||||
.ok_or((StatusCode::NOT_FOUND, format!("couldn't find qmdl file with name {}", qmdl_name)))?;
|
StatusCode::NOT_FOUND,
|
||||||
let qmdl_file = qmdl_store.open_entry_qmdl(&entry).await
|
format!("couldn't find qmdl file with name {}", qmdl_idx),
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("error opening QMDL file: {}", e)))?;
|
))?;
|
||||||
|
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 limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
|
||||||
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
|
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
|
||||||
|
|
||||||
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);
|
let body = Body::from_stream(qmdl_stream);
|
||||||
Ok((headers, body).into_response())
|
Ok((headers, body).into_response())
|
||||||
}
|
}
|
||||||
@@ -37,10 +55,44 @@ pub async fn get_qmdl(State(state): State<Arc<ServerState>>, Path(qmdl_name): Pa
|
|||||||
// Bundles the server's static files (html/css/js) into the binary for easy distribution
|
// Bundles the server's static files (html/css/js) into the binary for easy distribution
|
||||||
static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static");
|
static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static");
|
||||||
|
|
||||||
pub async fn serve_static(Path(path): Path<String>) -> impl IntoResponse {
|
pub async fn serve_static(
|
||||||
|
State(state): State<Arc<ServerState>>,
|
||||||
|
Path(path): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
let path = path.trim_start_matches('/');
|
let path = path.trim_start_matches('/');
|
||||||
let mime_type = mime_guess::from_path(path).first_or_text_plain();
|
let mime_type = mime_guess::from_path(path).first_or_text_plain();
|
||||||
|
|
||||||
|
// if we're in debug mode, return the files from the build directory so we
|
||||||
|
// don't have to rebuild every time the JS/HTML change
|
||||||
|
if state.debug_mode {
|
||||||
|
let mut build_path = std::path::PathBuf::new();
|
||||||
|
build_path.push("bin");
|
||||||
|
build_path.push("static");
|
||||||
|
for part in path.split("/") {
|
||||||
|
build_path.push(part);
|
||||||
|
}
|
||||||
|
return match File::open(build_path).await {
|
||||||
|
Ok(mut file) => {
|
||||||
|
let mut body = String::new();
|
||||||
|
file.read_to_string(&mut body)
|
||||||
|
.await
|
||||||
|
.expect("failed to read file");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(mime_type.as_ref()).unwrap(),
|
||||||
|
)
|
||||||
|
.body(Body::from(body))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(_) => Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
match STATIC_DIR.get_file(path) {
|
match STATIC_DIR.get_file(path) {
|
||||||
None => Response::builder()
|
None => Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
|||||||
+20
-9
@@ -3,9 +3,9 @@ use std::sync::Arc;
|
|||||||
use crate::qmdl_store::ManifestEntry;
|
use crate::qmdl_store::ManifestEntry;
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
|
|
||||||
use axum::Json;
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::Json;
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
@@ -65,10 +65,16 @@ pub struct MemoryStats {
|
|||||||
// runs the given command and returns its stdout as a string
|
// runs the given command and returns its stdout as a string
|
||||||
async fn get_cmd_output(mut cmd: Command) -> Result<String, String> {
|
async fn get_cmd_output(mut cmd: Command) -> Result<String, String> {
|
||||||
let cmd_str = format!("{:?}", &cmd);
|
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))?;
|
.map_err(|e| format!("error running command {}: {}", &cmd_str, e))?;
|
||||||
if !output.status.success() {
|
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())
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
}
|
}
|
||||||
@@ -79,7 +85,8 @@ impl MemoryStats {
|
|||||||
let mut free_cmd = Command::new("free");
|
let mut free_cmd = Command::new("free");
|
||||||
free_cmd.arg("-k");
|
free_cmd.arg("-k");
|
||||||
let stdout = get_cmd_output(free_cmd).await?;
|
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>());
|
.flat_map(|part| part.parse::<usize>());
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
total: humanize_kb(numbers.next().ok_or("error parsing free output")?),
|
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")
|
// turns a number of kilobytes (like 28293) into a human-readable string (like "28.3M")
|
||||||
fn humanize_kb(kb: usize) -> String {
|
fn humanize_kb(kb: usize) -> String {
|
||||||
if kb < 1000{
|
if kb < 1000 {
|
||||||
return format!("{}K", kb);
|
return format!("{}K", kb);
|
||||||
}
|
}
|
||||||
format!("{:.1}M", kb as f64 / 1024.0)
|
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;
|
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
match SystemStats::new(qmdl_store.path.to_str().unwrap()).await {
|
match SystemStats::new(qmdl_store.path.to_str().unwrap()).await {
|
||||||
Ok(stats) => Ok(Json(stats)),
|
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);
|
error!("error getting system stats: {}", err);
|
||||||
Err((
|
Err((
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
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 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 qmdl_store = state.qmdl_store_lock.read().await;
|
||||||
let mut entries = qmdl_store.manifest.entries.clone();
|
let mut entries = qmdl_store.manifest.entries.clone();
|
||||||
let current_entry = qmdl_store.current_entry.map(|index| entries.remove(index));
|
let current_entry = qmdl_store.current_entry.map(|index| entries.remove(index));
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ th[scope='row'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr.current {
|
tr.current {
|
||||||
|
background-color: #53fe7b;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.warning {
|
||||||
background-color: #fe537b;
|
background-color: #fe537b;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>rayhunter</title>
|
<title>rayhunter</title>
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<button onclick="startRecording()">Start Recording</button>
|
<button onclick="startRecording()">Start Recording</button>
|
||||||
<button onclick="stopRecording()">Stop Recording</button>
|
<button onclick="stopRecording()">Stop Recording</button>
|
||||||
|
<button onclick="deleteAllRecodings()">Delete All Recordings</button>
|
||||||
</div>
|
</div>
|
||||||
<table id="qmdl-manifest-table">
|
<table id="qmdl-manifest-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -27,15 +29,17 @@
|
|||||||
<th scope="col">Size (bytes)</th>
|
<th scope="col">Size (bytes)</th>
|
||||||
<th scope="col">PCAP</th>
|
<th scope="col">PCAP</th>
|
||||||
<th scope="col">QMDL</th>
|
<th scope="col">QMDL</th>
|
||||||
|
<th scope="col">Analysis Result</th>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
<div>
|
<div>
|
||||||
<h3>System stats</h3>
|
<h3>Live System stats</h3>
|
||||||
<pre id="system-stats">Loading...</pre>
|
<pre id="system-stats">Loading...</pre>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Analysis Report</h3>
|
<h3>Analysis Report of Current Capture</h3>
|
||||||
<pre id="analysis-report">Loading...</pre>
|
<pre id="analysis-report">Loading...</pre>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+156
-27
@@ -1,16 +1,107 @@
|
|||||||
|
const STATUS_RUNNING = 'running';
|
||||||
|
const STATUS_QUEUED = 'queued';
|
||||||
|
const STATUS_NEEDS_UPDATE = 'needs-update';
|
||||||
|
const STATUS_COMPLETE = 'complete';
|
||||||
|
|
||||||
async function populateDivs() {
|
async function populateDivs() {
|
||||||
const systemStats = await getSystemStats();
|
const systemStats = await getSystemStats();
|
||||||
const systemStatsDiv = document.getElementById('system-stats');
|
const systemStatsDiv = document.getElementById('system-stats');
|
||||||
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
|
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
|
||||||
|
|
||||||
const analysisReport = await getAnalysisReport();
|
|
||||||
const analysisReportDiv = document.getElementById('analysis-report');
|
const analysisReportDiv = document.getElementById('analysis-report');
|
||||||
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2);
|
try {
|
||||||
|
const analysisReport = await getAnalysisReport('live');
|
||||||
|
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
analysisReportDiv.innerHTML = e.toString();
|
||||||
|
}
|
||||||
|
|
||||||
const qmdlManifest = await getQmdlManifest();
|
const qmdlManifest = await getQmdlManifest();
|
||||||
|
await updateAnalysisStatus(qmdlManifest);
|
||||||
|
await updateAnalysisResults(qmdlManifest);
|
||||||
updateQmdlManifestTable(qmdlManifest);
|
updateQmdlManifestTable(qmdlManifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setStatus(qmdlManifest, name, status) {
|
||||||
|
// ignore qmdlManifest.current_entry, it's always running
|
||||||
|
for (const entry of qmdlManifest.entries) {
|
||||||
|
if (entry.name === name) {
|
||||||
|
entry['status'] = status;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAnalysisStatus(qmdlManifest) {
|
||||||
|
const status = JSON.parse(await req('GET', '/api/analysis'));
|
||||||
|
if (status.running) {
|
||||||
|
setStatus(qmdlManifest, status.running, STATUS_RUNNING);
|
||||||
|
}
|
||||||
|
for (const queued in status.queued) {
|
||||||
|
setStatus(qmdlManifest, queued, STATUS_QUEUED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNewlineDelimitedJSON(inputStr) {
|
||||||
|
const lines = inputStr.split('\n');
|
||||||
|
const result = [];
|
||||||
|
let currentLine = '';
|
||||||
|
while (lines.length > 0) {
|
||||||
|
currentLine += lines.shift();
|
||||||
|
try {
|
||||||
|
const entry = JSON.parse(currentLine);
|
||||||
|
result.push(entry);
|
||||||
|
currentLine = '';
|
||||||
|
// if this chunk wasn't valid JSON, there was an escaped newline in the
|
||||||
|
// JSON line, so simply continue to the next one
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEntryAnalysisResult(entry) {
|
||||||
|
entry.analysis = {
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
const report = parseNewlineDelimitedJSON(await req('GET', `/api/analysis-report/${entry.name}`));
|
||||||
|
for (const row of report) {
|
||||||
|
if (row["analysis"]) {
|
||||||
|
const timestamp = new Date(row["timestamp"]);
|
||||||
|
const analysis = row["analysis"];
|
||||||
|
for (const warning of analysis) {
|
||||||
|
entry.analysis.warnings.push({
|
||||||
|
timestamp,
|
||||||
|
warning,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (entry.analysis.warnings.length === 0) {
|
||||||
|
entry.analysis_result = `0 warnings!`;
|
||||||
|
} else {
|
||||||
|
entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`;
|
||||||
|
for (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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAnalysisResults(qmdlManifest) {
|
||||||
|
if (qmdlManifest.current_entry) {
|
||||||
|
await updateEntryAnalysisResult(qmdlManifest.current_entry);
|
||||||
|
}
|
||||||
|
for (const entry of qmdlManifest.entries) {
|
||||||
|
if (entry.status === STATUS_NEEDS_UPDATE) {
|
||||||
|
await updateEntryAnalysisResult(entry);
|
||||||
|
entry.status = STATUS_COMPLETE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateQmdlManifestTable(manifest) {
|
function updateQmdlManifestTable(manifest) {
|
||||||
const table = document.getElementById('qmdl-manifest-table');
|
const table = document.getElementById('qmdl-manifest-table');
|
||||||
const numRows = table.rows.length;
|
const numRows = table.rows.length;
|
||||||
@@ -18,43 +109,69 @@ function updateQmdlManifestTable(manifest) {
|
|||||||
table.deleteRow(1);
|
table.deleteRow(1);
|
||||||
}
|
}
|
||||||
if (manifest.current_entry) {
|
if (manifest.current_entry) {
|
||||||
const row = createEntryRow(manifest.current_entry);
|
const row = createEntryRow(manifest.current_entry, true);
|
||||||
row.classList.add('current');
|
row.classList.add('current');
|
||||||
table.appendChild(row)
|
table.appendChild(row)
|
||||||
}
|
}
|
||||||
for (let entry of manifest.entries) {
|
for (let entry of manifest.entries) {
|
||||||
table.appendChild(createEntryRow(entry));
|
table.appendChild(createEntryRow(entry), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEntryRow(entry) {
|
function createLink(uri, text) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = uri;
|
||||||
|
link.innerText = text;
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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 row = document.createElement('tr');
|
||||||
const name = document.createElement('th');
|
const name = document.createElement('th');
|
||||||
name.scope = 'row';
|
name.scope = 'row';
|
||||||
name.innerText = entry.name;
|
name.innerText = entry.name;
|
||||||
row.appendChild(name);
|
row.appendChild(name);
|
||||||
|
|
||||||
for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) {
|
for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) {
|
||||||
const td = document.createElement('td');
|
const td = document.createElement('td');
|
||||||
td.innerText = entry[key];
|
td.innerText = entry[key];
|
||||||
row.appendChild(td);
|
row.appendChild(td);
|
||||||
}
|
}
|
||||||
const pcap_td = document.createElement('td');
|
|
||||||
const pcap_link = document.createElement('a');
|
const pcapTd = document.createElement('td');
|
||||||
pcap_link.href = `/api/pcap/${entry.name}`;
|
pcapTd.appendChild(createLink(`/api/pcap/${entry.name}`, 'pcap'));
|
||||||
pcap_link.innerText = 'pcap';
|
row.appendChild(pcapTd);
|
||||||
pcap_td.appendChild(pcap_link);
|
|
||||||
row.appendChild(pcap_td);
|
const qmdlTd = document.createElement('td');
|
||||||
const qmdl_td = document.createElement('td');
|
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}.qmdl`, 'qmdl'));
|
||||||
const qmdl_link = document.createElement('a');
|
row.appendChild(qmdlTd);
|
||||||
qmdl_link.href = `/api/qmdl/${entry.name}`;
|
|
||||||
qmdl_link.innerText = 'qmdl';
|
const analysisResult = document.createElement('td');
|
||||||
qmdl_td.appendChild(qmdl_link);
|
analysisResult.innerHTML = entry.analysis_result;
|
||||||
row.appendChild(qmdl_td);
|
if (entry.analysis.warnings.length > 0) {
|
||||||
|
row.classList.add("warning");
|
||||||
|
}
|
||||||
|
row.appendChild(analysisResult);
|
||||||
|
|
||||||
|
const actionsButtons = document.createElement('td');
|
||||||
|
actionsButtons.appendChild(createButton(`/api/delete-recording/${entry.name}`, 'Delete'));
|
||||||
|
row.appendChild(actionsButtons);
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAnalysisReport() {
|
async function getAnalysisReport(name) {
|
||||||
const rows = await req('GET', '/api/analysis-report');
|
const rows = await req('GET', `/api/analysis-report/${name}`);
|
||||||
return rows.split('\n')
|
return rows.split('\n')
|
||||||
.filter(row => row.length > 0)
|
.filter(row => row.length > 0)
|
||||||
.map(row => JSON.parse(row));
|
.map(row => JSON.parse(row));
|
||||||
@@ -67,22 +184,27 @@ async function getSystemStats() {
|
|||||||
async function getQmdlManifest() {
|
async function getQmdlManifest() {
|
||||||
const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest'));
|
const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest'));
|
||||||
if (manifest.current_entry) {
|
if (manifest.current_entry) {
|
||||||
manifest.current_entry.start_time = new Date(manifest.current_entry.start_time);
|
parseQmdlEntry(manifest.current_entry);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (entry of manifest.entries) {
|
for (entry of manifest.entries) {
|
||||||
entry.start_time = new Date(entry.start_time);
|
parseQmdlEntry(entry);
|
||||||
entry.last_message_time = new Date(entry.last_message_time);
|
|
||||||
}
|
}
|
||||||
// sort them in reverse chronological order
|
// sort them in reverse chronological order
|
||||||
manifest.entries.reverse();
|
manifest.entries.reverse();
|
||||||
return manifest;
|
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() {
|
async function startRecording() {
|
||||||
await req('POST', '/api/start-recording');
|
await req('POST', '/api/start-recording');
|
||||||
populateDivs();
|
populateDivs();
|
||||||
@@ -93,6 +215,13 @@ async function stopRecording() {
|
|||||||
populateDivs();
|
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) {
|
async function req(method, url) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
|
|||||||
Vendored
+10
-2
@@ -1,10 +1,18 @@
|
|||||||
# cat config.toml
|
# cat config.toml
|
||||||
qmdl_store_path = "/data/rayhunter/qmdl"
|
qmdl_store_path = "/data/rayhunter/qmdl"
|
||||||
port = 8080
|
port = 8080
|
||||||
readonly_mode = false
|
debug_mode = false
|
||||||
|
enable_dummy_analyzer = false
|
||||||
|
colorblind_mode = false
|
||||||
# UI Levels:
|
# UI Levels:
|
||||||
|
#
|
||||||
|
# Orbic and TP-Link with color display:
|
||||||
# 0 = invisible mode, no indicator that rayhunter is running
|
# 0 = invisible mode, no indicator that rayhunter is running
|
||||||
# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running
|
# 1 = Subtle mode, display a 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
|
# 2 = Demo Mode, display a fun orca gif
|
||||||
# 3 = display the EFF logo
|
# 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
|
ui_level = 1
|
||||||
|
|||||||
Vendored
-97
@@ -1,97 +0,0 @@
|
|||||||
#!/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
|
|
||||||
check_adb
|
|
||||||
force_debug_mode
|
|
||||||
setup_rootshell
|
|
||||||
setup_rayhunter
|
|
||||||
test_rayhunter
|
|
||||||
}
|
|
||||||
|
|
||||||
check_adb() {
|
|
||||||
if ! command -v adb &> /dev/null
|
|
||||||
then
|
|
||||||
echo "adb not found, please ensure it's installed or check the README.md"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
force_debug_mode() {
|
|
||||||
# Force a switch into the debug mode to enable ADB
|
|
||||||
"$SERIAL_PATH" AT
|
|
||||||
echo -n "adb enabled, waiting for reboot"
|
|
||||||
wait_for_adb_shell
|
|
||||||
echo "it's alive!"
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_adb_shell() {
|
|
||||||
until adb shell true 2> /dev/null
|
|
||||||
do
|
|
||||||
echo -n .
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_rootshell() {
|
|
||||||
_adb_push rootshell /tmp/
|
|
||||||
"$SERIAL_PATH" "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell"
|
|
||||||
sleep 1
|
|
||||||
"$SERIAL_PATH" "AT+SYSCMD=chown root /bin/rootshell"
|
|
||||||
sleep 1
|
|
||||||
"$SERIAL_PATH" "AT+SYSCMD=chmod 4755 /bin/rootshell"
|
|
||||||
echo "we have root!"
|
|
||||||
adb shell /bin/rootshell -c id
|
|
||||||
}
|
|
||||||
|
|
||||||
_adb_push() {
|
|
||||||
adb push "$(dirname "$0")/$1" "$2"
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_rayhunter() {
|
|
||||||
adb shell '/bin/rootshell -c "mkdir -p /data/rayhunter"'
|
|
||||||
_adb_push config.toml.example /data/rayhunter/config.toml
|
|
||||||
_adb_push rayhunter-daemon /data/rayhunter/
|
|
||||||
_adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon
|
|
||||||
_adb_push scripts/misc-daemon /tmp/misc-daemon
|
|
||||||
adb shell '/bin/rootshell -c "cp /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"'
|
|
||||||
adb shell '/bin/rootshell -c "cp /tmp/misc-daemon /etc/init.d/misc-daemon"'
|
|
||||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/rayhunter_daemon"'
|
|
||||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/misc-daemon"'
|
|
||||||
echo -n "rebooting, this may take a sec..."
|
|
||||||
adb shell '/bin/rootshell -c reboot'
|
|
||||||
|
|
||||||
# first wait for shutdown (it can take ~10s)
|
|
||||||
until ! adb shell true 2> /dev/null
|
|
||||||
do
|
|
||||||
echo -n '.'
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
# now wait for boot to finish
|
|
||||||
wait_for_adb_shell
|
|
||||||
|
|
||||||
echo "rebooted successfully!"
|
|
||||||
}
|
|
||||||
|
|
||||||
test_rayhunter() {
|
|
||||||
URL="http://localhost:8080"
|
|
||||||
adb forward tcp:8080 tcp:8080
|
|
||||||
echo -n "checking for rayhunter server..."
|
|
||||||
|
|
||||||
SECONDS=0
|
|
||||||
while (( SECONDS < 30 )); do
|
|
||||||
if curl -L --fail-with-body "$URL" -o /dev/null -s; then
|
|
||||||
echo
|
|
||||||
echo "success! you can access rayhunter at $URL"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
echo -n "."
|
|
||||||
done
|
|
||||||
echo "timeout reached! failed to reach rayhunter url $URL, something went wrong :("
|
|
||||||
}
|
|
||||||
Vendored
-6
@@ -1,6 +0,0 @@
|
|||||||
#!/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
export SERIAL_PATH="./serial-ubuntu-latest/serial"
|
|
||||||
. "$(dirname "$0")"/install-common.sh
|
|
||||||
install
|
|
||||||
Vendored
-6
@@ -1,6 +0,0 @@
|
|||||||
#!/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
export SERIAL_PATH="./serial-mac-latest/serial"
|
|
||||||
. "$(dirname "$0")"/install-common.sh
|
|
||||||
install
|
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
force_debug_mode() {
|
||||||
|
echo "Using adb at $ADB"
|
||||||
|
echo "Force a switch into the debug mode to enable ADB"
|
||||||
|
"$SERIAL_PATH" --root
|
||||||
|
echo -n "adb enabled, waiting for reboot..."
|
||||||
|
wait_for_adb_shell
|
||||||
|
echo " it's alive!"
|
||||||
|
echo -n "waiting for atfwd_daemon to startup..."
|
||||||
|
wait_for_atfwd_daemon
|
||||||
|
echo " done!"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_atfwd_daemon() {
|
||||||
|
until [ -n "$(_adb_shell 'pgrep atfwd_daemon')" ]
|
||||||
|
do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_adb_shell() {
|
||||||
|
until _adb_shell true 2> /dev/null
|
||||||
|
do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_rootshell() {
|
||||||
|
_adb_push rootshell /tmp/
|
||||||
|
_at_syscmd "cp /tmp/rootshell /bin/rootshell"
|
||||||
|
sleep 1
|
||||||
|
_at_syscmd "chown root /bin/rootshell"
|
||||||
|
sleep 1
|
||||||
|
_at_syscmd "chmod 4755 /bin/rootshell"
|
||||||
|
_adb_shell '/bin/rootshell -c id'
|
||||||
|
echo "we have root!"
|
||||||
|
}
|
||||||
|
|
||||||
|
_adb_push() {
|
||||||
|
"$ADB" push "$(dirname "$0")/$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
_adb_shell() {
|
||||||
|
"$ADB" shell "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
_at_syscmd() {
|
||||||
|
"$SERIAL_PATH" "AT+SYSCMD=$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
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-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"
|
||||||
|
_adb_push scripts/misc-daemon /tmp/misc-daemon
|
||||||
|
_at_syscmd "mv /tmp/misc-daemon /etc/init.d/misc-daemon"
|
||||||
|
|
||||||
|
_at_syscmd "chmod 755 /etc/init.d/rayhunter_daemon"
|
||||||
|
_at_syscmd "chmod 755 /etc/init.d/misc-daemon"
|
||||||
|
|
||||||
|
echo -n "waiting for reboot..."
|
||||||
|
_at_syscmd "shutdown -r -t 1 now"
|
||||||
|
|
||||||
|
# first wait for shutdown (it can take ~10s)
|
||||||
|
until ! _adb_shell true 2> /dev/null
|
||||||
|
do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# now wait for boot to finish
|
||||||
|
wait_for_adb_shell
|
||||||
|
|
||||||
|
echo " done!"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rayhunter() {
|
||||||
|
URL="http://localhost:8080"
|
||||||
|
"$ADB" forward tcp:8080 tcp:8080 > /dev/null
|
||||||
|
echo -n "checking for rayhunter server..."
|
||||||
|
|
||||||
|
SECONDS=0
|
||||||
|
while (( SECONDS < 30 )); do
|
||||||
|
if curl -L --fail-with-body "$URL" -o /dev/null -s; then
|
||||||
|
echo "success!"
|
||||||
|
echo "you can access rayhunter at $URL"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
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
|
||||||
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
#! /bin/bash
|
#! /bin/sh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ case "$1" in
|
|||||||
start)
|
start)
|
||||||
echo -n "Starting rayhunter: "
|
echo -n "Starting rayhunter: "
|
||||||
start-stop-daemon -S -b --make-pidfile --pidfile /tmp/rayhunter.pid \
|
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"
|
echo "done"
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
|
|||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
#!/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cargo build --bin serial
|
|
||||||
cargo build --bin rootshell --target armv7-unknown-linux-gnueabihf --release
|
|
||||||
|
|
||||||
# Force a switch into the debug mode to enable ADB
|
|
||||||
cargo run --bin serial -- AT
|
|
||||||
echo -n "adb enabled, waiting for reboot"
|
|
||||||
until adb shell true 2> /dev/null
|
|
||||||
do
|
|
||||||
echo -n .
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
echo "it's alive!"
|
|
||||||
adb push target/armv7-unknown-linux-gnueabihf/release/rootshell /tmp/
|
|
||||||
cargo run --bin serial -- "AT+SYSCMD=mv /tmp/rootshell /bin/rootshell"
|
|
||||||
sleep 1
|
|
||||||
cargo run --bin serial -- "AT+SYSCMD=chown root /bin/rootshell"
|
|
||||||
sleep 1
|
|
||||||
cargo run --bin serial -- "AT+SYSCMD=chmod 4755 /bin/rootshell"
|
|
||||||
echo "we have root!"
|
|
||||||
adb shell /bin/rootshell -c id
|
|
||||||
adb shell '/bin/rootshell -c "mkdir /data/rayhunter"'
|
|
||||||
adb push config.toml.example /data/rayhunter/config.toml
|
|
||||||
adb push scripts/rayhunter_daemon /tmp/rayhunter_daemon
|
|
||||||
adb push scripts/misc-daemon /tmp/misc-daemon
|
|
||||||
adb shell '/bin/rootshell -c "mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon"'
|
|
||||||
adb shell '/bin/rootshell -c "mv /tmp/misc-daemon /etc/init.d/misc-daemon"'
|
|
||||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/rayhunter_daemon"'
|
|
||||||
adb shell '/bin/rootshell -c "chmod 755 /etc/init.d/misc-daemon"'
|
|
||||||
./make.sh
|
|
||||||
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter"
|
name = "rayhunter"
|
||||||
version = "0.1.0"
|
version = "0.2.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
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"
|
name = "rayhunter"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
orbic = []
|
||||||
|
tplink = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "1.5.0"
|
bytes = "1.5.0"
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
@@ -17,10 +22,11 @@ deku = { version = "0.16.0", features = ["logging"] }
|
|||||||
env_logger = "0.10.1"
|
env_logger = "0.10.1"
|
||||||
libc = "0.2.150"
|
libc = "0.2.150"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
|
nix = { version = "0.29.0", features = ["feature"] }
|
||||||
pcap-file-tokio = "0.1.0"
|
pcap-file-tokio = "0.1.0"
|
||||||
thiserror = "1.0.50"
|
thiserror = "1.0.50"
|
||||||
telcom-parser = { path = "../telcom-parser" }
|
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-core = "0.3.30"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::util::RuntimeMetadata;
|
||||||
use crate::{diag::MessagesContainer, gsmtap_parser};
|
use crate::{diag::MessagesContainer, gsmtap_parser};
|
||||||
|
|
||||||
use super::{imsi_provided::ImsiProvidedAnalyzer, information_element::InformationElement, lte_downgrade::LteSib6And7DowngradeAnalyzer, null_cipher::NullCipherAnalyzer};
|
use super::{
|
||||||
|
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
|
||||||
|
imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement,
|
||||||
|
priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
|
||||||
|
};
|
||||||
|
|
||||||
/// Qualitative measure of how severe a Warning event type is.
|
/// Qualitative measure of how severe a Warning event type is.
|
||||||
/// The levels should break down like this:
|
/// The levels should break down like this:
|
||||||
@@ -18,7 +23,7 @@ pub enum Severity {
|
|||||||
High,
|
High,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [QualitativeWarning] events will always be shown to the user in some manner,
|
/// `QualitativeWarning` events will always be shown to the user in some manner,
|
||||||
/// while `Informational` ones may be hidden based on user settings.
|
/// while `Informational` ones may be hidden based on user settings.
|
||||||
#[derive(Serialize, Debug, Clone)]
|
#[derive(Serialize, Debug, Clone)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
@@ -60,19 +65,20 @@ pub trait Analyzer {
|
|||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct AnalyzerMetadata {
|
pub struct AnalyzerMetadata {
|
||||||
name: String,
|
pub name: String,
|
||||||
description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct ReportMetadata {
|
pub struct ReportMetadata {
|
||||||
analyzers: Vec<AnalyzerMetadata>,
|
pub analyzers: Vec<AnalyzerMetadata>,
|
||||||
|
pub rayhunter: RuntimeMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone)]
|
#[derive(Serialize, Debug, Clone)]
|
||||||
pub struct PacketAnalysis {
|
pub struct PacketAnalysis {
|
||||||
timestamp: DateTime<FixedOffset>,
|
pub timestamp: DateTime<FixedOffset>,
|
||||||
events: Vec<Option<Event>>,
|
pub events: Vec<Option<Event>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
@@ -86,22 +92,47 @@ impl AnalysisRow {
|
|||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.skipped_message_reasons.is_empty() && self.analysis.is_empty()
|
self.skipped_message_reasons.is_empty() && self.analysis.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn contains_warnings(&self) -> bool {
|
||||||
|
for analysis in &self.analysis {
|
||||||
|
for event in analysis.events.iter().flatten() {
|
||||||
|
if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Harness {
|
pub struct Harness {
|
||||||
analyzers: Vec<Box<dyn Analyzer + Send>>,
|
analyzers: Vec<Box<dyn Analyzer + Send>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Harness {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Harness {
|
impl Harness {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { analyzers: Vec::new() }
|
Self {
|
||||||
|
analyzers: Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_all_analyzers() -> Self {
|
pub fn new_with_all_analyzers() -> Self {
|
||||||
let mut harness = Harness::new();
|
let mut harness = Harness::new();
|
||||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer{}));
|
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
|
||||||
harness.add_analyzer(Box::new(ImsiProvidedAnalyzer{}));
|
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
|
||||||
harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
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
|
harness
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,26 +187,29 @@ impl Harness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
|
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))
|
.map(|analyzer| analyzer.analyze_information_element(ie))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_names(&self) -> Vec<Cow<'_, str>> {
|
pub fn get_names(&self) -> Vec<Cow<'_, str>> {
|
||||||
self.analyzers.iter()
|
self.analyzers
|
||||||
|
.iter()
|
||||||
.map(|analyzer| analyzer.get_name())
|
.map(|analyzer| analyzer.get_name())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_descriptions(&self) -> Vec<Cow<'_, str>> {
|
pub fn get_descriptions(&self) -> Vec<Cow<'_, str>> {
|
||||||
self.analyzers.iter()
|
self.analyzers
|
||||||
|
.iter()
|
||||||
.map(|analyzer| analyzer.get_description())
|
.map(|analyzer| analyzer.get_description())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_metadata(&self) -> ReportMetadata {
|
pub fn get_metadata(&self) -> ReportMetadata {
|
||||||
let names = self.get_names();
|
let names = self.get_names();
|
||||||
let descriptions = self.get_names();
|
let descriptions = self.get_descriptions();
|
||||||
let mut analyzers = Vec::new();
|
let mut analyzers = Vec::new();
|
||||||
for (name, description) in names.iter().zip(descriptions.iter()) {
|
for (name, description) in names.iter().zip(descriptions.iter()) {
|
||||||
analyzers.push(AnalyzerMetadata {
|
analyzers.push(AnalyzerMetadata {
|
||||||
@@ -184,8 +218,11 @@ impl Harness {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rayhunter = RuntimeMetadata::new();
|
||||||
|
|
||||||
ReportMetadata {
|
ReportMetadata {
|
||||||
analyzers,
|
analyzers,
|
||||||
|
rayhunter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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::analyzer::{Analyzer, Event, EventType, Severity};
|
||||||
use super::information_element::{InformationElement, LteInformationElement};
|
use super::information_element::{InformationElement, LteInformationElement};
|
||||||
|
|
||||||
pub struct ImsiProvidedAnalyzer {
|
pub struct ImsiProvidedAnalyzer {}
|
||||||
}
|
|
||||||
|
|
||||||
impl Analyzer for ImsiProvidedAnalyzer {
|
impl Analyzer for ImsiProvidedAnalyzer {
|
||||||
fn get_name(&self) -> Cow<str> {
|
fn get_name(&self) -> Cow<str> {
|
||||||
@@ -18,8 +17,12 @@ impl Analyzer for ImsiProvidedAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||||
let InformationElement::LTE(LteInformationElement::PCCH(pcch_msg)) = ie else {
|
let pcch_msg = match ie {
|
||||||
return None;
|
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 {
|
let PCCH_MessageType::C1(PCCH_MessageType_c1::Paging(paging)) = &pcch_msg.message else {
|
||||||
return None;
|
return None;
|
||||||
@@ -27,9 +30,11 @@ impl Analyzer for ImsiProvidedAnalyzer {
|
|||||||
for record in &paging.paging_record_list.as_ref()?.0 {
|
for record in &paging.paging_record_list.as_ref()?.0 {
|
||||||
if let PagingUE_Identity::Imsi(_) = record.ue_identity {
|
if let PagingUE_Identity::Imsi(_) = record.ue_identity {
|
||||||
return Some(Event {
|
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(),
|
message: "IMSI was provided to cell".to_string(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
//! the term to refer to a structured, fully parsed message in any telcom
|
//! the term to refer to a structured, fully parsed message in any telcom
|
||||||
//! standard.
|
//! standard.
|
||||||
|
|
||||||
|
use crate::gsmtap::{GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
|
||||||
use telcom_parser::{decode, lte_rrc};
|
use telcom_parser::{decode, lte_rrc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use crate::gsmtap::{GsmtapType, LteRrcSubtype, GsmtapMessage};
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum InformationElementError {
|
pub enum InformationElementError {
|
||||||
@@ -19,14 +19,18 @@ pub enum InformationElementError {
|
|||||||
pub enum InformationElement {
|
pub enum InformationElement {
|
||||||
GSM,
|
GSM,
|
||||||
UMTS,
|
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,
|
FiveG,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum LteInformationElement {
|
pub enum LteInformationElement {
|
||||||
DlCcch(lte_rrc::DL_CCCH_Message),
|
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),
|
UlCcch(lte_rrc::UL_CCCH_Message),
|
||||||
UlDcch(lte_rrc::UL_DCCH_Message),
|
UlDcch(lte_rrc::UL_DCCH_Message),
|
||||||
BcchBch(lte_rrc::BCCH_BCH_Message),
|
BcchBch(lte_rrc::BCCH_BCH_Message),
|
||||||
@@ -40,6 +44,8 @@ pub enum LteInformationElement {
|
|||||||
SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message),
|
SbcchSlBch(lte_rrc::SBCCH_SL_BCH_Message),
|
||||||
SbcchSlBchV2x(lte_rrc::SBCCH_SL_BCH_Message_V2X_r14),
|
SbcchSlBchV2x(lte_rrc::SBCCH_SL_BCH_Message_V2X_r14),
|
||||||
|
|
||||||
|
// FIXME: actually parse NAS messages
|
||||||
|
NAS(Vec<u8>),
|
||||||
// FIXME: unclear which message these "NB" types map to
|
// FIXME: unclear which message these "NB" types map to
|
||||||
//DlCcchNb(),
|
//DlCcchNb(),
|
||||||
//DlDcchNb(),
|
//DlDcchNb(),
|
||||||
@@ -58,11 +64,11 @@ impl TryFrom<&GsmtapMessage> for InformationElement {
|
|||||||
fn try_from(gsmtap_msg: &GsmtapMessage) -> Result<Self, Self::Error> {
|
fn try_from(gsmtap_msg: &GsmtapMessage) -> Result<Self, Self::Error> {
|
||||||
match gsmtap_msg.header.gsmtap_type {
|
match gsmtap_msg.header.gsmtap_type {
|
||||||
GsmtapType::LteRrc(lte_rrc_subtype) => {
|
GsmtapType::LteRrc(lte_rrc_subtype) => {
|
||||||
use LteRrcSubtype as L;
|
|
||||||
use LteInformationElement as R;
|
use LteInformationElement as R;
|
||||||
|
use LteRrcSubtype as L;
|
||||||
let lte = match lte_rrc_subtype {
|
let lte = match lte_rrc_subtype {
|
||||||
L::DlCcch => R::DlCcch(decode(&gsmtap_msg.payload)?),
|
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::UlCcch => R::UlCcch(decode(&gsmtap_msg.payload)?),
|
||||||
L::UlDcch => R::UlDcch(decode(&gsmtap_msg.payload)?),
|
L::UlDcch => R::UlDcch(decode(&gsmtap_msg.payload)?),
|
||||||
L::BcchBch => R::BcchBch(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::BcchDlSchMbms => R::BcchDlSchMbms(decode(&gsmtap_msg.payload)?),
|
||||||
L::SbcchSlBch => R::SbcchSlBch(decode(&gsmtap_msg.payload)?),
|
L::SbcchSlBch => R::SbcchSlBch(decode(&gsmtap_msg.payload)?),
|
||||||
L::SbcchSlBchV2x => R::SbcchSlBchV2x(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))
|
Ok(InformationElement::LTE(Box::new(lte)))
|
||||||
},
|
}
|
||||||
_ => Err(InformationElementError::UnsupportedGsmtapType(gsmtap_msg.header.gsmtap_type)),
|
GsmtapType::LteNas(LteNasSubtype::Plain) => Ok(InformationElement::LTE(Box::new(
|
||||||
|
LteInformationElement::NAS(gsmtap_msg.payload.clone()),
|
||||||
|
))),
|
||||||
|
_ => Err(InformationElementError::UnsupportedGsmtapType(
|
||||||
|
gsmtap_msg.header.gsmtap_type,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
pub mod analyzer;
|
pub mod analyzer;
|
||||||
pub mod information_element;
|
pub mod connection_redirect_downgrade;
|
||||||
pub mod lte_downgrade;
|
|
||||||
pub mod imsi_provided;
|
pub mod imsi_provided;
|
||||||
|
pub mod imsi_requested;
|
||||||
|
pub mod information_element;
|
||||||
pub mod null_cipher;
|
pub mod null_cipher;
|
||||||
|
pub mod priority_2g_downgrade;
|
||||||
|
pub mod util;
|
||||||
|
|||||||
@@ -1,25 +1,41 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use telcom_parser::lte_rrc::{CipheringAlgorithm_r12, DL_CCCH_MessageType, DL_CCCH_MessageType_c1, DL_DCCH_MessageType, DL_DCCH_MessageType_c1, PCCH_MessageType, PCCH_MessageType_c1, PagingUE_Identity, RRCConnectionReconfiguration, RRCConnectionReconfigurationCriticalExtensions, RRCConnectionReconfigurationCriticalExtensions_c1, RRCConnectionReconfiguration_r8_IEs, RRCConnectionRelease_v890_IEs, SCG_Configuration_r12, SecurityConfigHO_v1530HandoverType_v1530, SecurityModeCommand, SecurityModeCommandCriticalExtensions, SecurityModeCommandCriticalExtensions_c1};
|
use telcom_parser::lte_rrc::{
|
||||||
|
CipheringAlgorithm_r12, DL_DCCH_MessageType, DL_DCCH_MessageType_c1,
|
||||||
|
RRCConnectionReconfiguration, RRCConnectionReconfigurationCriticalExtensions,
|
||||||
|
RRCConnectionReconfigurationCriticalExtensions_c1, SCG_Configuration_r12,
|
||||||
|
SecurityConfigHO_v1530HandoverType_v1530, SecurityModeCommand,
|
||||||
|
SecurityModeCommandCriticalExtensions, SecurityModeCommandCriticalExtensions_c1,
|
||||||
|
};
|
||||||
|
|
||||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||||
use super::information_element::{InformationElement, LteInformationElement};
|
use super::information_element::{InformationElement, LteInformationElement};
|
||||||
|
|
||||||
pub struct NullCipherAnalyzer {
|
pub struct NullCipherAnalyzer {}
|
||||||
}
|
|
||||||
|
|
||||||
impl NullCipherAnalyzer {
|
impl NullCipherAnalyzer {
|
||||||
fn check_rrc_connection_reconfiguration_cipher(&self, reconfiguration: &RRCConnectionReconfiguration) -> bool {
|
fn check_rrc_connection_reconfiguration_cipher(
|
||||||
let RRCConnectionReconfigurationCriticalExtensions::C1(c1) = &reconfiguration.critical_extensions else {
|
&self,
|
||||||
|
reconfiguration: &RRCConnectionReconfiguration,
|
||||||
|
) -> bool {
|
||||||
|
let RRCConnectionReconfigurationCriticalExtensions::C1(c1) =
|
||||||
|
&reconfiguration.critical_extensions
|
||||||
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let RRCConnectionReconfigurationCriticalExtensions_c1::RrcConnectionReconfiguration_r8(c1) = c1 else {
|
let RRCConnectionReconfigurationCriticalExtensions_c1::RrcConnectionReconfiguration_r8(c1) =
|
||||||
|
c1
|
||||||
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if let Some(handover) = &c1.security_config_ho {
|
if let Some(handover) = &c1.security_config_ho {
|
||||||
let maybe_security_config = match &handover.handover_type {
|
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::IntraLTE(lte) => {
|
||||||
telcom_parser::lte_rrc::SecurityConfigHOHandoverType::InterRAT(rat) => Some(&rat.security_algorithm_config),
|
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 let Some(security_config) = maybe_security_config {
|
||||||
if security_config.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 {
|
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
|
// Use map/flatten to dig into a long chain of nested Option types
|
||||||
let maybe_v1250 = c1.non_critical_extension.as_ref()
|
let maybe_v1250 = c1
|
||||||
.map(|v890| v890.non_critical_extension.as_ref()).flatten()
|
.non_critical_extension
|
||||||
.map(|v920| v920.non_critical_extension.as_ref()).flatten()
|
.as_ref()
|
||||||
.map(|v1020| v1020.non_critical_extension.as_ref()).flatten()
|
.and_then(|v890| v890.non_critical_extension.as_ref())
|
||||||
.map(|v1130| v1130.non_critical_extension.as_ref()).flatten();
|
.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 {
|
let Some(v1250) = maybe_v1250 else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(SCG_Configuration_r12::Setup(scg_setup)) = v1250.scg_configuration_r12.as_ref() {
|
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()
|
let maybe_cipher = scg_setup
|
||||||
.map(|mci| mci.ciphering_algorithm_scg_r12.as_ref()).flatten();
|
.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 let Some(cipher) = maybe_cipher {
|
||||||
if cipher.0 == CipheringAlgorithm_r12::EEA0 {
|
if cipher.0 == CipheringAlgorithm_r12::EEA0 {
|
||||||
return true;
|
return true;
|
||||||
@@ -48,18 +69,26 @@ impl NullCipherAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let maybe_v1530_security_config = v1250.non_critical_extension.as_ref()
|
let maybe_v1530_security_config = v1250
|
||||||
.map(|v1310| v1310.non_critical_extension.as_ref()).flatten()
|
.non_critical_extension
|
||||||
.map(|v1430| v1430.non_critical_extension.as_ref()).flatten()
|
.as_ref()
|
||||||
.map(|v1510| v1510.non_critical_extension.as_ref()).flatten()
|
.and_then(|v1310| v1310.non_critical_extension.as_ref())
|
||||||
.map(|v1530| v1530.security_config_ho_v1530.as_ref()).flatten();
|
.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 {
|
let Some(v1530_security_config) = maybe_v1530_security_config else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let maybe_security_algorithm = match &v1530_security_config.handover_type_v1530 {
|
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::Intra5GC(intra_5gc) => {
|
||||||
SecurityConfigHO_v1530HandoverType_v1530::Fivegc_ToEPC(to_epc) => Some(&to_epc.security_algorithm_config_r15),
|
intra_5gc.security_algorithm_config_r15.as_ref()
|
||||||
SecurityConfigHO_v1530HandoverType_v1530::Epc_To5GC(to_5gc) => Some(&to_5gc.security_algorithm_config_r15),
|
}
|
||||||
|
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 let Some(security_algorithm) = maybe_security_algorithm {
|
||||||
if security_algorithm.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 {
|
if security_algorithm.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 {
|
||||||
@@ -76,7 +105,13 @@ impl NullCipherAnalyzer {
|
|||||||
let SecurityModeCommandCriticalExtensions_c1::SecurityModeCommand_r8(r8) = &c1 else {
|
let SecurityModeCommandCriticalExtensions_c1::SecurityModeCommand_r8(r8) = &c1 else {
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -93,20 +128,30 @@ impl Analyzer for NullCipherAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
|
||||||
let InformationElement::LTE(LteInformationElement::DlDcch(dcch_msg)) = ie else {
|
let dcch_msg = match ie {
|
||||||
return None;
|
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 {
|
let DL_DCCH_MessageType::C1(c1) = &dcch_msg.message else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let null_cipher_detected = match c1 {
|
let null_cipher_detected = match c1 {
|
||||||
DL_DCCH_MessageType_c1::RrcConnectionReconfiguration(reconfiguration) => self.check_rrc_connection_reconfiguration_cipher(reconfiguration),
|
DL_DCCH_MessageType_c1::RrcConnectionReconfiguration(reconfiguration) => {
|
||||||
DL_DCCH_MessageType_c1::SecurityModeCommand(command) => self.check_security_mode_command_cipher(command),
|
self.check_rrc_connection_reconfiguration_cipher(reconfiguration)
|
||||||
|
}
|
||||||
|
DL_DCCH_MessageType_c1::SecurityModeCommand(command) => {
|
||||||
|
self.check_security_mode_command_cipher(command)
|
||||||
|
}
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
if null_cipher_detected {
|
if null_cipher_detected {
|
||||||
return Some(Event {
|
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(),
|
message: "Cell suggested use of null cipher".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,31 @@ use std::borrow::Cow;
|
|||||||
|
|
||||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||||
use super::information_element::{InformationElement, LteInformationElement};
|
use super::information_element::{InformationElement, LteInformationElement};
|
||||||
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".
|
/// Based on heuristic T7 from Shinjo Park's "Why We Cannot Win".
|
||||||
pub struct LteSib6And7DowngradeAnalyzer {
|
pub struct LteSib6And7DowngradeAnalyzer {}
|
||||||
}
|
|
||||||
|
|
||||||
impl LteSib6And7DowngradeAnalyzer {
|
impl LteSib6And7DowngradeAnalyzer {
|
||||||
fn unpack_system_information<'a>(&self, ie: &'a InformationElement) -> Option<&'a SystemInformation_r8_IEsSib_TypeAndInfo> {
|
fn unpack_system_information<'a>(
|
||||||
if let InformationElement::LTE(LteInformationElement::BcchDlSch(bcch_dl_sch_message)) = ie {
|
&self,
|
||||||
if let BCCH_DL_SCH_MessageType::C1(BCCH_DL_SCH_MessageType_c1::SystemInformation(system_information)) = &bcch_dl_sch_message.message {
|
ie: &'a InformationElement,
|
||||||
if let SystemInformationCriticalExtensions::SystemInformation_r8(sib) = &system_information.critical_extensions {
|
) -> Option<&'a SystemInformation_r8_IEsSib_TypeAndInfo> {
|
||||||
return Some(&sib.sib_type_and_info);
|
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.")
|
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;
|
let sibs = &self.unpack_system_information(ie)?.0;
|
||||||
for sib in sibs {
|
for sib in sibs {
|
||||||
match sib {
|
match sib {
|
||||||
SystemInformation_r8_IEsSib_TypeAndInfo_Entry::Sib6(sib6) => {
|
SystemInformation_r8_IEsSib_TypeAndInfo_Entry::Sib6(sib6) => {
|
||||||
if let Some(carrier_info_list) = sib6.carrier_freq_list_utra_fdd.as_ref() {
|
if let Some(carrier_info_list) = sib6.carrier_freq_list_utra_fdd.as_ref() {
|
||||||
for carrier_info in &carrier_info_list.0 {
|
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 {
|
if p == 0 {
|
||||||
return Some(Event {
|
return Some(Event {
|
||||||
event_type: EventType::QualitativeWarning { severity: Severity::High },
|
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() {
|
if let Some(carrier_info_list) = sib6.carrier_freq_list_utra_tdd.as_ref() {
|
||||||
for carrier_info in &carrier_info_list.0 {
|
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 {
|
if p == 0 {
|
||||||
return Some(Event {
|
return Some(Event {
|
||||||
event_type: EventType::QualitativeWarning { severity: Severity::High },
|
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 {
|
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 {
|
if p == 0 {
|
||||||
return Some(Event {
|
return Some(Event {
|
||||||
event_type: EventType::QualitativeWarning { severity: Severity::High },
|
event_type: EventType::QualitativeWarning {
|
||||||
message: "LTE cell advertised a 2G cell for priority 0 reselection".to_string(),
|
severity: Severity::High,
|
||||||
|
},
|
||||||
|
message:
|
||||||
|
"LTE cell advertised a 2G cell for priority 0 reselection"
|
||||||
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => {},
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -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;
|
||||||
+104
-67
@@ -5,7 +5,7 @@ use crc::{Algorithm, Crc};
|
|||||||
use deku::prelude::*;
|
use deku::prelude::*;
|
||||||
|
|
||||||
use crate::hdlc::{self, hdlc_decapsulate};
|
use crate::hdlc::{self, hdlc_decapsulate};
|
||||||
use log::{warn, error};
|
use log::{error, warn};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
||||||
@@ -42,7 +42,7 @@ pub enum LogConfigRequest {
|
|||||||
log_type: u32,
|
log_type: u32,
|
||||||
log_mask_bitsize: u32,
|
log_mask_bitsize: u32,
|
||||||
log_mask: Vec<u8>,
|
log_mask: Vec<u8>,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||||
@@ -93,13 +93,19 @@ impl MessagesContainer {
|
|||||||
Ok(data) => match Message::from_bytes((&data, 0)) {
|
Ok(data) => match Message::from_bytes((&data, 0)) {
|
||||||
Ok(((leftover_bytes, _), res)) => {
|
Ok(((leftover_bytes, _), res)) => {
|
||||||
if !leftover_bytes.is_empty() {
|
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));
|
result.push(Ok(res));
|
||||||
},
|
}
|
||||||
Err(e) => result.push(Err(DiagParsingError::MessageParsingError(e, data))),
|
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>,
|
msg: Vec<u8>,
|
||||||
},
|
},
|
||||||
#[deku(id = "0xb0c0")]
|
#[deku(id = "0xb0c0")]
|
||||||
LteRrcOtaMessage{
|
LteRrcOtaMessage {
|
||||||
ext_header_version: u8,
|
ext_header_version: u8,
|
||||||
#[deku(ctx = "*ext_header_version")]
|
#[deku(ctx = "*ext_header_version")]
|
||||||
packet: LteRrcOtaPacket,
|
packet: LteRrcOtaPacket,
|
||||||
@@ -183,6 +189,8 @@ pub enum LogBody {
|
|||||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||||
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
||||||
Nas4GMessage {
|
Nas4GMessage {
|
||||||
|
#[deku(ctx = "log_type")]
|
||||||
|
direction: Nas4GMessageDirection,
|
||||||
ext_header_version: u8,
|
ext_header_version: u8,
|
||||||
rrc_rel: u8,
|
rrc_rel: u8,
|
||||||
rrc_version_minor: u8,
|
rrc_version_minor: u8,
|
||||||
@@ -208,7 +216,20 @@ pub enum LogBody {
|
|||||||
NrRrcOtaMessage {
|
NrRrcOtaMessage {
|
||||||
#[deku(count = "hdr_len")]
|
#[deku(count = "hdr_len")]
|
||||||
msg: Vec<u8>,
|
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)]
|
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||||
@@ -349,15 +370,17 @@ pub enum ResponsePayload {
|
|||||||
#[deku(ctx = "subopcode: u32", id = "subopcode")]
|
#[deku(ctx = "subopcode: u32", id = "subopcode")]
|
||||||
pub enum LogConfigResponse {
|
pub enum LogConfigResponse {
|
||||||
#[deku(id = "1")]
|
#[deku(id = "1")]
|
||||||
RetrieveIdRanges {
|
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
|
||||||
log_mask_sizes: [u32; 16],
|
|
||||||
},
|
|
||||||
|
|
||||||
#[deku(id = "3")]
|
#[deku(id = "3")]
|
||||||
SetMask,
|
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 current_byte: u8 = 0;
|
||||||
let mut num_bits_written: u8 = 0;
|
let mut num_bits_written: u8 = 0;
|
||||||
let mut log_mask: Vec<u8> = vec![];
|
let mut log_mask: Vec<u8> = vec![];
|
||||||
@@ -398,31 +421,35 @@ mod test {
|
|||||||
log_mask_bitsize: 0,
|
log_mask_bitsize: 0,
|
||||||
log_mask: vec![],
|
log_mask: vec![],
|
||||||
});
|
});
|
||||||
assert_eq!(req.to_bytes().unwrap(), vec![
|
assert_eq!(
|
||||||
115, 0, 0, 0,
|
req.to_bytes().unwrap(),
|
||||||
3, 0, 0, 0,
|
vec![115, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,]
|
||||||
0, 0, 0, 0,
|
);
|
||||||
0, 0, 0, 0,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_log_mask_request() {
|
fn test_build_log_mask_request() {
|
||||||
let log_type = 11;
|
let log_type = 11;
|
||||||
let bitsize = 513;
|
let bitsize = 513;
|
||||||
let req = build_log_mask_request(log_type, bitsize, &crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING);
|
let req = build_log_mask_request(
|
||||||
assert_eq!(req, Request::LogConfig(LogConfigRequest::SetMask {
|
log_type,
|
||||||
log_type: log_type,
|
bitsize,
|
||||||
log_mask_bitsize: bitsize,
|
&crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING,
|
||||||
log_mask: vec![
|
);
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
assert_eq!(
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0,
|
req,
|
||||||
0x0, 0x0, 0xc, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
Request::LogConfig(LogConfigRequest::SetMask {
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
log_type,
|
||||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
log_mask_bitsize: bitsize,
|
||||||
0x0,
|
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]
|
#[test]
|
||||||
@@ -433,53 +460,53 @@ mod test {
|
|||||||
mdm_field: -1,
|
mdm_field: -1,
|
||||||
hdlc_encapsulated_request: vec![1, 2, 3, 4],
|
hdlc_encapsulated_request: vec![1, 2, 3, 4],
|
||||||
};
|
};
|
||||||
assert_eq!(req.to_bytes().unwrap(), vec![
|
assert_eq!(req.to_bytes().unwrap(), vec![32, 0, 0, 0, 1, 2, 3, 4,]);
|
||||||
32, 0, 0, 0,
|
|
||||||
1, 2, 3, 4,
|
|
||||||
]);
|
|
||||||
let req = RequestContainer {
|
let req = RequestContainer {
|
||||||
data_type: DataType::UserSpace,
|
data_type: DataType::UserSpace,
|
||||||
use_mdm: true,
|
use_mdm: true,
|
||||||
mdm_field: -1,
|
mdm_field: -1,
|
||||||
hdlc_encapsulated_request: vec![1, 2, 3, 4],
|
hdlc_encapsulated_request: vec![1, 2, 3, 4],
|
||||||
};
|
};
|
||||||
assert_eq!(req.to_bytes().unwrap(), vec![
|
assert_eq!(
|
||||||
32, 0, 0, 0,
|
req.to_bytes().unwrap(),
|
||||||
255, 255, 255, 255,
|
vec![32, 0, 0, 0, 255, 255, 255, 255, 1, 2, 3, 4,]
|
||||||
1, 2, 3, 4,
|
);
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_logs() {
|
fn test_logs() {
|
||||||
let data = vec![
|
let data = vec![
|
||||||
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20,
|
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0, 160, 0,
|
||||||
14, 48, 0, 160, 0, 2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1,
|
2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1, 238, 173, 213, 77, 208,
|
||||||
238, 173, 213, 77, 208
|
|
||||||
];
|
];
|
||||||
let msg = Message::from_bytes((&data, 0)).unwrap().1;
|
let msg = Message::from_bytes((&data, 0)).unwrap().1;
|
||||||
assert_eq!(msg, Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
msg,
|
||||||
outer_length: 38,
|
Message::Log {
|
||||||
inner_length: 38,
|
pending_msgs: 0,
|
||||||
log_type: 0xb0c0,
|
outer_length: 38,
|
||||||
timestamp: Timestamp { ts: 72659535985485082 },
|
inner_length: 38,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
log_type: 0xb0c0,
|
||||||
ext_header_version: 20,
|
timestamp: Timestamp {
|
||||||
packet: LteRrcOtaPacket::V8 {
|
ts: 72659535985485082
|
||||||
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],
|
|
||||||
},
|
},
|
||||||
},
|
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 {
|
fn make_container(data_type: DataType, message: HdlcEncapsulatedMessage) -> MessagesContainer {
|
||||||
@@ -500,7 +527,9 @@ mod test {
|
|||||||
outer_length: length_with_payload,
|
outer_length: length_with_payload,
|
||||||
inner_length: length_with_payload,
|
inner_length: length_with_payload,
|
||||||
log_type: 0xb0c0,
|
log_type: 0xb0c0,
|
||||||
timestamp: Timestamp { ts: 72659535985485082 },
|
timestamp: Timestamp {
|
||||||
|
ts: 72659535985485082,
|
||||||
|
},
|
||||||
body: LogBody::LteRrcOtaMessage {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
ext_header_version: 20,
|
ext_header_version: 20,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
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_data = hdlc::hdlc_encapsulate(&serialized, &CRC_CCITT);
|
||||||
let encapsulated = HdlcEncapsulatedMessage {
|
let encapsulated = HdlcEncapsulatedMessage {
|
||||||
len: encapsulated_data.len() as u32,
|
len: encapsulated_data.len() as u32,
|
||||||
@@ -559,7 +590,10 @@ mod test {
|
|||||||
container.num_messages += 1;
|
container.num_messages += 1;
|
||||||
let result = container.into_messages();
|
let result = container.into_messages();
|
||||||
assert_eq!(result[0], Ok(message1));
|
assert_eq!(result[0], Ok(message1));
|
||||||
assert!(matches!(result[1], Err(DiagParsingError::MessageParsingError(_, _))));
|
assert!(matches!(
|
||||||
|
result[1],
|
||||||
|
Err(DiagParsingError::MessageParsingError(_, _))
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -574,6 +608,9 @@ mod test {
|
|||||||
container.num_messages += 1;
|
container.num_messages += 1;
|
||||||
let result = container.into_messages();
|
let result = container.into_messages();
|
||||||
assert_eq!(result[0], Ok(message1));
|
assert_eq!(result[0], Ok(message1));
|
||||||
assert!(matches!(result[1], Err(DiagParsingError::HdlcDecapsulationError(_, _))));
|
assert!(matches!(
|
||||||
|
result[1],
|
||||||
|
Err(DiagParsingError::HdlcDecapsulationError(_, _))
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-37
@@ -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::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 crate::log_codes;
|
||||||
|
|
||||||
|
use deku::prelude::*;
|
||||||
|
use futures_core::TryStream;
|
||||||
|
use log::{error, info};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsRawFd;
|
||||||
use futures_core::TryStream;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use log::{info, warn, error};
|
|
||||||
use deku::prelude::*;
|
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
@@ -38,36 +41,37 @@ pub enum DiagDeviceError {
|
|||||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
|
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
|
||||||
// Layer 2:
|
// Layer 2:
|
||||||
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
|
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
|
||||||
|
|
||||||
// Layer 3:
|
// Layer 3:
|
||||||
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C, // 0x512f
|
log_codes::LOG_GSM_RR_SIGNALING_MESSAGE_C, // 0x512f
|
||||||
log_codes::WCDMA_SIGNALLING_MESSAGE, // 0x412f
|
log_codes::WCDMA_SIGNALLING_MESSAGE, // 0x412f
|
||||||
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C, // 0xb0c0
|
log_codes::LOG_LTE_RRC_OTA_MSG_LOG_C, // 0xb0c0
|
||||||
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C, // 0xb821
|
log_codes::LOG_NR_RRC_OTA_MSG_LOG_C, // 0xb821
|
||||||
|
|
||||||
// NAS:
|
// NAS:
|
||||||
log_codes::LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C, // 0x713a
|
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_IN_MSG_LOG_C, // 0xb0e2
|
||||||
log_codes::LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C, // 0xb0e3
|
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_IN_MSG_LOG_C, // 0xb0ec
|
||||||
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
|
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
|
||||||
|
|
||||||
// User IP traffic:
|
// 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 BUFFER_LEN: usize = 1024 * 1024 * 10;
|
||||||
const MEMORY_DEVICE_MODE: i32 = 2;
|
const MEMORY_DEVICE_MODE: u32 = 2;
|
||||||
|
|
||||||
#[cfg(target_arch = "arm")]
|
#[cfg(target_arch = "arm")]
|
||||||
const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
|
const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
||||||
|
#[cfg(target_arch = "aarch64")]
|
||||||
|
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
||||||
|
|
||||||
#[cfg(target_arch = "arm")]
|
#[cfg(target_arch = "arm")]
|
||||||
const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7;
|
const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7;
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||||
|
#[cfg(target_arch = "aarch64")]
|
||||||
|
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||||
|
|
||||||
pub struct DiagDevice {
|
pub struct DiagDevice {
|
||||||
file: File,
|
file: File,
|
||||||
@@ -95,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 {
|
futures::stream::try_unfold(self, |dev| async {
|
||||||
let container = dev.get_next_messages_container().await?;
|
let container = dev.get_next_messages_container().await?;
|
||||||
Ok(Some((container, dev)))
|
Ok(Some((container, dev)))
|
||||||
@@ -104,16 +110,25 @@ impl DiagDevice {
|
|||||||
|
|
||||||
async fn get_next_messages_container(&mut self) -> Result<MessagesContainer, DiagDeviceError> {
|
async fn get_next_messages_container(&mut self) -> Result<MessagesContainer, DiagDeviceError> {
|
||||||
let mut bytes_read = 0;
|
let mut bytes_read = 0;
|
||||||
while bytes_read == 0 {
|
// TP-Link M7350 sometimes sends too small messages, we need to be able to deal with short reads.
|
||||||
bytes_read = self.file.read(&mut self.read_buf).await
|
while bytes_read <= 8 {
|
||||||
|
bytes_read = self
|
||||||
|
.file
|
||||||
|
.read(&mut self.read_buf)
|
||||||
|
.await
|
||||||
.map_err(DiagDeviceError::DeviceReadFailed)?;
|
.map_err(DiagDeviceError::DeviceReadFailed)?;
|
||||||
}
|
}
|
||||||
let ((leftover_bytes, _), container) = MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0))
|
|
||||||
.map_err(DiagDeviceError::ParseMessagesContainerError)?;
|
info!(
|
||||||
if !leftover_bytes.is_empty() {
|
"Parsing messages container size = {:?} [{:?}]",
|
||||||
warn!("warning: {} leftover bytes when parsing MessagesContainer", leftover_bytes.len());
|
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<()> {
|
async fn write_request(&mut self, req: &Request) -> DiagResult<()> {
|
||||||
@@ -123,7 +138,9 @@ impl DiagDevice {
|
|||||||
use_mdm: self.use_mdm > 0,
|
use_mdm: self.use_mdm > 0,
|
||||||
mdm_field: -1,
|
mdm_field: -1,
|
||||||
hdlc_encapsulated_request: hdlc_encapsulate(req_bytes, &CRC_CCITT),
|
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 {
|
if let Err(err) = self.file.write(&buf).await {
|
||||||
// For reasons I don't entirely understand, calls to write(2) on
|
// For reasons I don't entirely understand, calls to write(2) on
|
||||||
// /dev/diag always return 0 bytes written, though the written
|
// /dev/diag always return 0 bytes written, though the written
|
||||||
@@ -158,13 +175,17 @@ impl DiagDevice {
|
|||||||
for msg in self.read_response().await? {
|
for msg in self.read_response().await? {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(Message::Log { .. }) => info!("skipping log response..."),
|
Ok(Message::Log { .. }) => info!("skipping log response..."),
|
||||||
Ok(Message::Response { payload, status, .. }) => match payload {
|
Ok(Message::Response {
|
||||||
ResponsePayload::LogConfig(LogConfigResponse::RetrieveIdRanges { log_mask_sizes }) => {
|
payload, status, ..
|
||||||
|
}) => match payload {
|
||||||
|
ResponsePayload::LogConfig(LogConfigResponse::RetrieveIdRanges {
|
||||||
|
log_mask_sizes,
|
||||||
|
}) => {
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
return Err(DiagDeviceError::RequestFailed(status, req));
|
return Err(DiagDeviceError::RequestFailed(status, req));
|
||||||
}
|
}
|
||||||
return Ok(log_mask_sizes);
|
return Ok(log_mask_sizes);
|
||||||
},
|
}
|
||||||
_ => info!("skipping non-LogConfigResponse response..."),
|
_ => info!("skipping non-LogConfigResponse response..."),
|
||||||
},
|
},
|
||||||
Err(e) => error!("error parsing message: {:?}", e),
|
Err(e) => error!("error parsing message: {:?}", e),
|
||||||
@@ -175,20 +196,26 @@ impl DiagDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn set_log_mask(&mut self, log_type: u32, log_mask_bitsize: u32) -> DiagResult<()> {
|
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?;
|
self.write_request(&req).await?;
|
||||||
|
|
||||||
for msg in self.read_response().await? {
|
for msg in self.read_response().await? {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(Message::Log { .. }) => info!("skipping log response..."),
|
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 let ResponsePayload::LogConfig(LogConfigResponse::SetMask) = payload {
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
return Err(DiagDeviceError::RequestFailed(status, req));
|
return Err(DiagDeviceError::RequestFailed(status, req));
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => error!("error parsing message: {:?}", e),
|
Err(e) => error!("error parsing message: {:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,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
|
// 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 {
|
unsafe {
|
||||||
if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 {
|
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(
|
let ret = libc::ioctl(
|
||||||
fd,
|
fd,
|
||||||
DIAG_IOCTL_SWITCH_LOGGING,
|
DIAG_IOCTL_SWITCH_LOGGING,
|
||||||
&mut [mode, -1, 0] as *mut _, // diag_logging_mode_param_t
|
&mut params as *mut _,
|
||||||
std::mem::size_of::<[i32; 3]>(), 0, 0, 0, 0
|
std::mem::size_of::<diag_logging_mode_param_t>(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
);
|
);
|
||||||
if ret < 0 {
|
if ret < 0 {
|
||||||
let msg = format!("DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}", ret);
|
let msg = format!(
|
||||||
return Err(DiagDeviceError::InitializationFailed(msg))
|
"DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}",
|
||||||
|
ret
|
||||||
|
);
|
||||||
|
return Err(DiagDeviceError::InitializationFailed(msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +300,7 @@ fn determine_use_mdm(fd: i32) -> DiagResult<i32> {
|
|||||||
unsafe {
|
unsafe {
|
||||||
if libc::ioctl(fd, DIAG_IOCTL_REMOTE_DEV, &use_mdm as *const i32) < 0 {
|
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);
|
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)
|
Ok(use_mdm)
|
||||||
|
|||||||
+22
-17
@@ -6,24 +6,24 @@ use deku::prelude::*;
|
|||||||
pub enum GsmtapType {
|
pub enum GsmtapType {
|
||||||
Um(UmSubtype),
|
Um(UmSubtype),
|
||||||
Abis,
|
Abis,
|
||||||
UmBurst, /* raw burst bits */
|
UmBurst, /* raw burst bits */
|
||||||
SIM, /* ISO 7816 smart card interface */
|
SIM, /* ISO 7816 smart card interface */
|
||||||
TetraI1, /* tetra air interface */
|
TetraI1, /* tetra air interface */
|
||||||
TetraI1Burst, /* tetra air interface */
|
TetraI1Burst, /* tetra air interface */
|
||||||
WmxBurst, /* WiMAX burst */
|
WmxBurst, /* WiMAX burst */
|
||||||
GbLlc, /* GPRS Gb interface: LLC */
|
GbLlc, /* GPRS Gb interface: LLC */
|
||||||
GbSndcp, /* GPRS Gb interface: SNDCP */
|
GbSndcp, /* GPRS Gb interface: SNDCP */
|
||||||
Gmr1Um, /* GMR-1 L2 packets */
|
Gmr1Um, /* GMR-1 L2 packets */
|
||||||
UmtsRlcMac,
|
UmtsRlcMac,
|
||||||
UmtsRrc(UmtsRrcSubtype),
|
UmtsRrc(UmtsRrcSubtype),
|
||||||
LteRrc(LteRrcSubtype), /* LTE interface */
|
LteRrc(LteRrcSubtype), /* LTE interface */
|
||||||
LteMac, /* LTE MAC interface */
|
LteMac, /* LTE MAC interface */
|
||||||
LteMacFramed, /* LTE MAC with context hdr */
|
LteMacFramed, /* LTE MAC with context hdr */
|
||||||
OsmocoreLog, /* libosmocore logging */
|
OsmocoreLog, /* libosmocore logging */
|
||||||
QcDiag, /* Qualcomm DIAG frame */
|
QcDiag, /* Qualcomm DIAG frame */
|
||||||
LteNas(LteNasSubtype), /* LTE Non-Access Stratum */
|
LteNas(LteNasSubtype), /* LTE Non-Access Stratum */
|
||||||
E1T1, /* E1/T1 Lines */
|
E1T1, /* E1/T1 Lines */
|
||||||
GsmRlp, /* GSM RLP frames as per 3GPP TS 24.022 */
|
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
|
// 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,
|
SysInfoTypeSB1 = 58,
|
||||||
SysInfoTypeSB2 = 59,
|
SysInfoTypeSB2 = 59,
|
||||||
ToTargetRNCContainer = 60,
|
ToTargetRNCContainer = 60,
|
||||||
TargetRNCToSourceRNCContainer = 61
|
TargetRNCToSourceRNCContainer = 61,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@@ -200,6 +200,11 @@ pub struct GsmtapHeader {
|
|||||||
#[deku(update = "self.gsmtap_type.get_type()")]
|
#[deku(update = "self.gsmtap_type.get_type()")]
|
||||||
pub packet_type: u8,
|
pub packet_type: u8,
|
||||||
pub timeslot: u8,
|
pub timeslot: u8,
|
||||||
|
#[deku(bits = 1)]
|
||||||
|
pub pcs_band_indicator: bool,
|
||||||
|
#[deku(bits = 1)]
|
||||||
|
pub uplink: bool,
|
||||||
|
#[deku(bits = 14)]
|
||||||
pub arfcn: u16,
|
pub arfcn: u16,
|
||||||
pub signal_dbm: i8,
|
pub signal_dbm: i8,
|
||||||
pub signal_noise_ratio_db: u8,
|
pub signal_noise_ratio_db: u8,
|
||||||
@@ -213,15 +218,15 @@ pub struct GsmtapHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GsmtapHeader {
|
impl GsmtapHeader {
|
||||||
pub fn new(
|
pub fn new(gsmtap_type: GsmtapType) -> Self {
|
||||||
gsmtap_type: GsmtapType,
|
|
||||||
) -> Self {
|
|
||||||
GsmtapHeader {
|
GsmtapHeader {
|
||||||
gsmtap_type,
|
gsmtap_type,
|
||||||
version: 2,
|
version: 2,
|
||||||
header_len: 4,
|
header_len: 4,
|
||||||
packet_type: gsmtap_type.get_type(),
|
packet_type: gsmtap_type.get_type(),
|
||||||
timeslot: 0,
|
timeslot: 0,
|
||||||
|
pcs_band_indicator: false,
|
||||||
|
uplink: false,
|
||||||
arfcn: 0,
|
arfcn: 0,
|
||||||
signal_dbm: 0,
|
signal_dbm: 0,
|
||||||
signal_noise_ratio_db: 0,
|
signal_noise_ratio_db: 0,
|
||||||
|
|||||||
+52
-16
@@ -13,7 +13,10 @@ pub enum GsmtapParserError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(msg: Message) -> Result<Option<(Timestamp, GsmtapMessage)>, 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)? {
|
match log_to_gsmtap(body)? {
|
||||||
Some(msg) => Ok(Some((timestamp, msg))),
|
Some(msg) => Ok(Some((timestamp, msg))),
|
||||||
None => Ok(None),
|
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> {
|
fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserError> {
|
||||||
match value {
|
match value {
|
||||||
LogBody::LteRrcOtaMessage { ext_header_version, packet } => {
|
LogBody::LteRrcOtaMessage {
|
||||||
|
ext_header_version,
|
||||||
|
packet,
|
||||||
|
} => {
|
||||||
let gsmtap_type = match ext_header_version {
|
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),
|
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
||||||
2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch),
|
2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch),
|
||||||
3 => GsmtapType::LteRrc(LteRrcSubtype::MCCH),
|
3 => GsmtapType::LteRrc(LteRrcSubtype::MCCH),
|
||||||
@@ -36,7 +43,12 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
6 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
|
6 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
|
||||||
7 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
|
7 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
|
||||||
8 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch),
|
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() {
|
0x09 | 0x0c => match packet.get_pdu_num() {
|
||||||
8 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
8 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
||||||
@@ -47,7 +59,12 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
13 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
|
13 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
|
||||||
14 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
|
14 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
|
||||||
15 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch),
|
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() {
|
0x0e..=0x10 => match packet.get_pdu_num() {
|
||||||
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
||||||
@@ -58,7 +75,12 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
7 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
|
7 => GsmtapType::LteRrc(LteRrcSubtype::DlDcch),
|
||||||
8 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
|
8 => GsmtapType::LteRrc(LteRrcSubtype::UlCcch),
|
||||||
9 => GsmtapType::LteRrc(LteRrcSubtype::UlDcch),
|
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() {
|
0x13 | 0x1a | 0x1b => match packet.get_pdu_num() {
|
||||||
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
||||||
@@ -76,8 +98,13 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
49 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb),
|
49 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb),
|
||||||
50 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb),
|
50 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb),
|
||||||
52 => GsmtapType::LteRrc(LteRrcSubtype::UlDcchNb),
|
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() {
|
0x14 | 0x18 | 0x19 => match packet.get_pdu_num() {
|
||||||
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
1 => GsmtapType::LteRrc(LteRrcSubtype::BcchBch),
|
||||||
2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch),
|
2 => GsmtapType::LteRrc(LteRrcSubtype::BcchDlSch),
|
||||||
@@ -94,12 +121,20 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
58 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb),
|
58 => GsmtapType::LteRrc(LteRrcSubtype::DlDcchNb),
|
||||||
59 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb),
|
59 => GsmtapType::LteRrc(LteRrcSubtype::UlCcchNb),
|
||||||
61 => GsmtapType::LteRrc(LteRrcSubtype::UlDcchNb),
|
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);
|
let mut header = GsmtapHeader::new(gsmtap_type);
|
||||||
// Wireshark GSMTAP only accepts 14 bits of ARFCN
|
|
||||||
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
|
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
|
||||||
header.frame_number = packet.get_sfn();
|
header.frame_number = packet.get_sfn();
|
||||||
header.subslot = packet.get_subfn();
|
header.subslot = packet.get_subfn();
|
||||||
@@ -107,18 +142,19 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
|||||||
header,
|
header,
|
||||||
payload: packet.take_payload(),
|
payload: packet.take_payload(),
|
||||||
}))
|
}))
|
||||||
},
|
}
|
||||||
LogBody::Nas4GMessage { msg, .. } => {
|
LogBody::Nas4GMessage { msg, direction, .. } => {
|
||||||
// currently we only handle "plain" (i.e. non-secure) NAS messages
|
// currently we only handle "plain" (i.e. non-secure) NAS messages
|
||||||
let header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain));
|
let mut header = GsmtapHeader::new(GsmtapType::LteNas(LteNasSubtype::Plain));
|
||||||
|
header.uplink = matches!(direction, Nas4GMessageDirection::Uplink);
|
||||||
Ok(Some(GsmtapMessage {
|
Ok(Some(GsmtapMessage {
|
||||||
header,
|
header,
|
||||||
payload: msg,
|
payload: msg,
|
||||||
}))
|
}))
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!("gsmtap_sink: ignoring unhandled log type: {:?}", value);
|
error!("gsmtap_sink: ignoring unhandled log type: {:?}", value);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-5
@@ -3,11 +3,14 @@
|
|||||||
//! here:
|
//! here:
|
||||||
//! https://github.com/P1sec/QCSuper/blob/master/docs/The%20Diag%20protocol.md#the-diag-protocol-over-usb
|
//! https://github.com/P1sec/QCSuper/blob/master/docs/The%20Diag%20protocol.md#the-diag-protocol-over-usb
|
||||||
|
|
||||||
use crc::Crc;
|
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
|
use crc::Crc;
|
||||||
use thiserror::Error;
|
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)]
|
#[derive(Debug, Clone, Error, PartialEq)]
|
||||||
pub enum HdlcError {
|
pub enum HdlcError {
|
||||||
@@ -29,7 +32,9 @@ pub fn hdlc_encapsulate(data: &[u8], crc: &Crc<u16>) -> Vec<u8> {
|
|||||||
for &b in data {
|
for &b in data {
|
||||||
match b {
|
match b {
|
||||||
MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]),
|
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),
|
_ => 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() {
|
for b in crc.checksum(data).to_le_bytes() {
|
||||||
match b {
|
match b {
|
||||||
MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]),
|
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),
|
_ => 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_lo = unescaped.pop().ok_or(HdlcError::MissingChecksum)?;
|
||||||
let checksum = [checksum_lo, checksum_hi].as_slice().get_u16_le();
|
let checksum = [checksum_lo, checksum_hi].as_slice().get_u16_le();
|
||||||
if checksum != crc.checksum(&unescaped) {
|
if checksum != crc.checksum(&unescaped) {
|
||||||
return Err(HdlcError::InvalidChecksum(checksum, crc.checksum(&unescaped)));
|
return Err(HdlcError::InvalidChecksum(
|
||||||
|
checksum,
|
||||||
|
crc.checksum(&unescaped),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(unescaped)
|
Ok(unescaped)
|
||||||
|
|||||||
+12
-5
@@ -1,9 +1,16 @@
|
|||||||
pub mod hdlc;
|
pub mod analysis;
|
||||||
pub mod diag;
|
pub mod diag;
|
||||||
pub mod diag_device;
|
|
||||||
pub mod qmdl;
|
|
||||||
pub mod log_codes;
|
|
||||||
pub mod gsmtap;
|
pub mod gsmtap;
|
||||||
pub mod gsmtap_parser;
|
pub mod gsmtap_parser;
|
||||||
|
pub mod hdlc;
|
||||||
|
pub mod log_codes;
|
||||||
pub mod pcap;
|
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;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//! Enumerates some relevant diag log codes. Copied from QCSuper
|
//! Enumerates some relevant diag log codes. Copied from QCSuper
|
||||||
|
|
||||||
|
|
||||||
// These are 2G-related log types.
|
// These are 2G-related log types.
|
||||||
|
|
||||||
pub const LOG_GSM_RR_SIGNALING_MESSAGE_C: u32 = 0x512f;
|
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_EXTENSION_SIB: u32 = 9;
|
||||||
pub const RRCLOG_SIB_CONTAINER: u32 = 10;
|
pub const RRCLOG_SIB_CONTAINER: u32 = 10;
|
||||||
|
|
||||||
|
|
||||||
// 3G layer 3 packets:
|
// 3G layer 3 packets:
|
||||||
|
|
||||||
pub const WCDMA_SIGNALLING_MESSAGE: u32 = 0x412f;
|
pub const WCDMA_SIGNALLING_MESSAGE: u32 = 0x412f;
|
||||||
|
|
||||||
|
|
||||||
// Upper layers
|
// Upper layers
|
||||||
|
|
||||||
pub const LOG_DATA_PROTOCOL_LOGGING_C: u32 = 0x11eb;
|
pub const LOG_DATA_PROTOCOL_LOGGING_C: u32 = 0x11eb;
|
||||||
|
|||||||
+38
-9
@@ -1,17 +1,18 @@
|
|||||||
//! Parse QMDL files and create a pcap file.
|
//! 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.
|
//! 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;
|
|
||||||
use crate::diag::Timestamp;
|
use crate::diag::Timestamp;
|
||||||
|
use crate::gsmtap::GsmtapMessage;
|
||||||
|
|
||||||
use tokio::io::AsyncWrite;
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use deku::prelude::*;
|
use deku::prelude::*;
|
||||||
use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
|
use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
|
||||||
use pcap_file_tokio::pcapng::blocks::interface_description::InterfaceDescriptionBlock;
|
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::pcapng::PcapNgWriter;
|
||||||
use pcap_file_tokio::PcapError;
|
use pcap_file_tokio::{Endianness, PcapError};
|
||||||
|
use std::borrow::Cow;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::io::AsyncWrite;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum GsmtapPcapError {
|
pub enum GsmtapPcapError {
|
||||||
@@ -25,7 +26,10 @@ pub enum GsmtapPcapError {
|
|||||||
Deku(#[from] DekuError),
|
Deku(#[from] DekuError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GsmtapPcapWriter<T> where T: AsyncWrite {
|
pub struct GsmtapPcapWriter<T>
|
||||||
|
where
|
||||||
|
T: AsyncWrite,
|
||||||
|
{
|
||||||
writer: PcapNgWriter<T>,
|
writer: PcapNgWriter<T>,
|
||||||
ip_id: u16,
|
ip_id: u16,
|
||||||
}
|
}
|
||||||
@@ -58,9 +62,29 @@ struct UdpHeader {
|
|||||||
checksum: u16,
|
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> {
|
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 })
|
Ok(GsmtapPcapWriter { writer, ip_id: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +98,13 @@ impl<T> GsmtapPcapWriter<T> where T: AsyncWrite + Unpin + Send {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn write_gsmtap_message(&mut self, msg: GsmtapMessage, timestamp: Timestamp) -> Result<(), GsmtapPcapError> {
|
pub async fn write_gsmtap_message(
|
||||||
let duration = timestamp.to_datetime()
|
&mut self,
|
||||||
|
msg: GsmtapMessage,
|
||||||
|
timestamp: Timestamp,
|
||||||
|
) -> Result<(), GsmtapPcapError> {
|
||||||
|
let duration = timestamp
|
||||||
|
.to_datetime()
|
||||||
.signed_duration_since(DateTime::UNIX_EPOCH)
|
.signed_duration_since(DateTime::UNIX_EPOCH)
|
||||||
.to_std()?;
|
.to_std()?;
|
||||||
|
|
||||||
|
|||||||
+66
-31
@@ -3,18 +3,24 @@
|
|||||||
//! QmdlReader and QmdlWriter can read and write MessagesContainers to and from
|
//! QmdlReader and QmdlWriter can read and write MessagesContainers to and from
|
||||||
//! QMDL files.
|
//! QMDL files.
|
||||||
|
|
||||||
use crate::diag::{MessagesContainer, MESSAGE_TERMINATOR, HdlcEncapsulatedMessage, DataType};
|
use crate::diag::{DataType, HdlcEncapsulatedMessage, MessagesContainer, MESSAGE_TERMINATOR};
|
||||||
|
|
||||||
use futures::TryStream;
|
use futures::TryStream;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, AsyncBufReadExt};
|
|
||||||
use log::error;
|
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,
|
writer: T,
|
||||||
pub total_written: usize,
|
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 {
|
pub fn new(writer: T) -> Self {
|
||||||
QmdlWriter::new_with_existing_size(writer, 0)
|
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>,
|
reader: BufReader<T>,
|
||||||
bytes_read: usize,
|
bytes_read: usize,
|
||||||
max_bytes: Option<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 {
|
pub fn new(reader: T, max_bytes: Option<usize>) -> Self {
|
||||||
QmdlReader {
|
QmdlReader {
|
||||||
reader: BufReader::new(reader),
|
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 {
|
futures::stream::try_unfold(self, |reader| async {
|
||||||
let maybe_container = reader.get_next_messages_container().await?;
|
let maybe_container = reader.get_next_messages_container().await?;
|
||||||
match maybe_container {
|
match maybe_container {
|
||||||
Some(container) => Ok(Some((container, reader))),
|
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 let Some(max_bytes) = self.max_bytes {
|
||||||
if self.bytes_read >= max_bytes {
|
if self.bytes_read >= max_bytes {
|
||||||
if self.bytes_read > max_bytes {
|
if self.bytes_read > max_bytes {
|
||||||
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);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -82,12 +101,10 @@ impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
|
|||||||
Ok(Some(MessagesContainer {
|
Ok(Some(MessagesContainer {
|
||||||
data_type: DataType::UserSpace,
|
data_type: DataType::UserSpace,
|
||||||
num_messages: 1,
|
num_messages: 1,
|
||||||
messages: vec![
|
messages: vec![HdlcEncapsulatedMessage {
|
||||||
HdlcEncapsulatedMessage {
|
len: bytes_read as u32,
|
||||||
len: bytes_read as u32,
|
data: buf,
|
||||||
data: buf,
|
}],
|
||||||
},
|
|
||||||
]
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,26 +113,29 @@ impl<T> QmdlReader<T> where T: AsyncRead + Unpin {
|
|||||||
mod test {
|
mod test {
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use crate::hdlc::hdlc_encapsulate;
|
|
||||||
use crate::diag::CRC_CCITT;
|
use crate::diag::CRC_CCITT;
|
||||||
|
use crate::hdlc::hdlc_encapsulate;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn get_test_messages() -> Vec<HdlcEncapsulatedMessage> {
|
fn get_test_messages() -> Vec<HdlcEncapsulatedMessage> {
|
||||||
let messages: Vec<HdlcEncapsulatedMessage> = (10..20).map(|i| {
|
let messages: Vec<HdlcEncapsulatedMessage> = (10..20)
|
||||||
let data = hdlc_encapsulate(&vec![i as u8; i], &CRC_CCITT);
|
.map(|i| {
|
||||||
HdlcEncapsulatedMessage {
|
let data = hdlc_encapsulate(&vec![i as u8; i], &CRC_CCITT);
|
||||||
len: data.len() as u32,
|
HdlcEncapsulatedMessage {
|
||||||
data,
|
len: data.len() as u32,
|
||||||
}
|
data,
|
||||||
}).collect();
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
messages
|
messages
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a byte array consisting of concatenated HDLC encapsulated
|
// returns a byte array consisting of concatenated HDLC encapsulated
|
||||||
// test messages
|
// test messages
|
||||||
fn get_test_message_bytes() -> Vec<u8> {
|
fn get_test_message_bytes() -> Vec<u8> {
|
||||||
get_test_messages().iter()
|
get_test_messages()
|
||||||
|
.iter()
|
||||||
.flat_map(|msg| msg.data.clone())
|
.flat_map(|msg| msg.data.clone())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -132,7 +152,7 @@ mod test {
|
|||||||
MessagesContainer {
|
MessagesContainer {
|
||||||
data_type: DataType::UserSpace,
|
data_type: DataType::UserSpace,
|
||||||
num_messages: messages2.len() as u32,
|
num_messages: messages2.len() as u32,
|
||||||
messages: messages2.to_vec()
|
messages: messages2.to_vec(),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -148,7 +168,10 @@ mod test {
|
|||||||
num_messages: 1,
|
num_messages: 1,
|
||||||
messages: vec![message],
|
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,
|
num_messages: 1,
|
||||||
messages: vec![message],
|
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]
|
#[tokio::test]
|
||||||
@@ -202,8 +231,14 @@ mod test {
|
|||||||
num_messages: 1,
|
num_messages: 1,
|
||||||
messages: vec![message],
|
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)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+359
-349
@@ -1,42 +1,46 @@
|
|||||||
use rayhunter::{diag::{
|
|
||||||
LogBody, LteRrcOtaPacket, Message, Timestamp
|
|
||||||
}, gsmtap_parser};
|
|
||||||
use deku::prelude::*;
|
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
|
// Tests here are based on https://github.com/fgsect/scat/blob/97442580e628de414c9f7c2a185f4e28d0ee7523/tests/test_diagltelogparser.py
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lte_rrc_ota() {
|
fn test_lte_rrc_ota() {
|
||||||
let v26_binary = &[
|
let v26_binary = &[
|
||||||
0x10, 0x0, 0x23, 0x0, 0x23, 0x0, 0xc0, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
0x10, 0x0, 0x23, 0x0, 0x23, 0x0, 0xc0, 0xb0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1a,
|
||||||
0x0, 0x0, 0x0, 0x1a, 0xf, 0x40, 0xf, 0x40, 0x1, 0xe, 0x1, 0x13, 0x7,
|
0xf, 0x40, 0xf, 0x40, 0x1, 0xe, 0x1, 0x13, 0x7, 0x0, 0x0, 0x0, 0x0, 0xb, 0x0, 0x0, 0x0,
|
||||||
0x0, 0x0, 0x0, 0x0, 0xb, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x10, 0x15
|
0x0, 0x2, 0x0, 0x10, 0x15,
|
||||||
];
|
];
|
||||||
let (_, parsed) = Message::from_bytes((v26_binary, 0)).unwrap();
|
let (_, parsed) = Message::from_bytes((v26_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 0x23,
|
&Message::Log {
|
||||||
inner_length: 0x23,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 0x23,
|
||||||
log_type: 0xb0c0,
|
inner_length: 0x23,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 26,
|
log_type: 0xb0c0,
|
||||||
packet: LteRrcOtaPacket::V25 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 15,
|
ext_header_version: 26,
|
||||||
rrc_rel_min: 64,
|
packet: LteRrcOtaPacket::V25 {
|
||||||
nr_rrc_rel_maj: 15,
|
rrc_rel_maj: 15,
|
||||||
nr_rrc_rel_min: 64,
|
rrc_rel_min: 64,
|
||||||
bearer_id: 1,
|
nr_rrc_rel_maj: 15,
|
||||||
phy_cell_id: 270,
|
nr_rrc_rel_min: 64,
|
||||||
earfcn: 1811,
|
bearer_id: 1,
|
||||||
sfn_subfn: 0,
|
phy_cell_id: 270,
|
||||||
pdu_num: 11,
|
earfcn: 1811,
|
||||||
sib_mask: 0,
|
sfn_subfn: 0,
|
||||||
len: 2,
|
pdu_num: 11,
|
||||||
packet: vec![0x10, 0x15],
|
sib_mask: 0,
|
||||||
|
len: 2,
|
||||||
|
packet: vec![0x10, 0x15],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
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.packet_type, 13);
|
||||||
@@ -49,41 +53,40 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 0);
|
assert_eq!(gsmtap_msg.header.subslot, 0);
|
||||||
|
|
||||||
let v26_binary = &[
|
let v26_binary = &[
|
||||||
0x10, 0x00, 0x23, 0x00, 0x23, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x23, 0x00, 0x23, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x1a, 0x0f, 0x40, 0x0f, 0x40, 0x01, 0x0e, 0x01, 0x13, 0x07, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x1a, 0x0f, 0x40, 0x0f, 0x40, 0x01, 0x0e, 0x01,
|
0x0b, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x10, 0x15,
|
||||||
0x13, 0x07, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x02, 0x00, 0x10, 0x15,
|
|
||||||
];
|
];
|
||||||
let (_, parsed) = Message::from_bytes((v26_binary, 0)).unwrap();
|
let (_, parsed) = Message::from_bytes((v26_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 35,
|
&Message::Log {
|
||||||
inner_length: 35,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 35,
|
||||||
log_type: 45248,
|
inner_length: 35,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 26,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V25 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 15,
|
ext_header_version: 26,
|
||||||
rrc_rel_min: 64,
|
packet: LteRrcOtaPacket::V25 {
|
||||||
nr_rrc_rel_maj: 15,
|
rrc_rel_maj: 15,
|
||||||
nr_rrc_rel_min: 64,
|
rrc_rel_min: 64,
|
||||||
bearer_id: 1,
|
nr_rrc_rel_maj: 15,
|
||||||
phy_cell_id: 270,
|
nr_rrc_rel_min: 64,
|
||||||
earfcn: 1811,
|
bearer_id: 1,
|
||||||
sfn_subfn: 0,
|
phy_cell_id: 270,
|
||||||
pdu_num: 11,
|
earfcn: 1811,
|
||||||
sib_mask: 0,
|
sfn_subfn: 0,
|
||||||
len: 2,
|
pdu_num: 11,
|
||||||
packet: vec![0x10, 0x15],
|
sib_mask: 0,
|
||||||
|
len: 2,
|
||||||
|
packet: vec![0x10, 0x15],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[
|
assert_eq!(&gsmtap_msg.payload, &[0x10, 0x15,]);
|
||||||
0x10, 0x15,
|
|
||||||
]);
|
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
||||||
assert_eq!(gsmtap_msg.header.arfcn, 1811);
|
assert_eq!(gsmtap_msg.header.arfcn, 1811);
|
||||||
@@ -94,44 +97,44 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 0);
|
assert_eq!(gsmtap_msg.header.subslot, 0);
|
||||||
|
|
||||||
let v24_binary = &[
|
let v24_binary = &[
|
||||||
0x10, 0x00, 0x2c, 0x00, 0x2c, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x2c, 0x00, 0x2c, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x18, 0x0f, 0x22, 0x00, 0x68, 0x00, 0xe4, 0x0c, 0x00, 0x00, 0x09, 0xdc, 0x05, 0x00,
|
||||||
0x18, 0x0f, 0x22, 0x00, 0x68, 0x00, 0xe4, 0x0c,
|
0x00, 0x00, 0x00, 0x0d, 0x00, 0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29,
|
||||||
0x00, 0x00, 0x09, 0xdc, 0x05, 0x00, 0x00, 0x00,
|
0x15, 0x16, 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();
|
let (_, parsed) = Message::from_bytes((v24_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 44,
|
&Message::Log {
|
||||||
inner_length: 44,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 44,
|
||||||
log_type: 45248,
|
inner_length: 44,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 24,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 15,
|
ext_header_version: 24,
|
||||||
rrc_rel_min: 34,
|
packet: LteRrcOtaPacket::V8 {
|
||||||
bearer_id: 0,
|
rrc_rel_maj: 15,
|
||||||
phy_cell_id: 104,
|
rrc_rel_min: 34,
|
||||||
earfcn: 3300,
|
bearer_id: 0,
|
||||||
sfn_subfn: 56329,
|
phy_cell_id: 104,
|
||||||
pdu_num: 5,
|
earfcn: 3300,
|
||||||
sib_mask: 0,
|
sfn_subfn: 56329,
|
||||||
len: 13,
|
pdu_num: 5,
|
||||||
packet: vec![
|
sib_mask: 0,
|
||||||
0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29,
|
len: 13,
|
||||||
0x15, 0x16, 0x0
|
packet: vec![
|
||||||
],
|
0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50, 0xdc, 0x29, 0x15, 0x16, 0x0
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[
|
assert_eq!(
|
||||||
0x40, 0x85, 0x8e, 0xc4, 0xe5, 0xbf, 0xe0, 0x50,
|
&gsmtap_msg.payload,
|
||||||
0xdc, 0x29, 0x15, 0x16, 0x00,
|
&[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.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
||||||
assert_eq!(gsmtap_msg.header.arfcn, 3300);
|
assert_eq!(gsmtap_msg.header.arfcn, 3300);
|
||||||
@@ -142,48 +145,48 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 9);
|
assert_eq!(gsmtap_msg.header.subslot, 9);
|
||||||
|
|
||||||
let v20_binary = &[
|
let v20_binary = &[
|
||||||
0x10, 0x00, 0x37, 0x00, 0x37, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x37, 0x00, 0x37, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x14, 0x0e, 0x30, 0x01, 0x09, 0x01, 0x9c, 0x18, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00,
|
||||||
0x14, 0x0e, 0x30, 0x01, 0x09, 0x01, 0x9c, 0x18,
|
0x00, 0x00, 0x00, 0x18, 0x00, 0x08, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x05, 0x43, 0x68,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
|
0xc0, 0x3b, 0xda, 0x30, 0x04, 0xa6, 0x88, 0x02, 0x8d, 0xa2, 0x00, 0x9a, 0x68, 0x40,
|
||||||
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();
|
let (_, parsed) = Message::from_bytes((v20_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 55,
|
&Message::Log {
|
||||||
inner_length: 55,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 55,
|
||||||
log_type: 45248,
|
inner_length: 55,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 20,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 14,
|
ext_header_version: 20,
|
||||||
rrc_rel_min: 48,
|
packet: LteRrcOtaPacket::V8 {
|
||||||
bearer_id: 1,
|
rrc_rel_maj: 14,
|
||||||
phy_cell_id: 265,
|
rrc_rel_min: 48,
|
||||||
earfcn: 6300,
|
bearer_id: 1,
|
||||||
sfn_subfn: 0,
|
phy_cell_id: 265,
|
||||||
pdu_num: 9,
|
earfcn: 6300,
|
||||||
sib_mask: 0,
|
sfn_subfn: 0,
|
||||||
len: 24,
|
pdu_num: 9,
|
||||||
packet: vec![
|
sib_mask: 0,
|
||||||
0x8, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x5, 0x43, 0x68,
|
len: 24,
|
||||||
0xc0, 0x3b, 0xda, 0x30, 0x4, 0xa6, 0x88, 0x2, 0x8d, 0xa2,
|
packet: vec![
|
||||||
0x0, 0x9a, 0x68, 0x40
|
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();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[
|
assert_eq!(
|
||||||
0x08, 0x10, 0xa7, 0x14, 0x53, 0x59, 0xa6, 0x05,
|
&gsmtap_msg.payload,
|
||||||
0x43, 0x68, 0xc0, 0x3b, 0xda, 0x30, 0x04, 0xa6,
|
&[
|
||||||
0x88, 0x02, 0x8d, 0xa2, 0x00, 0x9a, 0x68, 0x40,
|
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.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
||||||
assert_eq!(gsmtap_msg.header.arfcn, 6300);
|
assert_eq!(gsmtap_msg.header.arfcn, 6300);
|
||||||
@@ -194,41 +197,41 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 0);
|
assert_eq!(gsmtap_msg.header.subslot, 0);
|
||||||
|
|
||||||
let v19_binary = &[
|
let v19_binary = &[
|
||||||
0x10, 0x00, 0x28, 0x00, 0x28, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x28, 0x00, 0x28, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x13, 0x0e, 0x22, 0x00, 0x0b, 0x00, 0xfa, 0x09, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00,
|
||||||
0x13, 0x0e, 0x22, 0x00, 0x0b, 0x00, 0xfa, 0x09,
|
0x00, 0x00, 0x00, 0x09, 0x00, 0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00, 0x00,
|
||||||
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();
|
let (_, parsed) = Message::from_bytes((v19_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 40,
|
&Message::Log {
|
||||||
inner_length: 40,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 40,
|
||||||
log_type: 45248,
|
inner_length: 40,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 19,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 14,
|
ext_header_version: 19,
|
||||||
rrc_rel_min: 34,
|
packet: LteRrcOtaPacket::V8 {
|
||||||
bearer_id: 0,
|
rrc_rel_maj: 14,
|
||||||
phy_cell_id: 11,
|
rrc_rel_min: 34,
|
||||||
earfcn: 2554,
|
bearer_id: 0,
|
||||||
sfn_subfn: 0,
|
phy_cell_id: 11,
|
||||||
pdu_num: 50,
|
earfcn: 2554,
|
||||||
sib_mask: 0,
|
sfn_subfn: 0,
|
||||||
len: 9,
|
pdu_num: 50,
|
||||||
packet: vec![0x28, 0x18, 0x40, 0x16, 0x8, 0x8, 0x80, 0x0, 0x0],
|
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();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[
|
assert_eq!(
|
||||||
0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00,
|
&gsmtap_msg.payload,
|
||||||
0x00,
|
&[0x28, 0x18, 0x40, 0x16, 0x08, 0x08, 0x80, 0x00, 0x00,]
|
||||||
]);
|
);
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
||||||
assert_eq!(gsmtap_msg.header.arfcn, 2554);
|
assert_eq!(gsmtap_msg.header.arfcn, 2554);
|
||||||
@@ -239,40 +242,41 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 0);
|
assert_eq!(gsmtap_msg.header.subslot, 0);
|
||||||
|
|
||||||
let v15_binary = &[
|
let v15_binary = &[
|
||||||
0x10, 0x00, 0x26, 0x00, 0x26, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x26, 0x00, 0x26, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x0f, 0x0d, 0x21, 0x00, 0x9e, 0x00, 0x14, 0x05, 0x00, 0x00, 0x49, 0x8c, 0x05, 0x00,
|
||||||
0x0f, 0x0d, 0x21, 0x00, 0x9e, 0x00, 0x14, 0x05,
|
0x00, 0x00, 0x00, 0x07, 0x00, 0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0,
|
||||||
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();
|
let (_, parsed) = Message::from_bytes((v15_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 38,
|
&Message::Log {
|
||||||
inner_length: 38,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 38,
|
||||||
log_type: 45248,
|
inner_length: 38,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 15,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 13,
|
ext_header_version: 15,
|
||||||
rrc_rel_min: 33,
|
packet: LteRrcOtaPacket::V8 {
|
||||||
bearer_id: 0,
|
rrc_rel_maj: 13,
|
||||||
phy_cell_id: 158,
|
rrc_rel_min: 33,
|
||||||
earfcn: 1300,
|
bearer_id: 0,
|
||||||
sfn_subfn: 35913,
|
phy_cell_id: 158,
|
||||||
pdu_num: 5,
|
earfcn: 1300,
|
||||||
sib_mask: 0,
|
sfn_subfn: 35913,
|
||||||
len: 7,
|
pdu_num: 5,
|
||||||
packet: vec![0x40, 0xc, 0x8e, 0xc9, 0x42, 0x89, 0xe0],
|
sib_mask: 0,
|
||||||
|
len: 7,
|
||||||
|
packet: vec![0x40, 0xc, 0x8e, 0xc9, 0x42, 0x89, 0xe0],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[
|
assert_eq!(
|
||||||
0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0,
|
&gsmtap_msg.payload,
|
||||||
]);
|
&[0x40, 0x0c, 0x8e, 0xc9, 0x42, 0x89, 0xe0,]
|
||||||
|
);
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
||||||
assert_eq!(gsmtap_msg.header.arfcn, 1300);
|
assert_eq!(gsmtap_msg.header.arfcn, 1300);
|
||||||
@@ -283,49 +287,50 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 9);
|
assert_eq!(gsmtap_msg.header.subslot, 9);
|
||||||
|
|
||||||
let v15_binary = &[
|
let v15_binary = &[
|
||||||
0x10, 0x00, 0x3b, 0x00, 0x3b, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x3b, 0x00, 0x3b, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x0f, 0x0d, 0x21, 0x01, 0x9e, 0x00, 0x14, 0x05, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00,
|
||||||
0x0f, 0x0d, 0x21, 0x01, 0x9e, 0x00, 0x14, 0x05,
|
0x00, 0x00, 0x00, 0x1c, 0x00, 0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
|
0x04, 0x40, 0x1a, 0x00, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f, 0x00, 0x10, 0x67, 0xc1, 0x06,
|
||||||
0x00, 0x1c, 0x00, 0x08, 0x10, 0xa5, 0x34, 0x61,
|
0xd9, 0xe0, 0x00,
|
||||||
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();
|
let (_, parsed) = Message::from_bytes((v15_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 59,
|
&Message::Log {
|
||||||
inner_length: 59,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 59,
|
||||||
log_type: 45248,
|
inner_length: 59,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 15,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 13,
|
ext_header_version: 15,
|
||||||
rrc_rel_min: 33,
|
packet: LteRrcOtaPacket::V8 {
|
||||||
bearer_id: 1,
|
rrc_rel_maj: 13,
|
||||||
phy_cell_id: 158,
|
rrc_rel_min: 33,
|
||||||
earfcn: 1300,
|
bearer_id: 1,
|
||||||
sfn_subfn: 0,
|
phy_cell_id: 158,
|
||||||
pdu_num: 9,
|
earfcn: 1300,
|
||||||
sib_mask: 0,
|
sfn_subfn: 0,
|
||||||
len: 28,
|
pdu_num: 9,
|
||||||
packet: vec![
|
sib_mask: 0,
|
||||||
0x8, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68,
|
len: 28,
|
||||||
0x4, 0x40, 0x1a, 0x0, 0x49, 0x16, 0x7c, 0x23, 0x15, 0x9f,
|
packet: vec![
|
||||||
0x0, 0x10, 0x67, 0xc1, 0x6, 0xd9, 0xe0, 0x0
|
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();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[
|
assert_eq!(
|
||||||
0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c,
|
&gsmtap_msg.payload,
|
||||||
0x31, 0x68, 0x04, 0x40, 0x1a, 0x00, 0x49, 0x16,
|
&[
|
||||||
0x7c, 0x23, 0x15, 0x9f, 0x00, 0x10, 0x67, 0xc1,
|
0x08, 0x10, 0xa5, 0x34, 0x61, 0x41, 0xa3, 0x1c, 0x31, 0x68, 0x04, 0x40, 0x1a, 0x00,
|
||||||
0x06, 0xd9, 0xe0, 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.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
||||||
assert_eq!(gsmtap_msg.header.arfcn, 1300);
|
assert_eq!(gsmtap_msg.header.arfcn, 1300);
|
||||||
@@ -336,35 +341,36 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 0);
|
assert_eq!(gsmtap_msg.header.subslot, 0);
|
||||||
|
|
||||||
let v13_binary = &[
|
let v13_binary = &[
|
||||||
0x10, 0x00, 0x21, 0x00, 0x21, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x21, 0x00, 0x21, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x0d, 0x0c, 0x74, 0x01, 0x32, 0x00, 0x38, 0x18, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00,
|
||||||
0x0d, 0x0c, 0x74, 0x01, 0x32, 0x00, 0x38, 0x18,
|
0x00, 0x00, 0x00, 0x02, 0x00, 0x2c, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x02, 0x00, 0x2c, 0x00,
|
|
||||||
];
|
];
|
||||||
let (_, parsed) = Message::from_bytes((v13_binary, 0)).unwrap();
|
let (_, parsed) = Message::from_bytes((v13_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 33,
|
&Message::Log {
|
||||||
inner_length: 33,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 33,
|
||||||
log_type: 45248,
|
inner_length: 33,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 13,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 12,
|
ext_header_version: 13,
|
||||||
rrc_rel_min: 116,
|
packet: LteRrcOtaPacket::V8 {
|
||||||
bearer_id: 1,
|
rrc_rel_maj: 12,
|
||||||
phy_cell_id: 50,
|
rrc_rel_min: 116,
|
||||||
earfcn: 6200,
|
bearer_id: 1,
|
||||||
sfn_subfn: 0,
|
phy_cell_id: 50,
|
||||||
pdu_num: 8,
|
earfcn: 6200,
|
||||||
sib_mask: 0,
|
sfn_subfn: 0,
|
||||||
len: 2,
|
pdu_num: 8,
|
||||||
packet: vec![0x2c, 0x0],
|
sib_mask: 0,
|
||||||
|
len: 2,
|
||||||
|
packet: vec![0x2c, 0x0],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[0x2c, 0x00]);
|
assert_eq!(&gsmtap_msg.payload, &[0x2c, 0x00]);
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
@@ -377,40 +383,41 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 0);
|
assert_eq!(gsmtap_msg.header.subslot, 0);
|
||||||
|
|
||||||
let v9_binary = &[
|
let v9_binary = &[
|
||||||
0x10, 0x00, 0x26, 0x00, 0x26, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x26, 0x00, 0x26, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x09, 0x0b, 0x70, 0x00, 0x00, 0x01, 0x14, 0x05, 0x00, 0x00, 0x09, 0x91, 0x0b, 0x00,
|
||||||
0x09, 0x0b, 0x70, 0x00, 0x00, 0x01, 0x14, 0x05,
|
0x00, 0x00, 0x00, 0x07, 0x00, 0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0,
|
||||||
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();
|
let (_, parsed) = Message::from_bytes((v9_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 38,
|
&Message::Log {
|
||||||
inner_length: 38,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 38,
|
||||||
log_type: 45248,
|
inner_length: 38,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 9,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 11,
|
ext_header_version: 9,
|
||||||
rrc_rel_min: 112,
|
packet: LteRrcOtaPacket::V8 {
|
||||||
bearer_id: 0,
|
rrc_rel_maj: 11,
|
||||||
phy_cell_id: 256,
|
rrc_rel_min: 112,
|
||||||
earfcn: 1300,
|
bearer_id: 0,
|
||||||
sfn_subfn: 37129,
|
phy_cell_id: 256,
|
||||||
pdu_num: 11,
|
earfcn: 1300,
|
||||||
sib_mask: 0,
|
sfn_subfn: 37129,
|
||||||
len: 7,
|
pdu_num: 11,
|
||||||
packet: vec![0x40, 0xb, 0x8e, 0xc1, 0xdd, 0x13, 0xb0],
|
sib_mask: 0,
|
||||||
|
len: 7,
|
||||||
|
packet: vec![0x40, 0xb, 0x8e, 0xc1, 0xdd, 0x13, 0xb0],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[
|
assert_eq!(
|
||||||
0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0,
|
&gsmtap_msg.payload,
|
||||||
]);
|
&[0x40, 0x0b, 0x8e, 0xc1, 0xdd, 0x13, 0xb0,]
|
||||||
|
);
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
||||||
assert_eq!(gsmtap_msg.header.arfcn, 1300);
|
assert_eq!(gsmtap_msg.header.arfcn, 1300);
|
||||||
@@ -421,35 +428,36 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 9);
|
assert_eq!(gsmtap_msg.header.subslot, 9);
|
||||||
|
|
||||||
let v8_binary = &[
|
let v8_binary = &[
|
||||||
0x10, 0x00, 0x21, 0x00, 0x21, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x21, 0x00, 0x21, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x08, 0x0a, 0x72, 0x01, 0x0e, 0x00, 0x9c, 0x18, 0x00, 0x00, 0xa9, 0x33, 0x06, 0x00,
|
||||||
0x08, 0x0a, 0x72, 0x01, 0x0e, 0x00, 0x9c, 0x18,
|
0x00, 0x00, 0x00, 0x02, 0x00, 0x2e, 0x02,
|
||||||
0x00, 0x00, 0xa9, 0x33, 0x06, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x02, 0x00, 0x2e, 0x02,
|
|
||||||
];
|
];
|
||||||
let (_, parsed) = Message::from_bytes((v8_binary, 0)).unwrap();
|
let (_, parsed) = Message::from_bytes((v8_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 33,
|
&Message::Log {
|
||||||
inner_length: 33,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 33,
|
||||||
log_type: 45248,
|
inner_length: 33,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 8,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V8 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 10,
|
ext_header_version: 8,
|
||||||
rrc_rel_min: 114,
|
packet: LteRrcOtaPacket::V8 {
|
||||||
bearer_id: 1,
|
rrc_rel_maj: 10,
|
||||||
phy_cell_id: 14,
|
rrc_rel_min: 114,
|
||||||
earfcn: 6300,
|
bearer_id: 1,
|
||||||
sfn_subfn: 13225,
|
phy_cell_id: 14,
|
||||||
pdu_num: 6,
|
earfcn: 6300,
|
||||||
sib_mask: 0,
|
sfn_subfn: 13225,
|
||||||
len: 2,
|
pdu_num: 6,
|
||||||
packet: vec![0x2e, 0x2],
|
sib_mask: 0,
|
||||||
|
len: 2,
|
||||||
|
packet: vec![0x2e, 0x2],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
);
|
||||||
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[0x2e, 0x02]);
|
assert_eq!(&gsmtap_msg.payload, &[0x2e, 0x02]);
|
||||||
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
assert_eq!(gsmtap_msg.header.packet_type, 13);
|
||||||
@@ -462,46 +470,48 @@ fn test_lte_rrc_ota() {
|
|||||||
assert_eq!(gsmtap_msg.header.subslot, 9);
|
assert_eq!(gsmtap_msg.header.subslot, 9);
|
||||||
|
|
||||||
let v6_binary = &[
|
let v6_binary = &[
|
||||||
0x10, 0x00, 0x2f, 0x00, 0x2f, 0x00, 0xc0, 0xb0,
|
0x10, 0x00, 0x2f, 0x00, 0x2f, 0x00, 0xc0, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00, 0x06, 0x09, 0xb1, 0x00, 0x07, 0x01, 0x2c, 0x07, 0x25, 0x34, 0x02, 0x02, 0x00, 0x00,
|
||||||
0x06, 0x09, 0xb1, 0x00, 0x07, 0x01, 0x2c, 0x07,
|
0x00, 0x12, 0x00, 0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02, 0xd3, 0xb0, 0x98, 0x1c, 0x20,
|
||||||
0x25, 0x34, 0x02, 0x02, 0x00, 0x00, 0x00, 0x12,
|
0xa0, 0x81, 0x8c, 0x43, 0x26, 0xd0,
|
||||||
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();
|
let (_, parsed) = Message::from_bytes((v6_binary, 0)).unwrap();
|
||||||
assert_eq!(&parsed, &Message::Log {
|
assert_eq!(
|
||||||
pending_msgs: 0,
|
&parsed,
|
||||||
outer_length: 47,
|
&Message::Log {
|
||||||
inner_length: 47,
|
pending_msgs: 0,
|
||||||
timestamp: Timestamp { ts: 0 },
|
outer_length: 47,
|
||||||
log_type: 45248,
|
inner_length: 47,
|
||||||
body: LogBody::LteRrcOtaMessage {
|
timestamp: Timestamp { ts: 0 },
|
||||||
ext_header_version: 6,
|
log_type: 45248,
|
||||||
packet: LteRrcOtaPacket::V5 {
|
body: LogBody::LteRrcOtaMessage {
|
||||||
rrc_rel_maj: 9,
|
ext_header_version: 6,
|
||||||
rrc_rel_min: 177,
|
packet: LteRrcOtaPacket::V5 {
|
||||||
bearer_id: 0,
|
rrc_rel_maj: 9,
|
||||||
phy_cell_id: 263,
|
rrc_rel_min: 177,
|
||||||
earfcn: 1836,
|
bearer_id: 0,
|
||||||
sfn_subfn: 13349,
|
phy_cell_id: 263,
|
||||||
pdu_num: 2,
|
earfcn: 1836,
|
||||||
sib_mask: 2,
|
sfn_subfn: 13349,
|
||||||
len: 18,
|
pdu_num: 2,
|
||||||
packet: vec![
|
sib_mask: 2,
|
||||||
0x40, 0x49, 0x88, 0x5, 0xc0, 0x97, 0x2, 0xd3, 0xb0, 0x98,
|
len: 18,
|
||||||
0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43, 0x26, 0xd0
|
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();
|
let (_, gsmtap_msg) = gsmtap_parser::parse(parsed).unwrap().unwrap();
|
||||||
assert_eq!(&gsmtap_msg.payload, &[
|
assert_eq!(
|
||||||
0x40, 0x49, 0x88, 0x05, 0xc0, 0x97, 0x02, 0xd3,
|
&gsmtap_msg.payload,
|
||||||
0xb0, 0x98, 0x1c, 0x20, 0xa0, 0x81, 0x8c, 0x43,
|
&[
|
||||||
0x26, 0xd0,
|
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.packet_type, 13);
|
||||||
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
assert_eq!(gsmtap_msg.header.timeslot, 0);
|
||||||
assert_eq!(gsmtap_msg.header.arfcn, 1836);
|
assert_eq!(gsmtap_msg.header.arfcn, 1836);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cargo build --release --target="armv7-unknown-linux-gnueabihf"
|
cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug
|
||||||
|
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
|
||||||
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
adb push target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
||||||
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon restart"'
|
echo "rebooting the device..."
|
||||||
|
adb shell '/bin/rootshell -c "reboot"'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rootshell"
|
name = "rootshell"
|
||||||
version = "0.1.0"
|
version = "0.2.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
+21
-18
@@ -2,29 +2,32 @@
|
|||||||
//!
|
//!
|
||||||
//! It literally just runs bash as UID/GID 0, with special Android GIDs 3003
|
//! It literally just runs bash as UID/GID 0, with special Android GIDs 3003
|
||||||
//! (AID_INET) and 3004 (AID_NET_RAW).
|
//! (AID_INET) and 3004 (AID_NET_RAW).
|
||||||
use std::process::Command;
|
|
||||||
use std::os::unix::process::CommandExt;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "arm")]
|
||||||
use nix::unistd::Gid;
|
use nix::unistd::Gid;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
|
|
||||||
// Android's "paranoid network" feature restricts network access to
|
// Android's "paranoid network" feature restricts network access to
|
||||||
// processes in specific groups. More info here:
|
// processes in specific groups. More info here:
|
||||||
// https://www.elinux.org/Android_Security#Paranoid_network-ing
|
// https://www.elinux.org/Android_Security#Paranoid_network-ing
|
||||||
let gids = &[
|
#[cfg(target_arch = "arm")]
|
||||||
Gid::from_raw(3003), // AID_INET
|
{
|
||||||
Gid::from_raw(3004), // AID_NET_RAW
|
let gids = &[
|
||||||
];
|
Gid::from_raw(3003), // AID_INET
|
||||||
nix::unistd::setgroups(gids).expect("setgroups failed");
|
Gid::from_raw(3004), // AID_NET_RAW
|
||||||
|
];
|
||||||
|
nix::unistd::setgroups(gids).expect("setgroups failed");
|
||||||
|
}
|
||||||
|
|
||||||
// discard argv[0]
|
// discard argv[0]
|
||||||
let _ = args.next();
|
let _ = args.next();
|
||||||
Command::new("/bin/bash")
|
// This call will only return if there is an error
|
||||||
.args(args)
|
let error = Command::new("/bin/bash").args(args).uid(0).gid(0).exec();
|
||||||
.uid(0)
|
eprintln!("Error running command: {error}");
|
||||||
.gid(0)
|
std::process::exit(1);
|
||||||
.exec();
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,10 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "serial"
|
name = "serial"
|
||||||
version = "0.1.0"
|
version = "0.2.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rusb = "0.9.3"
|
anyhow = "1.0.97"
|
||||||
|
nusb = "0.1.13"
|
||||||
|
tokio = { version = "1.44.2", features = ["macros", "rt", "time"] }
|
||||||
|
|||||||
+113
-96
@@ -3,154 +3,171 @@
|
|||||||
//! This binary has two main functions, putting the orbic device in update mode which enables ADB
|
//! 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
|
//! 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
|
//! 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.
|
//! 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::str;
|
||||||
use std::thread::sleep;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use rusb::{
|
use anyhow::{bail, Context, Result};
|
||||||
Context, DeviceHandle, UsbContext,
|
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();
|
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>", args[0]);
|
println!("usage: {0} [<command> | --root]", args[0]);
|
||||||
return;
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
match Context::new() {
|
if args[1] == "--root" {
|
||||||
Ok(mut context) => match open_orbic(&mut context) {
|
enable_command_mode()
|
||||||
Some(mut handle) => {
|
} else {
|
||||||
send_command(&mut handle, &args[1])
|
match open_orbic()? {
|
||||||
},
|
Some(interface) => send_command(interface, &args[1]).await,
|
||||||
None => panic!("No Orbic device found"),
|
None => bail!(ORBIC_NOT_FOUND),
|
||||||
},
|
}
|
||||||
Err(e) => panic!("Failed to initialize libusb: {0}", e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an AT command to the usb device over the serial port
|
/// Sends an AT command to the usb device over the serial port
|
||||||
///
|
///
|
||||||
/// First establish a USB handle and context by calling `open_orbic(<T>)
|
/// First establish a USB handle and context by calling `open_orbic(<T>)
|
||||||
fn send_command<T: UsbContext>(
|
async fn send_command(interface: Interface, command: &str) -> Result<()> {
|
||||||
handle: &mut DeviceHandle<T>,
|
|
||||||
command: &str,
|
|
||||||
) {
|
|
||||||
let mut data = String::new();
|
let mut data = String::new();
|
||||||
data.push_str("\r\n");
|
data.push_str("\r\n");
|
||||||
data.push_str(command);
|
data.push_str(command);
|
||||||
data.push_str("\r\n");
|
data.push_str("\r\n");
|
||||||
|
|
||||||
let timeout = Duration::from_secs(1);
|
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
|
// 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
|
// 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
|
// 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
|
// 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
|
||||||
if !responsestr.starts_with("\r\nOK\r\n") {
|
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
|
||||||
println!("Received unexpected response{0}", responsestr)
|
// the garbage with `from_utf8_lossy` and look for our expected success string.
|
||||||
|
let responsestr = String::from_utf8_lossy(&response);
|
||||||
|
if !responsestr.contains("\r\nOK\r\n") {
|
||||||
|
println!("Received unexpected response: {0}", responsestr);
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a command to switch the device into generic mode, exposing serial
|
/// Send a command to switch the device into generic mode, exposing serial
|
||||||
///
|
///
|
||||||
/// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition.
|
/// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition.
|
||||||
fn switch_device<T: UsbContext>(
|
fn enable_command_mode() -> Result<()> {
|
||||||
handle: &mut DeviceHandle<T>,
|
if open_orbic()?.is_some() {
|
||||||
) {
|
println!("Device already in command mode. Doing nothing...");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let timeout = Duration::from_secs(1);
|
let timeout = Duration::from_secs(1);
|
||||||
|
|
||||||
if let Err(e) = handle.write_control(0x40, 0xa0, 0, 0, &[], timeout) {
|
if let Some(device) = open_device(0x05c6, 0xf626)? {
|
||||||
// If the device reboots while the command is still executing we
|
let enable_command_mode = Control {
|
||||||
// may get a pipe error here
|
control_type: ControlType::Vendor,
|
||||||
if e == rusb::Error::Pipe {
|
recipient: Recipient::Device,
|
||||||
return
|
request: 0xa0,
|
||||||
}
|
value: 0,
|
||||||
panic!("Failed to send device switch control request: {0}", e)
|
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 == nusb::transfer::TransferError::Stall {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
bail!("Failed to send device switch control request: {0}", e)
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bail!(ORBIC_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle and contet for the orbic device
|
/// Get an Interface for the orbic device
|
||||||
///
|
fn open_orbic() -> Result<Option<Interface>> {
|
||||||
/// If the device isn't already in command mode this function will call swtich_device to switch it into command mode
|
|
||||||
fn open_orbic<T: UsbContext>(
|
|
||||||
context: &mut T,
|
|
||||||
) -> Option<DeviceHandle<T>> {
|
|
||||||
// Device after initial mode switch
|
// Device after initial mode switch
|
||||||
if let Some(handle) = open_device(context, 0x05c6, 0xf601) {
|
if let Some(device) = open_device(0x05c6, 0xf601)? {
|
||||||
return Some(handle)
|
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
|
// Device with rndis enabled as well
|
||||||
if let Some(handle) = open_device(context, 0x05c6, 0xf622) {
|
if let Some(device) = open_device(0x05c6, 0xf622)? {
|
||||||
return Some(handle)
|
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 in out-of-the-box state, need to switch to diag mode
|
Ok(None)
|
||||||
match open_device(context, 0x05c6, 0xf626) {
|
|
||||||
Some(mut handle) => switch_device(&mut handle),
|
|
||||||
None => panic!("No Orbic device detected")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _ in 1..10 {
|
|
||||||
if let Some(handle) = open_device(context, 0x05c6, 0xf601) {
|
|
||||||
return Some(handle)
|
|
||||||
}
|
|
||||||
sleep(Duration::from_secs(10))
|
|
||||||
}
|
|
||||||
panic!("No Orbic device detected")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic function to open a USB device
|
/// General function to open a USB device
|
||||||
fn open_device<T: UsbContext>(
|
fn open_device(vid: u16, pid: u16) -> Result<Option<Device>> {
|
||||||
context: &mut T,
|
let devices = match nusb::list_devices() {
|
||||||
vid: u16,
|
Ok(d) => d,
|
||||||
pid: u16,
|
Err(_) => return Ok(None),
|
||||||
) -> Option<DeviceHandle<T>> {
|
|
||||||
let devices = match context.devices() {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => return None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for device in devices.iter() {
|
for device in devices {
|
||||||
let device_desc = match device.device_descriptor() {
|
if device.vendor_id() == vid && device.product_id() == pid {
|
||||||
Ok(d) => d,
|
match device.open() {
|
||||||
Err(_) => continue,
|
Ok(d) => return Ok(Some(d)),
|
||||||
};
|
Err(e) => bail!("device found but failed to open: {}", e),
|
||||||
|
}
|
||||||
if device_desc.vendor_id() == vid && device_desc.product_id() == pid {
|
}
|
||||||
match device.open() {
|
|
||||||
Ok(handle) => return Some(handle),
|
|
||||||
Err(e) => panic!("device found but failed to open: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telcom-parser"
|
name = "telcom-parser"
|
||||||
version = "0.1.0"
|
version = "0.2.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ pub enum ParsingError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode<T>(data: &[u8]) -> Result<T, 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);
|
let mut asn_data = PerCodecData::from_slice_uper(data);
|
||||||
T::uper_decode(&mut asn_data)
|
T::uper_decode(&mut asn_data).map_err(ParsingError::UperDecodeError)
|
||||||
.map_err(ParsingError::UperDecodeError)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use telcom_parser::lte_rrc::BCCH_DL_SCH_Message;
|
|
||||||
use asn1_codecs::{uper::UperCodec, PerCodecData};
|
use asn1_codecs::{uper::UperCodec, PerCodecData};
|
||||||
|
use telcom_parser::lte_rrc::BCCH_DL_SCH_Message;
|
||||||
|
|
||||||
fn hex_to_bin(hex: &str) -> Vec<u8> {
|
fn hex_to_bin(hex: &str) -> Vec<u8> {
|
||||||
(0..hex.len())
|
(0..hex.len())
|
||||||
.step_by(2)
|
.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()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
curl -LOs "https://github.com/EFForg/rayhunter/releases/latest/download/release.tar"
|
||||||
|
curl -LOs "https://github.com/EFForg/rayhunter/releases/latest/download/release.tar.sha256"
|
||||||
|
if ! sha256sum -c --quiet release.tar.sha256; then
|
||||||
|
echo "Download corrupted! (╯°□°)╯︵ ┻━┻"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar -xf release.tar
|
||||||
|
./install-linux.sh
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
rm -rf build
|
||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
import pycrate_mobile
|
||||||
|
from pycrate_mobile import NASLTE
|
||||||
|
import pycrate_core
|
||||||
|
import binascii
|
||||||
|
import sys
|
||||||
|
import pprint
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
import pycrate_mobile.TS24301_EMM
|
||||||
|
|
||||||
|
EPS_IMSI_ATTACH = 2
|
||||||
|
|
||||||
|
def parse_nas_message(buffer, uplink=None):
|
||||||
|
if isinstance(buffer, str): #handle string argument or raw bytes
|
||||||
|
bin = binascii.unhexlify(buffer)
|
||||||
|
else:
|
||||||
|
bin = buffer
|
||||||
|
if uplink:
|
||||||
|
parsed = NASLTE.parse_NASLTE_MO(bin)
|
||||||
|
elif uplink == None: #We don't know if its an up or downlink
|
||||||
|
parsed = NASLTE.parse_NASLTE_MO(bin)
|
||||||
|
if parsed[0] == None:
|
||||||
|
parsed = NASLTE.parse_NASLTE_MT(bin)
|
||||||
|
else:
|
||||||
|
parsed = NASLTE.parse_NASLTE_MT(bin)
|
||||||
|
|
||||||
|
if parsed[0] is None: # Not a NAS Packet
|
||||||
|
raise TypeError("Not a nas packet")
|
||||||
|
return parsed[0]
|
||||||
|
|
||||||
|
def heur_ue_imsi_sent(msg):
|
||||||
|
output = "device transmitted IMSI to base station!"
|
||||||
|
|
||||||
|
if type(msg) not in [pycrate_mobile.TS24301_EMM.EMMAttachRequest, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage]:
|
||||||
|
return (False, None)
|
||||||
|
|
||||||
|
if isinstance(msg, pycrate_mobile.TS24301_EMM.EMMSecProtNASMessage):
|
||||||
|
try:
|
||||||
|
msg = msg['EMMAttachRequest']
|
||||||
|
except pycrate_core.elt.EltErr:
|
||||||
|
return (False, None)
|
||||||
|
|
||||||
|
if msg['EPSAttachType']['V'].to_int() == EPS_IMSI_ATTACH: #EPSAttachType Value is 'Combined EPS/IMSI Attach (2)'
|
||||||
|
return (True, output)
|
||||||
|
return (False, None)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("usage: nasparse.py [hex encoded nas message]")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
buffer = sys.argv[1]
|
||||||
|
msg = parse_nas_message(buffer)
|
||||||
|
pprint.pprint(msg)
|
||||||
|
triggered, message = heur_ue_imsi_sent(msg)
|
||||||
|
if triggered:
|
||||||
|
print(message)
|
||||||
|
exit(1)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
import unittest
|
||||||
|
import nasparse
|
||||||
|
|
||||||
|
|
||||||
|
class TestNasparse(unittest.TestCase):
|
||||||
|
imsi_sent_msg = '07412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1000000000000'
|
||||||
|
sec_imsi_sent_msg = '1727db4b7c0207412208391185184409309005f0700000100030023ed031d127298080211001000010810600000000830600000000000d00000300ff0003130184000a000005000010005c0a009011034f18a6f15d0103c1'
|
||||||
|
non_nas_msg = 'deadbeefcafe'
|
||||||
|
other_nas_msg = '074413780004023fd121'
|
||||||
|
other_nas_mt_msg = "023fd12100000000000000000000000000000000000000000000000000000000"
|
||||||
|
ciphered_nas_msg = "27ed6146bd0162a5d62d62e1ce501720dc8bd84f1167fd"
|
||||||
|
|
||||||
|
def run_heur(self, msg):
|
||||||
|
buf = nasparse.parse_nas_message(msg)
|
||||||
|
return nasparse.heur_ue_imsi_sent(buf)[0]
|
||||||
|
|
||||||
|
def test_imsi_sent(self):
|
||||||
|
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "imsi_sent_msg should trigger heuristic")
|
||||||
|
|
||||||
|
def test_sec_imsi_sent(self):
|
||||||
|
self.assertEqual(self.run_heur(self.imsi_sent_msg), True, "sec_imsi_sent_msg should trigger heuristic")
|
||||||
|
|
||||||
|
def test_non_nas_msg(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.run_heur(self.non_nas_msg)
|
||||||
|
|
||||||
|
def test_other_nas(self):
|
||||||
|
self.assertEqual(self.run_heur(self.other_nas_msg), False, "other_nas_msg should not trigger heuristic")
|
||||||
|
|
||||||
|
def test_other_nas_mt(self):
|
||||||
|
self.assertEqual(self.run_heur(self.other_nas_mt_msg), False, "other_nas_mt_msg should not trigger heuristic")
|
||||||
|
|
||||||
|
def test_ciphered_nas(self):
|
||||||
|
self.assertEqual(self.run_heur(self.ciphered_nas_msg), False, "ciphered_nas_msg should not trigger heuristic")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
import nasparse
|
||||||
|
from scapy.utils import RawPcapNgReader
|
||||||
|
import sys
|
||||||
|
|
||||||
|
TYPE_LTE_NAS = 0x12
|
||||||
|
UDP_LEN = 28
|
||||||
|
|
||||||
|
def process_pcap(pcap_path):
|
||||||
|
print('Opening {}...'.format(pcap_path))
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for pkt_data, pkt_metadata in RawPcapNgReader(pcap_path):
|
||||||
|
count += 1
|
||||||
|
gsmtap_len = pkt_data[UDP_LEN+1] * 4 # gsmtap header length is stored in the 2nd byte of GSMTAP as a number of 32 bit words
|
||||||
|
header_end = gsmtap_len + UDP_LEN #length of UDP/IP header plus GSMTAP header
|
||||||
|
|
||||||
|
gsmtap_hdr = pkt_data[UDP_LEN:header_end]
|
||||||
|
|
||||||
|
if gsmtap_hdr[2] != TYPE_LTE_NAS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# uplink status is the 7th bit of the 5th byte of the GSMTAP header.
|
||||||
|
# Uplink (Mobile originated) = 0 Downlink (mobile terminated) = 1
|
||||||
|
uplink = (gsmtap_hdr[4] & 0b01000000) >> 6
|
||||||
|
buffer = pkt_data[header_end:]
|
||||||
|
msg = nasparse.parse_nas_message(buffer, uplink)
|
||||||
|
triggered, message = nasparse.heur_ue_imsi_sent(msg)
|
||||||
|
if triggered:
|
||||||
|
print(f"Frame {count} triggered heuristic: {message}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("usage: pcap_check.py [path/to/pcap/file]")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
pcap_path = sys.argv[1]
|
||||||
|
process_pcap(pcap_path)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
asn1tools==0.166.0
|
asn1tools==0.166.0
|
||||||
bitstruct==8.19.0
|
bitstruct==8.19.0
|
||||||
diskcache==5.6.3
|
diskcache==5.6.3
|
||||||
|
pycrate==0.7.8
|
||||||
pyparsing==3.1.2
|
pyparsing==3.1.2
|
||||||
|
|||||||
Executable
+17
@@ -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 "$@"
|
||||||
Reference in New Issue
Block a user