Compare commits

..

4 Commits

Author SHA1 Message Date
Markus Unterwaditzer 4d54ea03e8 Remove some global reset styles in favor of explicit border colors, restore button cursors 2026-04-24 02:00:57 +02:00
Markus Unterwaditzer 2b427c64d7 upgrade tailwind
* moved to vite plugin for tailwind (it's recommended now)
* removed autoprefixer (v4 uses its own CSS thing now)
* postcss.config.js was used to wire up tailwind and autoprefixer, so
  it's gone
* tailwind.config.ts is gone, because v4 stores config in app.css using
  css variables
* fixed some renamed classes
2026-04-24 01:58:53 +02:00
Markus Unterwaditzer ed3ad389d0 downgrade tailwind for now, and fix build errors 2026-04-24 01:58:53 +02:00
dependabot[bot] e2fd9de62d Bump the dependency-type group in /daemon/web with 18 updates
Bumps the dependency-type group in /daemon/web with 18 updates:

| Package | From | To |
| --- | --- | --- |
| [@sveltejs/adapter-auto](https://github.com/sveltejs/kit/tree/HEAD/packages/adapter-auto) | `3.3.1` | `7.0.1` |
| [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) | `2.53.4` | `2.57.1` |
| [@sveltejs/vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte/tree/HEAD/packages/vite-plugin-svelte) | `6.2.1` | `7.0.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.7.0` | `25.6.0` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.4.21` | `10.5.0` |
| [eslint](https://github.com/eslint/eslint) | `9.37.0` | `10.2.0` |
| [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) | `9.1.2` | `10.1.8` |
| [eslint-plugin-svelte](https://github.com/sveltejs/eslint-plugin-svelte/tree/HEAD/packages/eslint-plugin-svelte) | `2.46.1` | `3.17.0` |
| [globals](https://github.com/sindresorhus/globals) | `15.15.0` | `17.5.0` |
| [prettier](https://github.com/prettier/prettier) | `3.6.2` | `3.8.3` |
| [prettier-plugin-svelte](https://github.com/sveltejs/prettier-plugin-svelte) | `3.4.0` | `3.5.1` |
| [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) | `5.53.7` | `5.55.4` |
| [svelte-check](https://github.com/sveltejs/language-tools) | `4.3.2` | `4.4.6` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `3.4.18` | `4.2.2` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.9.3` | `6.0.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.46.0` | `8.58.2` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.2` | `8.0.8` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `3.2.4` | `4.1.4` |


Updates `@sveltejs/adapter-auto` from 3.3.1 to 7.0.1
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/adapter-auto/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/adapter-auto@7.0.1/packages/adapter-auto)

Updates `@sveltejs/kit` from 2.53.4 to 2.57.1
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.57.1/packages/kit)

Updates `@sveltejs/vite-plugin-svelte` from 6.2.1 to 7.0.0
- [Release notes](https://github.com/sveltejs/vite-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/vite-plugin-svelte/blob/main/packages/vite-plugin-svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/vite-plugin-svelte/commits/@sveltejs/vite-plugin-svelte@7.0.0/packages/vite-plugin-svelte)

Updates `@types/node` from 24.7.0 to 25.6.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `autoprefixer` from 10.4.21 to 10.5.0
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.21...10.5.0)

Updates `eslint` from 9.37.0 to 10.2.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.37.0...v10.2.0)

Updates `eslint-config-prettier` from 9.1.2 to 10.1.8
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/commits/v10.1.8)

Updates `eslint-plugin-svelte` from 2.46.1 to 3.17.0
- [Release notes](https://github.com/sveltejs/eslint-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/eslint-plugin-svelte/commits/eslint-plugin-svelte@3.17.0/packages/eslint-plugin-svelte)

Updates `globals` from 15.15.0 to 17.5.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v17.5.0)

Updates `prettier` from 3.6.2 to 3.8.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.6.2...3.8.3)

Updates `prettier-plugin-svelte` from 3.4.0 to 3.5.1
- [Changelog](https://github.com/sveltejs/prettier-plugin-svelte/blob/v3.5.1/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/prettier-plugin-svelte/commits/v3.5.1)

Updates `svelte` from 5.53.7 to 5.55.4
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.55.4/packages/svelte)

Updates `svelte-check` from 4.3.2 to 4.4.6
- [Release notes](https://github.com/sveltejs/language-tools/releases)
- [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check@4.3.2...svelte-check@4.4.6)

Updates `tailwindcss` from 3.4.18 to 4.2.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/tailwindcss)

Updates `typescript` from 5.9.3 to 6.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

Updates `typescript-eslint` from 8.46.0 to 8.58.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.2/packages/typescript-eslint)

Updates `vite` from 7.3.2 to 8.0.8
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

Updates `vitest` from 3.2.4 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: "@sveltejs/adapter-auto"
  dependency-version: 7.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.57.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dependency-type
- dependency-name: "@sveltejs/vite-plugin-svelte"
  dependency-version: 7.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: autoprefixer
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dependency-type
- dependency-name: eslint
  dependency-version: 10.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: eslint-config-prettier
  dependency-version: 10.1.8
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: eslint-plugin-svelte
  dependency-version: 3.17.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: globals
  dependency-version: 17.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dependency-type
- dependency-name: prettier-plugin-svelte
  dependency-version: 3.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dependency-type
- dependency-name: svelte
  dependency-version: 5.55.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dependency-type
- dependency-name: svelte-check
  dependency-version: 4.4.6
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dependency-type
- dependency-name: tailwindcss
  dependency-version: 4.2.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: typescript-eslint
  dependency-version: 8.58.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dependency-type
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dependency-type
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-24 01:58:53 +02:00
45 changed files with 855 additions and 1897 deletions
+4 -16
View File
@@ -1,7 +1,3 @@
# open-pull-requests-limit is used to disable automated version updates
# security updates are unaffected. see
# * https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configuring-dependabot-version-updates#disabling-dependabot-version-updates
# * https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference#open-pull-requests-limit-
version: 2 version: 2
updates: updates:
# Rust dependencies # Rust dependencies
@@ -9,10 +5,8 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 0
groups: groups:
security: dependency-type:
applies-to: "security-updates"
patterns: patterns:
- "*" - "*"
@@ -21,10 +15,8 @@ updates:
directory: "/tools" directory: "/tools"
schedule: schedule:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 0
groups: groups:
security: dependency-type:
applies-to: "security-updates"
patterns: patterns:
- "*" - "*"
@@ -33,10 +25,8 @@ updates:
directory: "/daemon/web" directory: "/daemon/web"
schedule: schedule:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 0
groups: groups:
security: dependency-type:
applies-to: "security-updates"
patterns: patterns:
- "*" - "*"
@@ -45,9 +35,7 @@ updates:
directory: "/installer-gui" directory: "/installer-gui"
schedule: schedule:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 0
groups: groups:
security: dependency-type:
applies-to: "security-updates"
patterns: patterns:
- "*" - "*"
+14 -17
View File
@@ -28,7 +28,6 @@ jobs:
daemon_needed: ${{ steps.files_changed.outputs.daemon_count != '0' || steps.files_changed.outputs.installer_build != '0' }} daemon_needed: ${{ steps.files_changed.outputs.daemon_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
web_changed: ${{ steps.files_changed.outputs.web_count != '0' }} web_changed: ${{ steps.files_changed.outputs.web_count != '0' }}
docs_changed: ${{ steps.files_changed.outputs.docs_count != '0' || steps.files_changed.outputs.daemon_count != '0' }} docs_changed: ${{ steps.files_changed.outputs.docs_count != '0' || steps.files_changed.outputs.daemon_count != '0' }}
installer_build: ${{ steps.files_changed.outputs.installer_build != '0' }}
installer_changed: ${{ steps.files_changed.outputs.installer_count != '0' }} installer_changed: ${{ steps.files_changed.outputs.installer_count != '0' }}
installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count != '0' }} installer_gui_changed: ${{ steps.files_changed.outputs.installer_gui_count != '0' }}
rootshell_needed: ${{ steps.files_changed.outputs.rootshell_count != '0' || steps.files_changed.outputs.installer_build != '0' }} rootshell_needed: ${{ steps.files_changed.outputs.rootshell_count != '0' || steps.files_changed.outputs.installer_build != '0' }}
@@ -42,13 +41,11 @@ jobs:
run: | run: |
lcommit=${{ github.event.pull_request.base.sha || 'origin/main' }} lcommit=${{ github.event.pull_request.base.sha || 'origin/main' }}
# We rebuild everything if any of these conditions hold: # If we are on main, if workflow/cargo config files changed, or if
# * We are on main # the latest commit message contains "#build-all", run everything.
# * Changes are made to github workflows # Use #build-all in a commit message to force a full build on a PR
# * A cargo-workspace file changed (lockfile or .cargo), as that could affect any crate anywhere # branch (useful for testing release builds without merging to main).
# * Something from the script or dist folder changed (could be gated to installer, but some scripts like build_wpa_supplicant are part of the build process) if [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo || git log -1 --format='%s %b' | grep -qF '#build-all'
# * #build-all was used by the user to explicitly ask for this
if [ ${GITHUB_REF} = 'refs/heads/main' ] || git diff --name-only $lcommit..HEAD | grep -qe ^.github/workflows/ -e ^.cargo -e '^Cargo\.lock$' -e '^Cargo\.toml$' -e ^dist/ -e ^scripts/ || git log -1 --format='%s %b' | grep -qF '#build-all'
then then
echo "building everything" echo "building everything"
echo code_count=forced >> "$GITHUB_OUTPUT" echo code_count=forced >> "$GITHUB_OUTPUT"
@@ -144,13 +141,13 @@ jobs:
npm install npm install
npm run build npm run build
popd popd
cargo check --verbose NO_FIRMWARE_BIN=true cargo check --verbose
- name: Run tests - name: Run tests
run: | run: |
cargo test --verbose NO_FIRMWARE_BIN=true cargo test --verbose
- name: Run clippy - name: Run clippy
run: | run: |
cargo clippy --verbose NO_FIRMWARE_BIN=true cargo clippy --verbose
installer_gui_check: installer_gui_check:
# we test the GUI installer separately to: # we test the GUI installer separately to:
@@ -176,9 +173,9 @@ jobs:
# fmt --all runs on all workspace packages so this is handled by # fmt --all runs on all workspace packages so this is handled by
# check_and_test above # check_and_test above
- name: Check - name: Check
run: cargo check --package installer-gui --verbose run: NO_FIRMWARE_BIN=true cargo check --package installer-gui --verbose
- name: Run clippy - name: Run clippy
run: cargo clippy --package installer-gui --verbose run: NO_FIRMWARE_BIN=true cargo clippy --package installer-gui --verbose
test_daemon_frontend: test_daemon_frontend:
needs: files_changed needs: files_changed
@@ -230,12 +227,12 @@ jobs:
shell: bash shell: bash
run: | run: |
cd installer cd installer
cargo check --verbose NO_FIRMWARE_BIN=true cargo check --verbose
- name: cargo test - name: cargo test
shell: bash shell: bash
run: | run: |
cd installer cd installer
cargo test --verbose --no-default-features NO_FIRMWARE_BIN=true cargo test --verbose --no-default-features
build_rayhunter_check: build_rayhunter_check:
if: needs.files_changed.outputs.daemon_changed == 'true' if: needs.files_changed.outputs.daemon_changed == 'true'
@@ -308,7 +305,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
build_wpa_supplicant: build_wpa_supplicant:
if: needs.files_changed.outputs.installer_build == 'true' if: needs.files_changed.outputs.installer_changed == 'true'
needs: needs:
- files_changed - files_changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -628,7 +625,7 @@ jobs:
- name: Build rayhunter-daemon openapi docs - name: Build rayhunter-daemon openapi docs
run: | run: |
mkdir -p daemon/web/build mkdir -p daemon/web/build
touch daemon/web/build/{favicon.png,index.html.br,rayhunter_orca_only.png,rayhunter_text.png} touch daemon/web/build/{favicon.png,index.html.gz,rayhunter_orca_only.png,rayhunter_text.png}
cargo run --bin gen_api --features apidocs -- ./rayhunter-openapi.json cargo run --bin gen_api --features apidocs -- ./rayhunter-openapi.json
- name: Make swagger folder - name: Make swagger folder
run: | run: |
+1 -1
View File
@@ -76,7 +76,7 @@ This one is for maintainers of Rayhunter.
1. Make a PR changing the versions in `Cargo.toml` and other files. 1. Make a PR changing the versions in `Cargo.toml` and other files.
This could be automated better but right now it's manual. You can do this easily with sed: This could be automated better but right now it's manual. You can do this easily with sed:
`sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml installer-gui/src-tauri/Cargo.toml` `sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml`
2. Merge PR and make a tag. 2. Merge PR and make a tag.
Generated
+175 -483
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter-check" name = "rayhunter-check"
version = "0.11.0" version = "0.10.2"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.11.0" version = "0.10.2"
edition = "2024" edition = "2024"
rust-version = "1.88.0" rust-version = "1.88.0"
@@ -21,7 +21,7 @@ apidocs = ["dep:utoipa", "wifi-station/utoipa"]
[dependencies] [dependencies]
rayhunter = { path = "../lib" } rayhunter = { path = "../lib" }
wifi-station = "0.10.1" wifi-station = { git = "https://github.com/BeigeBox/wifi-station", rev = "e8ec5b4" }
toml = "0.8.8" toml = "0.8.8"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] } tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] }
@@ -40,7 +40,7 @@ image = { version = "0.25.1", default-features = false, features = ["png", "gif
tempfile = "3.10.2" tempfile = "3.10.2"
async_zip = { version = "0.0.17", features = ["tokio"] } async_zip = { version = "0.0.17", features = ["tokio"] }
anyhow = "1.0.98" anyhow = "1.0.98"
reqwest = { version = "0.12.20", default-features = false, features = ["stream"] } reqwest = { version = "0.12.20", default-features = false }
rustls-rustcrypto = { version = "0.0.2-alpha", optional = true } rustls-rustcrypto = { version = "0.0.2-alpha", optional = true }
rustls-post-quantum = { version = "0.2.4", optional = true } rustls-post-quantum = { version = "0.2.4", optional = true }
async-trait = "0.1.88" async-trait = "0.1.88"
+6 -38
View File
@@ -46,43 +46,10 @@ pub struct Config {
pub wifi_enabled: bool, pub wifi_enabled: bool,
/// Vector containing wifi client DNS servers /// Vector containing wifi client DNS servers
pub dns_servers: Option<Vec<String>>, pub dns_servers: Option<Vec<String>>,
/// WebDAV upload configuration. The upload worker runs whenever `webdav.url` is non-empty. /// Wifi client firewall mode
pub webdav: WebdavConfig, pub firewall_restrict_outbound: bool,
} /// Vector containing additional wifi client firewall ports to open
pub firewall_allowed_ports: Option<Vec<u16>>,
/// Configuration for uploading finished QMDL recordings to a WebDAV server.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
#[cfg_attr(feature = "apidocs", derive(utoipa::ToSchema))]
pub struct WebdavConfig {
/// WebDAV server base URL, e.g. "https://example.com/remote.php/files/untitaker/my-subfolder/"
pub url: String,
/// Optional username for HTTP Basic auth
pub username: Option<String>,
/// Optional password for HTTP Basic auth
pub password: Option<String>,
/// Timeout (in seconds) for each upload request
pub upload_timeout_secs: u64,
/// How often (in seconds) the worker scans for entries to upload
pub poll_interval_secs: u64,
/// Minimum age (in seconds) an entry must have before it becomes eligible for upload
pub min_age_secs: i64,
/// Delete the file locally after a successful upload
pub delete_on_upload: bool,
}
impl Default for WebdavConfig {
fn default() -> Self {
WebdavConfig {
url: String::new(),
username: None,
password: None,
upload_timeout_secs: 300,
poll_interval_secs: 3600,
min_age_secs: 86400,
delete_on_upload: false,
}
}
} }
impl Default for Config { impl Default for Config {
@@ -105,7 +72,8 @@ impl Default for Config {
wifi_security: None, wifi_security: None,
wifi_enabled: false, wifi_enabled: false,
dns_servers: None, dns_servers: None,
webdav: WebdavConfig::default(), firewall_restrict_outbound: true,
firewall_allowed_ports: None,
} }
} }
} }
+2 -16
View File
@@ -10,7 +10,6 @@ use axum::http::header::CONTENT_TYPE;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use futures::{StreamExt, TryStreamExt, future}; use futures::{StreamExt, TryStreamExt, future};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use rayhunter::Device;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
@@ -371,7 +370,7 @@ impl DiagTask {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn run_diag_read_thread( pub fn run_diag_read_thread(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
device: Device, mut dev: DiagDevice,
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>, mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
qmdl_file_tx: Sender<DiagDeviceCtrlMessage>, qmdl_file_tx: Sender<DiagDeviceCtrlMessage>,
ui_update_sender: Sender<display::DisplayState>, ui_update_sender: Sender<display::DisplayState>,
@@ -383,21 +382,8 @@ pub fn run_diag_read_thread(
min_space_to_continue_mb: u64, min_space_to_continue_mb: u64,
) { ) {
task_tracker.spawn(async move { task_tracker.spawn(async move {
info!("Using configuration for device: {0:?}", device);
let mut dev = DiagDevice::new(&device)
.await?;
dev.config_logs()
.await?;
let mut diag_stream = pin!(dev.as_stream().into_stream()); let mut diag_stream = pin!(dev.as_stream().into_stream());
let mut diag_task = DiagTask::new( let mut diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config, notification_channel, min_space_to_start_mb, min_space_to_continue_mb);
ui_update_sender,
analysis_sender,
analyzer_config,
notification_channel,
min_space_to_start_mb,
min_space_to_continue_mb
);
qmdl_file_tx qmdl_file_tx
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None }) .send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })
.await .await
+3
View File
@@ -1,3 +1,4 @@
use rayhunter::diag_device::DiagDeviceError;
use thiserror::Error; use thiserror::Error;
use crate::qmdl_store::RecordingStoreError; use crate::qmdl_store::RecordingStoreError;
@@ -6,6 +7,8 @@ use crate::qmdl_store::RecordingStoreError;
pub enum RayhunterError { pub enum RayhunterError {
#[error("Config file parsing error: {0}")] #[error("Config file parsing error: {0}")]
ConfigFileParsingError(#[from] toml::de::Error), ConfigFileParsingError(#[from] toml::de::Error),
#[error("Diag intialization error: {0}")]
DiagInitError(DiagDeviceError),
#[error("Tokio error: {0}")] #[error("Tokio error: {0}")]
TokioError(#[from] tokio::io::Error), TokioError(#[from] tokio::io::Error),
#[error("QmdlStore error: {0}")] #[error("QmdlStore error: {0}")]
+92
View File
@@ -0,0 +1,92 @@
use anyhow::{Result, bail};
use log::{info, warn};
use tokio::process::Command;
use wifi_station::detect_bridge_iface;
use crate::config::Config;
async fn run_iptables(args: &[&str]) -> Result<()> {
let out = Command::new("iptables").args(args).output().await?;
if !out.status.success() {
bail!(
"iptables {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
Ok(())
}
pub async fn apply(config: &Config) {
let _ = Command::new("iptables")
.args(["-F", "OUTPUT"])
.output()
.await;
if config.firewall_restrict_outbound {
// Fail open on partial setup error: reachability beats restriction when recovery means physical access.
match setup_outbound_whitelist(&config.firewall_allowed_ports, &config.ntfy_url).await {
Ok(()) => info!("outbound firewall active: allowing DHCP, DNS, HTTPS only"),
Err(e) => warn!("firewall setup failed: {e} (fail-open, outbound unrestricted)"),
}
}
}
async fn setup_outbound_whitelist(
extra_ports: &Option<Vec<u16>>,
ntfy_url: &Option<String>,
) -> Result<()> {
run_iptables(&["-A", "OUTPUT", "-o", "lo", "-j", "ACCEPT"]).await?;
run_iptables(&["-A", "OUTPUT", "-o", detect_bridge_iface(), "-j", "ACCEPT"]).await?;
run_iptables(&[
"-A",
"OUTPUT",
"-m",
"state",
"--state",
"ESTABLISHED,RELATED",
"-j",
"ACCEPT",
])
.await?;
run_iptables(&[
"-A", "OUTPUT", "-p", "udp", "--dport", "67:68", "-j", "ACCEPT",
])
.await?;
run_iptables(&["-A", "OUTPUT", "-p", "udp", "--dport", "53", "-j", "ACCEPT"]).await?;
run_iptables(&["-A", "OUTPUT", "-p", "tcp", "--dport", "53", "-j", "ACCEPT"]).await?;
run_iptables(&[
"-A", "OUTPUT", "-p", "tcp", "--dport", "443", "-j", "ACCEPT",
])
.await?;
if let Some(url) = ntfy_url
&& let Ok(parsed) = url::Url::parse(url)
&& let Some(port) = parsed.port_or_known_default()
&& port != 443
{
let port_str = port.to_string();
run_iptables(&[
"-A", "OUTPUT", "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT",
])
.await?;
info!("firewall: auto-allowed port {port} for ntfy");
}
if let Some(ports) = extra_ports {
for port in ports {
let port_str = port.to_string();
run_iptables(&[
"-A", "OUTPUT", "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT",
])
.await?;
}
}
run_iptables(&["-A", "OUTPUT", "-j", "DROP"]).await?;
let _ = tokio::fs::write("/proc/sys/net/bridge/bridge-nf-call-iptables", "0").await;
Ok(())
}
+1 -1
View File
@@ -5,13 +5,13 @@ pub mod crypto_provider;
pub mod diag; pub mod diag;
pub mod display; pub mod display;
pub mod error; pub mod error;
pub mod firewall;
pub mod key_input; pub mod key_input;
pub mod notifications; pub mod notifications;
pub mod pcap; pub mod pcap;
pub mod qmdl_store; pub mod qmdl_store;
pub mod server; pub mod server;
pub mod stats; pub mod stats;
pub mod webdav;
#[cfg(feature = "apidocs")] #[cfg(feature = "apidocs")]
use utoipa::OpenApi; use utoipa::OpenApi;
+12 -13
View File
@@ -5,14 +5,13 @@ mod crypto_provider;
mod diag; mod diag;
mod display; mod display;
mod error; mod error;
mod firewall;
mod key_input; mod key_input;
mod notifications; mod notifications;
mod pcap; mod pcap;
mod qmdl_store; mod qmdl_store;
mod server; mod server;
mod stats; mod stats;
mod webdav;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
@@ -28,7 +27,6 @@ use crate::server::{
scan_wifi, serve_static, set_config, set_time_offset, test_notification, scan_wifi, serve_static, set_config, set_time_offset, test_notification,
}; };
use crate::stats::{get_qmdl_manifest, get_system_stats}; use crate::stats::{get_qmdl_manifest, get_system_stats};
use crate::webdav::run_webdav_upload_worker;
use wifi_station::WifiStatus; use wifi_station::WifiStatus;
use analysis::{ use analysis::{
@@ -44,6 +42,7 @@ use diag::{
use log::{error, info}; use log::{error, info};
use qmdl_store::RecordingStoreError; use qmdl_store::RecordingStoreError;
use rayhunter::Device; use rayhunter::Device;
use rayhunter::diag_device::DiagDevice;
use stats::get_log; use stats::get_log;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::select; use tokio::select;
@@ -215,10 +214,18 @@ async fn run_with_config(
let notification_service = NotificationService::new(config.ntfy_url.clone()); let notification_service = NotificationService::new(config.ntfy_url.clone());
if !config.debug_mode { if !config.debug_mode {
info!("Using configuration for device: {0:?}", config.device);
let mut dev = DiagDevice::new(&config.device)
.await
.map_err(RayhunterError::DiagInitError)?;
dev.config_logs()
.await
.map_err(RayhunterError::DiagInitError)?;
info!("Starting Diag Thread"); info!("Starting Diag Thread");
run_diag_read_thread( run_diag_read_thread(
&task_tracker, &task_tracker,
config.device.clone(), dev,
diag_rx, diag_rx,
diag_tx.clone(), diag_tx.clone(),
ui_update_tx.clone(), ui_update_tx.clone(),
@@ -287,15 +294,7 @@ async fn run_with_config(
shutdown_token.clone(), shutdown_token.clone(),
wifi_status.clone(), wifi_status.clone(),
); );
firewall::apply(&config).await;
if !config.webdav.url.trim().is_empty() {
run_webdav_upload_worker(
&task_tracker,
shutdown_token.clone(),
qmdl_store_lock.clone(),
config.webdav.clone().into(),
);
}
let state = Arc::new(ServerState { let state = Arc::new(ServerState {
config_path: args.config_path.clone(), config_path: args.config_path.clone(),
+1 -113
View File
@@ -2,7 +2,7 @@ use std::io::{self, ErrorKind};
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use chrono::{DateTime, Local, TimeDelta}; use chrono::{DateTime, Local};
use log::{info, warn}; use log::{info, warn};
use rayhunter::util::RuntimeMetadata; use rayhunter::util::RuntimeMetadata;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -67,9 +67,6 @@ pub struct ManifestEntry {
pub arch: Option<String>, pub arch: Option<String>,
#[serde(default)] #[serde(default)]
pub stop_reason: Option<String>, pub stop_reason: Option<String>,
/// When the manifest was uploaded to a WebDAV server
#[cfg_attr(feature = "apidocs", schema(value_type = String))]
pub upload_time: Option<DateTime<Local>>,
} }
impl ManifestEntry { impl ManifestEntry {
@@ -85,7 +82,6 @@ impl ManifestEntry {
system_os: Some(metadata.system_os), system_os: Some(metadata.system_os),
arch: Some(metadata.arch), arch: Some(metadata.arch),
stop_reason: None, stop_reason: None,
upload_time: None,
} }
} }
@@ -216,7 +212,6 @@ impl RecordingStore {
system_os: None, system_os: None,
arch: None, arch: None,
stop_reason: None, stop_reason: None,
upload_time: None,
}); });
} }
@@ -347,23 +342,6 @@ impl RecordingStore {
Ok(()) Ok(())
} }
pub fn get_next_unuploaded_entry(&self, min_age: TimeDelta) -> Option<String> {
let now = rayhunter::clock::get_adjusted_now();
self.manifest
.entries
.iter()
.filter_map(|entry| {
if self.is_current_entry(&entry.name) || entry.upload_time.is_some() {
return None;
}
let age = now - entry.last_message_time.unwrap_or(entry.start_time);
(age > min_age).then_some((&entry.name, age))
})
.max_by_key(|(_, age)| *age)
.map(|(name, _)| name.clone())
}
// Finds an entry by filename // Finds an entry by filename
pub fn entry_for_name(&self, name: &str) -> Option<(usize, &ManifestEntry)> { pub fn entry_for_name(&self, name: &str) -> Option<(usize, &ManifestEntry)> {
let entry_index = self let entry_index = self
@@ -390,22 +368,6 @@ impl RecordingStore {
Ok(()) Ok(())
} }
pub async fn mark_entry_as_uploaded(
&mut self,
name: &str,
upload_time: DateTime<Local>,
) -> Result<(), RecordingStoreError> {
let entry_index = self
.manifest
.entries
.iter()
.position(|entry| entry.name == name)
.ok_or(RecordingStoreError::NoSuchEntryError)?;
self.manifest.entries[entry_index].upload_time = Some(upload_time);
self.write_manifest().await?;
Ok(())
}
pub fn is_current_entry(&self, name: &str) -> bool { pub fn is_current_entry(&self, name: &str) -> bool {
match self.current_entry { match self.current_entry {
Some(idx) => match self.manifest.entries.get(idx) { Some(idx) => match self.manifest.entries.get(idx) {
@@ -582,78 +544,4 @@ mod tests {
store.delete_all_entries().await.unwrap(); store.delete_all_entries().await.unwrap();
assert!(store.current_entry.is_none()); assert!(store.current_entry.is_none());
} }
#[tokio::test]
async fn test_mark_entry_as_uploaded_sets_time_and_persists() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
let _ = store.new_entry().await.unwrap();
let name = store.manifest.entries[0].name.clone();
store.close_current_entry().await.unwrap();
let upload_time = Local::now();
store
.mark_entry_as_uploaded(&name, upload_time)
.await
.unwrap();
assert_eq!(store.manifest.entries[0].upload_time, Some(upload_time));
let reloaded = RecordingStore::load(dir.path()).await.unwrap();
assert_eq!(reloaded.manifest.entries[0].upload_time, Some(upload_time));
}
#[tokio::test]
async fn test_mark_entry_as_uploaded_missing_entry() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
assert!(matches!(
store.mark_entry_as_uploaded("nope", Local::now()).await,
Err(RecordingStoreError::NoSuchEntryError)
));
}
#[tokio::test]
async fn test_get_next_unuploaded_entry() {
let dir = make_temp_dir();
let mut store = RecordingStore::create(dir.path()).await.unwrap();
for _ in 0..3 {
let _ = store.new_entry().await.unwrap();
}
store.manifest.entries[0].name = "entry-0".to_owned();
store.manifest.entries[0].start_time = Local::now() - TimeDelta::seconds(10);
store.manifest.entries[0].last_message_time = None;
store.manifest.entries[1].name = "entry-1".to_owned();
store.manifest.entries[1].start_time = Local::now() - TimeDelta::seconds(10);
store.manifest.entries[1].last_message_time = Some(Local::now() - TimeDelta::seconds(5));
store.manifest.entries[2].name = "entry-2".to_owned();
store.manifest.entries[2].start_time = Local::now() - TimeDelta::seconds(10);
store.manifest.entries[2].last_message_time = Some(Local::now() - TimeDelta::seconds(1));
assert_eq!(
store.get_next_unuploaded_entry(TimeDelta::seconds(3600)),
None,
);
assert_eq!(
store.get_next_unuploaded_entry(TimeDelta::seconds(3)),
Some("entry-0".to_owned())
);
store
.mark_entry_as_uploaded("entry-0", Local::now())
.await
.unwrap();
assert_eq!(
store.get_next_unuploaded_entry(TimeDelta::seconds(3)),
Some("entry-1".to_owned())
);
store
.mark_entry_as_uploaded("entry-1", Local::now())
.await
.unwrap();
assert_eq!(store.get_next_unuploaded_entry(TimeDelta::seconds(3)), None);
}
} }
+2 -2
View File
@@ -112,9 +112,9 @@ pub async fn serve_static(
"index.html" => ( "index.html" => (
[ [
(header::CONTENT_TYPE, HeaderValue::from_static("text/html")), (header::CONTENT_TYPE, HeaderValue::from_static("text/html")),
(header::CONTENT_ENCODING, HeaderValue::from_static("br")), (header::CONTENT_ENCODING, HeaderValue::from_static("gzip")),
], ],
include_bytes!("../web/build/index.html.br"), include_bytes!("../web/build/index.html.gz"),
) )
.into_response(), .into_response(),
path => { path => {
-446
View File
@@ -1,446 +0,0 @@
use std::fmt::Display;
use std::{sync::Arc, time::Duration};
use chrono::TimeDelta;
use log::{info, warn};
use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
use reqwest::{Body, Client, Response};
use tokio::fs::File;
use tokio::join;
use tokio::{select, sync::RwLock, time};
use tokio_util::io::ReaderStream;
use tokio_util::{sync::CancellationToken, task::TaskTracker};
use crate::config::WebdavConfig;
use crate::qmdl_store::RecordingStore;
pub struct WebdavUploadWorkerConfig {
poll_interval: Duration,
min_age: TimeDelta,
url: String,
username: Option<String>,
password: Option<String>,
timeout: Duration,
delete_on_upload: bool,
}
impl From<WebdavConfig> for WebdavUploadWorkerConfig {
fn from(value: WebdavConfig) -> Self {
WebdavUploadWorkerConfig {
poll_interval: Duration::from_secs(value.poll_interval_secs),
min_age: TimeDelta::seconds(value.min_age_secs),
url: value.url,
username: value.username,
password: value.password,
timeout: Duration::from_secs(value.upload_timeout_secs),
delete_on_upload: value.delete_on_upload,
}
}
}
enum FileKind {
Analysis,
Qmdl,
}
impl FileKind {
fn as_extension(&self) -> &'static str {
match self {
FileKind::Analysis => ".ndjson",
FileKind::Qmdl => ".qmdl",
}
}
}
impl Display for FileKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileKind::Analysis => write!(f, "analysis"),
FileKind::Qmdl => write!(f, "QMDL"),
}
}
}
#[derive(Debug, Clone)]
struct WebDavClient {
client: Client,
url: String,
username: Option<String>,
password: Option<String>,
}
impl WebDavClient {
fn new(
mut url: String,
username: Option<String>,
password: Option<String>,
timeout: Duration,
) -> Result<Self, reqwest::Error> {
if !url.ends_with('/') {
url.push('/');
}
Ok(Self {
client: reqwest::Client::builder().timeout(timeout).build()?,
url,
username,
password,
})
}
async fn try_upload_file(&self, file: File, name: &str) -> anyhow::Result<Response> {
let file_size = file.metadata().await?.len();
let stream = ReaderStream::new(file);
let body = Body::wrap_stream(stream);
let target = format!("{}{}", self.url, name);
let client = self
.client
.put(&target)
.header(CONTENT_TYPE, "application/octet-stream")
.header(CONTENT_LENGTH, file_size);
let client = match (&self.username, &self.password) {
(Some(username), Some(password)) => client.basic_auth(username, Some(password)),
(Some(username), None) => client.basic_auth(username, None::<&str>),
(None, None) => client,
(None, Some(_)) => {
warn!(
"Got WebDAV auth setting with no username but with a password, skipping authentication"
);
client
}
};
let resp = client.body(body).send().await?.error_for_status();
Ok(resp?)
}
}
async fn try_upload_entry(
client: WebDavClient,
store: Arc<RwLock<RecordingStore>>,
entry_name: String,
file_kind: FileKind,
shutdown_token: CancellationToken,
) -> Option<()> {
let read_lock = store.read().await;
let entry_idx = read_lock.entry_for_name(&entry_name)?.0;
let file = match file_kind {
FileKind::Analysis => read_lock.open_entry_analysis(entry_idx).await,
FileKind::Qmdl => read_lock.open_entry_qmdl(entry_idx).await,
};
drop(read_lock);
let Ok(file) = file.map_err(|err| {
warn!(
"Unable to open entry: {} {} file: {:?}",
entry_name, file_kind, err
)
}) else {
return None;
};
let file_name = format!("{}{}", entry_name, file_kind.as_extension());
let res = select! {
_ = shutdown_token.cancelled() => {
warn!(
"Cancelling upload for entry {} {} file: received shutdown signal",
entry_name, file_kind
);
return None;
},
res = client.try_upload_file(file, &file_name) => res,
};
match res {
Ok(_) => {
info!("Uploaded {} file for entry {}", file_kind, entry_name);
Some(())
}
Err(err) => {
warn!(
"Failed to upload {} file for entry {}: {:?}",
file_kind, entry_name, err
);
None
}
}
}
pub fn run_webdav_upload_worker(
task_tracker: &TaskTracker,
shutdown_token: CancellationToken,
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
config: WebdavUploadWorkerConfig,
) {
task_tracker.spawn(async move {
let mut interval = time::interval(config.poll_interval);
interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
let webdav_client = match WebDavClient::new(
config.url,
config.username,
config.password,
config.timeout,
) {
Ok(client) => client,
Err(err) => {
warn!("Unable to create WebDAV client: {:?}", err);
return;
}
};
loop {
select! {
_ = shutdown_token.cancelled() => break,
_ = interval.tick() => {
loop {
let Some(unuploaded_entry) = qmdl_store_lock
.read()
.await
.get_next_unuploaded_entry(config.min_age) else {
break;
};
let (Some(()), Some(())) = join!(
try_upload_entry(
webdav_client.clone(),
qmdl_store_lock.clone(),
unuploaded_entry.clone(),
FileKind::Qmdl,
shutdown_token.clone(),
),
try_upload_entry(
webdav_client.clone(),
qmdl_store_lock.clone(),
unuploaded_entry.clone(),
FileKind::Analysis,
shutdown_token.clone()
),
) else {
break;
};
if config.delete_on_upload {
match qmdl_store_lock.write().await.delete_entry(&unuploaded_entry).await {
Ok(_) => info!("Successfully deleted entry: {} after upload to WebDAV", unuploaded_entry),
Err(err) => warn!("Unable to delete entry: {} after upload to WebDAV: {}", unuploaded_entry, err),
}
} else {
match qmdl_store_lock.write().await.mark_entry_as_uploaded(&unuploaded_entry, rayhunter::clock::get_adjusted_now()).await {
Ok(_) => info!("Successfully marked entry: {} as uploaded", unuploaded_entry),
Err(err) => warn!("Unable to mark entry: {} as uploaded: {}", unuploaded_entry, err),
}
}
}
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{
Router,
body::Bytes,
extract::{Path as AxumPath, State},
http::{HeaderMap, StatusCode},
routing::put,
};
use tempfile::Builder;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
#[derive(Clone, Debug)]
struct RecordedPut {
path: String,
auth: Option<String>,
body: Vec<u8>,
}
async fn capture_put(
State(state): State<Arc<Mutex<Vec<RecordedPut>>>>,
AxumPath(path): AxumPath<String>,
headers: HeaderMap,
body: Bytes,
) -> StatusCode {
let auth = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.map(String::from);
state.lock().await.push(RecordedPut {
path,
auth,
body: body.to_vec(),
});
StatusCode::CREATED
}
async fn setup_webdav_server() -> (Arc<Mutex<Vec<RecordedPut>>>, String) {
crate::crypto_provider::install_default();
let state = Arc::new(Mutex::new(Vec::new()));
let app = Router::new()
.route("/{*path}", put(capture_put))
.with_state(state.clone());
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let url = format!("http://{}/dav", addr);
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(100)).await;
(state, url)
}
async fn cleanup_worker(shutdown: CancellationToken, tracker: TaskTracker) {
shutdown.cancel();
tracker.close();
tracker.wait().await;
}
async fn make_store_with_closed_entry(
dir: &std::path::Path,
) -> (Arc<RwLock<RecordingStore>>, String) {
let mut store = RecordingStore::create(dir).await.unwrap();
let (mut qmdl_file, mut analysis_file) = store.new_entry().await.unwrap();
qmdl_file.write_all(b"fake qmdl payload").await.unwrap();
qmdl_file.flush().await.unwrap();
analysis_file
.write_all(b"fake ndjson payload")
.await
.unwrap();
analysis_file.flush().await.unwrap();
let entry_index = store.current_entry.unwrap();
let name = store.manifest.entries[entry_index].name.clone();
store.update_entry_qmdl_size(entry_index, 17).await.unwrap();
store.close_current_entry().await.unwrap();
(Arc::new(RwLock::new(store)), name)
}
#[tokio::test]
async fn test_webdav_upload_worker_uploads_entry() {
let (captured, url) = setup_webdav_server().await;
let dir = Builder::new().prefix("webdav_test").tempdir().unwrap();
let (store, entry_name) = make_store_with_closed_entry(dir.path()).await;
let shutdown = CancellationToken::new();
let tracker = TaskTracker::new();
let config = WebdavUploadWorkerConfig {
poll_interval: Duration::from_millis(50),
min_age: TimeDelta::seconds(-1),
url,
username: Some("user".to_string()),
password: Some("password".to_string()),
timeout: Duration::from_secs(1),
delete_on_upload: false,
};
run_webdav_upload_worker(&tracker, shutdown.clone(), store.clone(), config);
tokio::time::sleep(Duration::from_millis(500)).await;
cleanup_worker(shutdown, tracker).await;
let recorded = captured.lock().await;
assert_eq!(recorded.len(), 2);
let paths: Vec<&str> = recorded.iter().map(|r| r.path.as_str()).collect();
let qmdl_path = format!("dav/{}.qmdl", entry_name);
let ndjson_path = format!("dav/{}.ndjson", entry_name);
assert!(paths.contains(&qmdl_path.as_str()));
assert!(paths.contains(&ndjson_path.as_str()));
for put in recorded.iter() {
assert_eq!(put.auth.as_deref(), Some("Basic dXNlcjpwYXNzd29yZA=="));
}
let qmdl_body = recorded
.iter()
.find(|r| r.path == qmdl_path)
.unwrap()
.body
.clone();
let ndjson_body = recorded
.iter()
.find(|r| r.path == ndjson_path)
.unwrap()
.body
.clone();
drop(recorded);
assert_eq!(qmdl_body, b"fake qmdl payload");
assert_eq!(ndjson_body, b"fake ndjson payload");
let store_read = store.read().await;
let (_, entry) = store_read.entry_for_name(&entry_name).unwrap();
assert!(entry.upload_time.is_some());
}
#[tokio::test]
async fn test_webdav_upload_worker_deletes_when_configured() {
let (captured, url) = setup_webdav_server().await;
let dir = Builder::new().prefix("webdav_test").tempdir().unwrap();
let (store, entry_name) = make_store_with_closed_entry(dir.path()).await;
let shutdown = CancellationToken::new();
let tracker = TaskTracker::new();
let config = WebdavUploadWorkerConfig {
poll_interval: Duration::from_millis(50),
min_age: TimeDelta::seconds(-1),
url,
username: None,
password: None,
timeout: Duration::from_secs(1),
delete_on_upload: true,
};
run_webdav_upload_worker(&tracker, shutdown.clone(), store.clone(), config);
tokio::time::sleep(Duration::from_millis(500)).await;
cleanup_worker(shutdown, tracker).await;
assert_eq!(captured.lock().await.len(), 2);
let store_read = store.read().await;
assert!(store_read.entry_for_name(&entry_name).is_none());
}
#[tokio::test]
async fn test_webdav_upload_worker_respects_min_age() {
let (captured, url) = setup_webdav_server().await;
let dir = Builder::new().prefix("webdav_test").tempdir().unwrap();
let (store, entry_name) = make_store_with_closed_entry(dir.path()).await;
let shutdown = CancellationToken::new();
let tracker = TaskTracker::new();
let config = WebdavUploadWorkerConfig {
poll_interval: Duration::from_millis(50),
min_age: TimeDelta::seconds(3600),
url,
username: None,
password: None,
timeout: Duration::from_secs(1),
delete_on_upload: false,
};
run_webdav_upload_worker(&tracker, shutdown.clone(), store.clone(), config);
tokio::time::sleep(Duration::from_millis(500)).await;
cleanup_worker(shutdown, tracker).await;
assert!(captured.lock().await.is_empty());
let store_read = store.read().await;
let (_, entry) = store_read.entry_for_name(&entry_name).unwrap();
assert!(entry.upload_time.is_none());
}
}
+291 -321
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -4,7 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build && node ./scripts/compress-index.js", "build": "vite build && gzip -9 ./build/index.html",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -18,23 +18,23 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-static": "^3.0.5", "@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.58.0", "@sveltejs/kit": "^2.57.1",
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.0",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"eslint": "^10.2.1", "eslint": "^10.2.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.17.1", "eslint-plugin-svelte": "^3.17.0",
"globals": "^17.5.0", "globals": "^17.5.0",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"prettier-plugin-svelte": "^3.5.1", "prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.55.5", "svelte": "^5.55.4",
"svelte-check": "^4.4.6", "svelte-check": "^4.4.6",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.59.0", "typescript-eslint": "^8.58.2",
"vite": "^8.0.10", "vite": "^8.0.9",
"vitest": "^4.1.5" "vitest": "^4.1.4"
} }
} }
-11
View File
@@ -1,11 +0,0 @@
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
import { brotliCompressSync, constants } from 'node:zlib';
const input = './build/index.html';
const output = './build/index.html.br';
const compressed = brotliCompressSync(readFileSync(input), {
params: { [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY },
});
writeFileSync(output, compressed);
unlinkSync(input);
@@ -13,11 +13,6 @@
manager: AnalysisManager; manager: AnalysisManager;
current: boolean; current: boolean;
} = $props(); } = $props();
const date_formatter = new Intl.DateTimeFormat(undefined, {
timeStyle: 'long',
dateStyle: 'short',
});
</script> </script>
<div class="container mt-2"> <div class="container mt-2">
@@ -54,30 +49,20 @@
{:else} {:else}
<p>No warnings to display!</p> <p>No warnings to display!</p>
{/if} {/if}
<div> {#if metadata !== undefined && metadata.rayhunter !== undefined}
<p class="text-lg underline">Metadata</p>
{#if metadata !== undefined && metadata.rayhunter !== undefined}
<p><b>Rayhunter version:</b> {metadata.rayhunter.rayhunter_version}</p>
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
{:else}
<p>N/A (analysis generated by an older version of rayhunter)</p>
{/if}
{#if entry.upload_time}
<p>
<b>WebDAV uploaded at:</b>
<span class="text-green-700"
>{date_formatter.format(entry.upload_time)}</span
>
</p>
{/if}
</div>
{#if metadata && metadata.analyzers}
<div> <div>
<p class="text-lg underline">Enabled Analyzers</p> <p class="text-lg underline">Metadata</p>
<p>Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}</p>
<p><b>Device system OS:</b> {metadata.rayhunter.system_os}</p>
</div>
<div>
<p class="text-lg underline">Analyzers</p>
{#each metadata.analyzers as analyzer} {#each metadata.analyzers as analyzer}
<p><b>{analyzer.name}:</b> {analyzer.description}</p> <p><b>{analyzer.name}:</b> {analyzer.description}</p>
{/each} {/each}
</div> </div>
{:else}
<p>N/A (analysis generated by an older version of rayhunter)</p>
{/if} {/if}
</div> </div>
{/if} {/if}
+57 -170
View File
@@ -26,15 +26,12 @@
let scanning = $state(false); let scanning = $state(false);
let scanResults = $state<WifiNetwork[]>([]); let scanResults = $state<WifiNetwork[]>([]);
let dnsServersInput = $state(''); let dnsServersInput = $state('');
let webdavExpanded = $state(false);
let webdavUrlInput = $state<HTMLInputElement | null>(null);
async function load_config() { async function load_config() {
try { try {
loading = true; loading = true;
config = await get_config(); config = await get_config();
dnsServersInput = config.dns_servers ? config.dns_servers.join(', ') : ''; dnsServersInput = config.dns_servers ? config.dns_servers.join(', ') : '';
webdavExpanded = config.webdav.url.trim() !== '';
message = ''; message = '';
messageType = null; messageType = null;
poll_wifi_status(); poll_wifi_status();
@@ -348,173 +345,6 @@
</div> </div>
</div> </div>
<div class="border-t border-gray-200 pt-4 mt-6 space-y-3">
<h3 class="text-lg font-semibold text-gray-800 mb-4">WebDAV Upload</h3>
<p class="text-xs text-gray-500">
Once a recording has been closed for at least the configured age, both the
.qmdl and .ndjson files are uploaded in the background to the WebDAV server.
</p>
<div class="flex items-center">
<input
id="webdav_enabled"
type="checkbox"
checked={webdavExpanded}
onchange={(e) => {
webdavExpanded = e.currentTarget.checked;
if (webdavExpanded) {
setTimeout(() => webdavUrlInput?.focus(), 0);
} else {
if (config) config.webdav.url = '';
}
}}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
/>
<label for="webdav_enabled" class="ml-2 block text-sm text-gray-700">
Enable WebDAV upload
</label>
</div>
{#if webdavExpanded}
<div>
<label
for="webdav_url"
class="block text-sm font-medium text-gray-700 mb-1"
>
Server URL
</label>
<input
id="webdav_url"
type="url"
bind:this={webdavUrlInput}
bind:value={config.webdav.url}
onblur={() => {
if (config && config.webdav.url.trim() === '') {
webdavExpanded = false;
}
}}
placeholder="https://dav.example.com/rayhunter/"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">
Files are uploaded via HTTP PUT under this base URL. No folders are
created, and folders in this base URL are assumed to exist already.
</p>
</div>
<div>
<label
for="webdav_username"
class="block text-sm font-medium text-gray-700 mb-1"
>
Username
</label>
<input
id="webdav_username"
type="text"
bind:value={config.webdav.username}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">
Optional. Leave blank for unauthenticated uploads.
</p>
</div>
<div>
<label
for="webdav_password"
class="block text-sm font-medium text-gray-700 mb-1"
>
Password
</label>
<input
id="webdav_password"
type="password"
bind:value={config.webdav.password}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">
A password without a username will be rejected and the request will
be sent unauthenticated.
</p>
</div>
<div>
<label
for="webdav_upload_timeout_secs"
class="block text-sm font-medium text-gray-700 mb-1"
>
Upload Timeout (seconds)
</label>
<input
id="webdav_upload_timeout_secs"
type="number"
min="1"
bind:value={config.webdav.upload_timeout_secs}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
/>
</div>
<div>
<label
for="webdav_poll_interval_secs"
class="block text-sm font-medium text-gray-700 mb-1"
>
Poll Interval (seconds)
</label>
<input
id="webdav_poll_interval_secs"
type="number"
min="1"
bind:value={config.webdav.poll_interval_secs}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">
How often the worker checks for new entries to upload.
</p>
</div>
<div>
<label
for="webdav_min_age_secs"
class="block text-sm font-medium text-gray-700 mb-1"
>
Minimum Age Before Upload (seconds)
</label>
<input
id="webdav_min_age_secs"
type="number"
min="0"
bind:value={config.webdav.min_age_secs}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">
How long a recording must be closed before it becomes eligible for
upload.
</p>
</div>
<div class="flex items-center">
<input
id="webdav_delete_on_upload"
type="checkbox"
bind:checked={config.webdav.delete_on_upload}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
/>
<label
for="webdav_delete_on_upload"
class="ml-2 block text-sm text-gray-700"
>
Delete on successful upload
</label>
</div>
<p class="text-xs text-gray-500">
When enabled, the local files are removed after a successful upload.
Otherwise the manifest is just marked as uploaded.
</p>
{/if}
</div>
{#if config.device === 'orbic' || config.device === 'moxee' || config.device === 'tmobile' || config.device === 'wingtech'} {#if config.device === 'orbic' || config.device === 'moxee' || config.device === 'tmobile' || config.device === 'wingtech'}
<div class="border-t border-gray-200 pt-4 mt-6 space-y-3"> <div class="border-t border-gray-200 pt-4 mt-6 space-y-3">
<h3 class="text-lg font-semibold text-gray-800 mb-4">WiFi Client Mode</h3> <h3 class="text-lg font-semibold text-gray-800 mb-4">WiFi Client Mode</h3>
@@ -670,6 +500,63 @@
</div> </div>
{/if} {/if}
<div class="border-t border-gray-200 pt-4 mt-6 space-y-3">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Device Security</h3>
<div class="flex items-center">
<input
id="firewall_restrict_outbound"
type="checkbox"
bind:checked={config.firewall_restrict_outbound}
class="h-4 w-4 text-rayhunter-blue focus:ring-rayhunter-blue border-gray-300 rounded-sm"
/>
<label
for="firewall_restrict_outbound"
class="ml-2 block text-sm text-gray-700"
>
Restrict outbound traffic
</label>
</div>
<p class="text-xs text-gray-500">
Only allows DNS, DHCP, and HTTPS (port 443) outbound. Blocks all other
outbound connections on every interface (WiFi and cellular). Loopback and
hotspot traffic are always allowed. Changes take effect immediately.
</p>
{#if config.firewall_restrict_outbound}
<div>
<label
for="firewall_allowed_ports"
class="block text-sm font-medium text-gray-700 mb-1"
>
Additional Allowed Ports
</label>
<input
id="firewall_allowed_ports"
type="text"
value={config.firewall_allowed_ports
? config.firewall_allowed_ports.join(', ')
: ''}
oninput={(e) => {
const val = (e.target as HTMLInputElement).value.trim();
config!.firewall_allowed_ports =
val.length > 0
? val
.split(',')
.map((s) => parseInt(s.trim()))
.filter((n) => !isNaN(n) && n >= 1 && n <= 65535)
: null;
}}
placeholder="22, 80"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-hidden focus:ring-2 focus:ring-rayhunter-blue"
/>
<p class="text-xs text-gray-500 mt-1">
Comma-separated TCP ports, e.g. 22, 80
</p>
</div>
{/if}
</div>
<div class="border-t border-gray-200 pt-4 mt-6"> <div class="border-t border-gray-200 pt-4 mt-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4"> <h3 class="text-lg font-semibold text-gray-800 mb-4">
Analyzer Heuristic Settings Analyzer Heuristic Settings
-5
View File
@@ -12,7 +12,6 @@ interface JsonManifestEntry {
last_message_time: string; last_message_time: string;
qmdl_size_bytes: number; qmdl_size_bytes: number;
stop_reason: string | null; stop_reason: string | null;
upload_time: string | null;
} }
export class Manifest { export class Manifest {
@@ -60,7 +59,6 @@ export class ManifestEntry {
public analysis_status: AnalysisStatus | undefined = $state(undefined); public analysis_status: AnalysisStatus | undefined = $state(undefined);
public analysis_report: AnalysisReport | string | undefined = $state(undefined); public analysis_report: AnalysisReport | string | undefined = $state(undefined);
public stop_reason: string | undefined = $state(undefined); public stop_reason: string | undefined = $state(undefined);
public upload_time: Date | undefined = $state(undefined);
constructor(json: JsonManifestEntry) { constructor(json: JsonManifestEntry) {
this.name = json.name; this.name = json.name;
@@ -72,9 +70,6 @@ export class ManifestEntry {
if (json.stop_reason) { if (json.stop_reason) {
this.stop_reason = json.stop_reason; this.stop_reason = json.stop_reason;
} }
if (json.upload_time) {
this.upload_time = new Date(json.upload_time);
}
} }
get_readable_qmdl_size(): string { get_readable_qmdl_size(): string {
-11
View File
@@ -18,16 +18,6 @@ export enum enabled_notifications {
LowBattery = 'LowBattery', LowBattery = 'LowBattery',
} }
export interface WebdavConfig {
url: string;
username: string | null;
password: string | null;
upload_timeout_secs: number;
poll_interval_secs: number;
min_age_secs: number;
delete_on_upload: boolean;
}
export interface Config { export interface Config {
device: string; device: string;
ui_level: number; ui_level: number;
@@ -45,7 +35,6 @@ export interface Config {
dns_servers: string[] | null; dns_servers: string[] | null;
firewall_restrict_outbound: boolean; firewall_restrict_outbound: boolean;
firewall_allowed_ports: number[] | null; firewall_allowed_ports: number[] | null;
webdav: WebdavConfig;
} }
export interface WifiStatus { export interface WifiStatus {
+11 -23
View File
@@ -43,29 +43,17 @@ wifi_enabled = false
# Defaults to ["9.9.9.9", "149.112.112.112"] (Quad9) if not specified. # Defaults to ["9.9.9.9", "149.112.112.112"] (Quad9) if not specified.
# dns_servers = ["9.9.9.9", "149.112.112.112"] # dns_servers = ["9.9.9.9", "149.112.112.112"]
# WebDAV Upload # Device Security
# If a [webdav] section is present, finished recordings (both the raw .qmdl file # Restrict outbound traffic to essential services only (DHCP, DNS,
# and its .ndjson analysis output) are uploaded in the background to a WebDAV # HTTPS, and replies to inbound connections). Applies to all outbound
# server once they've been closed for at least min_age_secs. After a successful # interfaces (WiFi and cellular). Loopback and hotspot bridge traffic
# upload the entry is either marked as uploaded in the manifest, or deleted # are always allowed. Defaults to true (recommended).
# locally if delete_on_upload = true. With no [webdav] section, no upload firewall_restrict_outbound = true
# worker runs.
# # Additional TCP ports to allow outbound when the firewall is active.
# [webdav] # DHCP (67-68), DNS (53), and HTTPS (443) are always allowed.
# url = "https://dav.example.com/rayhunter" # Example: allow HTTP (80) and SSH (22).
# # HTTP Basic auth. Both fields are optional; a password without a username is # firewall_allowed_ports = [80, 22]
# # rejected and the request is sent unauthenticated.
# username = "user"
# password = "pass"
# # Timeout in seconds for each upload request (default 300).
# upload_timeout_secs = 300
# # How often the worker scans for eligible entries (default 3600).
# poll_interval_secs = 3600
# # Minimum age in seconds before an entry becomes eligible for upload
# # (default 86400 = 1 day).
# min_age_secs = 86400
# # Delete the entry locally after a successful upload (default false).
# delete_on_upload = false
# Analyzer Configuration # Analyzer Configuration
# Enable/disable specific IMSI catcher detection heuristics # Enable/disable specific IMSI catcher detection heuristics
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
CONFIG="/data/rayhunter/config.toml"
case "$1" in
start)
if grep -q '^firewall_restrict_outbound = true' "$CONFIG" 2>/dev/null; then
iptables -F OUTPUT
iptables -A OUTPUT -o lo -j ACCEPT
for br in bridge0 br0; do
[ -d "/sys/class/net/$br" ] && iptables -A OUTPUT -o "$br" -j ACCEPT
done
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -p udp --dport 67:68 -j ACCEPT
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
iptables -A OUTPUT -j DROP
echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables 2>/dev/null
fi
;;
stop)
iptables -F OUTPUT
iptables -P OUTPUT ACCEPT
;;
esac
+2 -33
View File
@@ -51,39 +51,8 @@ You can also configure WiFi during installation:
./installer orbic --admin-password 'mypassword' --wifi-ssid 'MyNetwork' --wifi-password 'networkpass' ./installer orbic --admin-password 'mypassword' --wifi-ssid 'MyNetwork' --wifi-password 'networkpass'
``` ```
## WebDAV Upload ## Device Security
Rayhunter can automatically upload finished recordings to a WebDAV server. When a `[webdav]` section is present in `config.toml`, a background worker periodically scans the recording store and uploads any closed entry that is older than `min_age_secs`. Each eligible entry uploads two files: the raw `.qmdl` capture and its `.ndjson` analysis output. After a successful upload the entry is either marked as uploaded in the manifest (and skipped on subsequent polls), or deleted locally if `delete_on_upload = true`. With no `[webdav]` section, no upload worker runs. - **Restrict outbound traffic** limits what the device can send over the network. When enabled, only DNS, DHCP, and HTTPS traffic is allowed; everything else is blocked. This is enabled by default and prevents the device from phoning home to the carrier over cellular. If you need to allow additional ports (for example, port 80 for HTTP or port 22 for SSH), add them to the **Additional allowed ports** list.
WebDAV upload is currently configurable only by editing `config.toml` — there is no web UI control for it yet.
| Key | Required | Default | Description |
| --- | --- | --- | --- |
| `url` | yes | — | WebDAV server base URL, e.g. `https://example.com/remote.php/files/user/rayhunter/` |
| `username` | no | — | HTTP Basic auth username |
| `password` | no | — | HTTP Basic auth password |
| `upload_timeout_secs` | no | `300` | Timeout (seconds) for each upload request |
| `poll_interval_secs` | no | `3600` | How often (seconds) the worker scans for eligible entries |
| `min_age_secs` | no | `86400` | Minimum age (seconds) an entry must have before it becomes eligible for upload |
| `delete_on_upload` | no | `false` | Delete the entry locally after a successful upload |
Example:
```toml
[webdav]
url = "https://dav.example.com/rayhunter/"
username = "user"
password = "pass"
upload_timeout_secs = 300
poll_interval_secs = 3600
min_age_secs = 86400
delete_on_upload = false
```
A few notes on behavior:
- **Auth:** HTTP Basic. Supplying a `password` without a `username` is rejected — the request is sent unauthenticated and a warning is logged.
- **Retries and overwrites:** each entry's two files (`.qmdl` and `.ndjson`) must both upload successfully before the entry is marked as uploaded in the manifest. If one upload fails, the entry stays unmarked and both files are retried on the next poll — the one that previously succeeded will be overwritten on the server. Once an entry is marked as uploaded, Rayhunter will not upload it again.
- **Currently-recording entry:** the active recording is never uploaded; only closed entries are eligible.
If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in). If you prefer editing `config.toml` file, you need to obtain a shell on your [Orbic](./orbic.md#obtaining-a-shell) or [TP-Link](./tplink-m7350.md#obtaining-a-shell) device and edit the file manually. You can view the [default configuration file on GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).
+3
View File
@@ -44,6 +44,9 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
# Note: the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip' # Note: the arguments --admin-username 'myusername' and --admin-ip 'mydeviceip'
# may be required if different from the default. # may be required if different from the default.
# Optionally configure WiFi client mode during install (Orbic and Moxee only):
./installer orbic --admin-password 'mypassword' --wifi-ssid 'MyNetwork' --wifi-password 'networkpass'
# Or install over USB if you want ADB and a root shell (not recommended for most users) # Or install over USB if you want ADB and a root shell (not recommended for most users)
./installer orbic-usb ./installer orbic-usb
+1 -2
View File
@@ -18,8 +18,7 @@ The TP-Link M7350 supports many more frequency bands than Orbic and therefore wo
The TP-Link comes in many different *hardware versions*. Support for installation varies: The TP-Link comes in many different *hardware versions*. Support for installation varies:
* `1.0`, Confirmed working. Successfully tested by a user with the Windows installer (rayhunter-v0.10.2-windows-x86_64). Ensure the SD card is formatted as FAT32 before installation. * `1.0`, `2.0`: **Not supported**, devs are not able to obtain a device
* `2.0`: **Not supported**, devs are not able to obtain a device
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: **Tested, no known issues since 0.3.0.** * `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: **Tested, no known issues since 0.3.0.**
* `6.2`: **One user reported it is working, not tested** * `6.2`: **One user reported it is working, not tested**
* `4.0`: **Manual firmware downgrade required** ([issue](https://github.com/EFForg/rayhunter/issues/332)) * `4.0`: **Manual firmware downgrade required** ([issue](https://github.com/EFForg/rayhunter/issues/332))
+54 -69
View File
@@ -975,9 +975,9 @@
} }
}, },
"node_modules/@tauri-apps/api": { "node_modules/@tauri-apps/api": {
"version": "2.11.0", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -985,9 +985,9 @@
} }
}, },
"node_modules/@tauri-apps/cli": { "node_modules/@tauri-apps/cli": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz",
"integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==", "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==",
"dev": true, "dev": true,
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"bin": { "bin": {
@@ -1001,23 +1001,23 @@
"url": "https://opencollective.com/tauri" "url": "https://opencollective.com/tauri"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.11.1", "@tauri-apps/cli-darwin-arm64": "2.10.1",
"@tauri-apps/cli-darwin-x64": "2.11.1", "@tauri-apps/cli-darwin-x64": "2.10.1",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1",
"@tauri-apps/cli-linux-arm64-gnu": "2.11.1", "@tauri-apps/cli-linux-arm64-gnu": "2.10.1",
"@tauri-apps/cli-linux-arm64-musl": "2.11.1", "@tauri-apps/cli-linux-arm64-musl": "2.10.1",
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1",
"@tauri-apps/cli-linux-x64-gnu": "2.11.1", "@tauri-apps/cli-linux-x64-gnu": "2.10.1",
"@tauri-apps/cli-linux-x64-musl": "2.11.1", "@tauri-apps/cli-linux-x64-musl": "2.10.1",
"@tauri-apps/cli-win32-arm64-msvc": "2.11.1", "@tauri-apps/cli-win32-arm64-msvc": "2.10.1",
"@tauri-apps/cli-win32-ia32-msvc": "2.11.1", "@tauri-apps/cli-win32-ia32-msvc": "2.10.1",
"@tauri-apps/cli-win32-x64-msvc": "2.11.1" "@tauri-apps/cli-win32-x64-msvc": "2.10.1"
} }
}, },
"node_modules/@tauri-apps/cli-darwin-arm64": { "node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz",
"integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==", "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1032,9 +1032,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-darwin-x64": { "node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz",
"integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==", "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1049,9 +1049,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz",
"integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==", "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1066,16 +1066,13 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm64-gnu": { "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz",
"integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==", "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1086,16 +1083,13 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm64-musl": { "node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz",
"integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==", "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1106,16 +1100,13 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": { "node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz",
"integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==", "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1126,16 +1117,13 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-x64-gnu": { "node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz",
"integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==", "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1146,16 +1134,13 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-x64-musl": { "node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz",
"integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==", "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1166,9 +1151,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-arm64-msvc": { "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz",
"integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==", "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1183,9 +1168,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-ia32-msvc": { "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz",
"integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==", "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1200,9 +1185,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-x64-msvc": { "node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.11.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz",
"integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==", "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1217,12 +1202,12 @@
} }
}, },
"node_modules/@tauri-apps/plugin-opener": { "node_modules/@tauri-apps/plugin-opener": {
"version": "2.5.4", "version": "2.5.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz",
"integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.11.0" "@tauri-apps/api": "^2.8.0"
} }
}, },
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "installer-gui" name = "installer-gui"
version = "0.11.0" version = "0.10.2"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "installer" name = "installer"
version = "0.11.0" version = "0.10.2"
edition = "2024" edition = "2024"
[lib] [lib]
+18 -11
View File
@@ -1,6 +1,8 @@
use std::path::Path; use std::path::Path;
use std::process::exit;
fn main() { fn main() {
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
println!("cargo::rerun-if-env-changed=FIRMWARE_PROFILE"); println!("cargo::rerun-if-env-changed=FIRMWARE_PROFILE");
let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| { let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| {
// Default to firmware-devel for debug builds, firmware for release builds // Default to firmware-devel for debug builds, firmware for release builds
@@ -24,19 +26,24 @@ fn main() {
fn set_binary_var(include_dir: &Path, var: &str, file: &str) { fn set_binary_var(include_dir: &Path, var: &str, file: &str) {
println!("cargo::rerun-if-env-changed={var}"); println!("cargo::rerun-if-env-changed={var}");
if std::env::var_os(var).is_some() { if std::env::var_os("NO_FIRMWARE_BIN").is_some() {
let out_dir = std::env::var("OUT_DIR").unwrap();
std::fs::create_dir_all(&out_dir).unwrap();
let blank = Path::new(&out_dir).join("blank");
std::fs::write(&blank, []).unwrap();
println!("cargo::rustc-env={var}={}", blank.display());
return; return;
} }
let binary = include_dir.join(file); if std::env::var_os(var).is_none() {
println!("cargo::rerun-if-changed={}", binary.display()); let binary = include_dir.join(file);
if binary.exists() { println!("cargo::rerun-if-changed={}", binary.display());
if !binary.exists() {
println!(
"cargo::error=Firmware binary {file} not present at {}",
binary.display()
);
exit(0);
}
println!("cargo::rustc-env={var}={}", binary.display()); println!("cargo::rustc-env={var}={}", binary.display());
} else {
println!(
"cargo::warning=Firmware binary {file} not present at {}; \
installers that need it will fail",
binary.display()
);
println!("cargo::rustc-env={var}=");
} }
} }
+9 -16
View File
@@ -47,27 +47,20 @@ pub async fn install_config<C: DeviceConnection>(
/// ///
/// Skips any binary that is already present on the device (e.g. provided by firmware), /// Skips any binary that is already present on the device (e.g. provided by firmware),
/// since those may be newer or better-integrated than the bundled versions. /// since those may be newer or better-integrated than the bundled versions.
/// pub async fn install_wifi_tools<C: DeviceConnection>(
/// In debug builds the wpa-supplicant binaries may not be bundled (build.rs sets the conn: &mut C,
/// env vars to empty in that case); when so, this is a no-op so devs don't have to wpa_supplicant: &[u8],
/// build wpa-supplicant just to install on Orbic. wpa_cli: &[u8],
pub async fn install_wifi_tools<C: DeviceConnection>(conn: &mut C) -> Result<()> { iw: &[u8],
if env!("FILE_WPA_SUPPLICANT").is_empty() { ) -> Result<()> {
println!("wifi tools were not built into this installer, skipping");
return Ok(());
}
let tools: &[(&str, &str, &[u8])] = &[ let tools: &[(&str, &str, &[u8])] = &[
( (
"wpa_supplicant", "wpa_supplicant",
"/data/rayhunter/bin/wpa_supplicant", "/data/rayhunter/bin/wpa_supplicant",
crate::get_file!("FILE_WPA_SUPPLICANT"), wpa_supplicant,
), ),
( ("wpa_cli", "/data/rayhunter/bin/wpa_cli", wpa_cli),
"wpa_cli", ("iw", "/data/rayhunter/bin/iw", iw),
"/data/rayhunter/bin/wpa_cli",
crate::get_file!("FILE_WPA_CLI"),
),
("iw", "/data/rayhunter/bin/iw", crate::get_file!("FILE_IW")),
]; ];
for &(name, dest, payload) in tools { for &(name, dest, payload) in tools {
if device_has_binary(conn, name).await { if device_has_binary(conn, name).await {
-23
View File
@@ -1,23 +0,0 @@
#[cfg(debug_assertions)]
macro_rules! get_file {
($var:literal) => {{
let path = env!($var);
match ::std::fs::read(path) {
Ok(bytes) => bytes.leak() as &'static [u8],
Err(e) => panic!("Failed to read file for {}: {}", $var, e),
}
}};
}
#[cfg(not(debug_assertions))]
macro_rules! get_file {
($var:literal) => {{
const _: () = assert!(
!env!($var).is_empty(),
concat!($var, " was not bundled at build time"),
);
include_bytes!(env!($var)) as &'static [u8]
}};
}
pub(crate) use get_file;
-3
View File
@@ -6,9 +6,6 @@ use env_logger::Env;
use anyhow::bail; use anyhow::bail;
mod connection; mod connection;
mod files;
pub(crate) use files::*;
mod moxee; mod moxee;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
mod orbic; mod orbic;
+16 -3
View File
@@ -136,7 +136,7 @@ async fn force_debug_mode() -> Result<ADBUSBDevice> {
} }
async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> { async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
let rootshell_bin = crate::get_file!("FILE_ROOTSHELL"); let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
install_file(adb_device, "/bin/rootshell", rootshell_bin).await?; install_file(adb_device, "/bin/rootshell", rootshell_bin).await?;
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
@@ -151,7 +151,7 @@ async fn setup_rootshell(adb_device: &mut ADBUSBDevice) -> Result<()> {
} }
async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result<ADBUSBDevice> { async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Result<ADBUSBDevice> {
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON"); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
adb_at_syscmd( adb_at_syscmd(
&mut adb_device, &mut adb_device,
@@ -170,7 +170,13 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
device: &mut adb_device, device: &mut adb_device,
}; };
install_config(&mut conn, "orbic", reset_config).await?; install_config(&mut conn, "orbic", reset_config).await?;
install_wifi_tools(&mut conn).await?; install_wifi_tools(
&mut conn,
include_bytes!(env!("FILE_WPA_SUPPLICANT")),
include_bytes!(env!("FILE_WPA_CLI")),
include_bytes!(env!("FILE_IW")),
)
.await?;
} }
install_file( install_file(
@@ -185,8 +191,15 @@ async fn setup_rayhunter(mut adb_device: ADBUSBDevice, reset_config: bool) -> Re
include_bytes!("../../dist/scripts/misc-daemon"), include_bytes!("../../dist/scripts/misc-daemon"),
) )
.await?; .await?;
install_file(
&mut adb_device,
"/etc/init.d/S01iptables",
include_bytes!("../../dist/scripts/S01iptables"),
)
.await?;
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/rayhunter_daemon").await?; adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?; adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/S01iptables").await?;
println!("done"); println!("done");
print!("Waiting for reboot... "); print!("Waiting for reboot... ");
adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?; adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?;
+22 -2
View File
@@ -216,7 +216,7 @@ async fn wait_for_telnet(admin_ip: &str) -> Result<()> {
async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> { async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) -> Result<()> {
let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?; let addr = SocketAddr::from_str(&format!("{admin_ip}:{TELNET_PORT}"))?;
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON"); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
// Remount filesystem as read-write to allow modifications // Remount filesystem as read-write to allow modifications
// This is really only necessary for the Moxee Hotspot // This is really only necessary for the Moxee Hotspot
@@ -248,7 +248,13 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
) )
.await?; .await?;
install_wifi_tools(&mut conn).await?; install_wifi_tools(
&mut conn,
include_bytes!(env!("FILE_WPA_SUPPLICANT")),
include_bytes!(env!("FILE_WPA_CLI")),
include_bytes!(env!("FILE_IW")),
)
.await?;
install_config(&mut conn, "orbic", reset_config).await?; install_config(&mut conn, "orbic", reset_config).await?;
@@ -267,6 +273,13 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
false, false,
) )
.await?; .await?;
telnet_send_file(
addr,
"/etc/init.d/S01iptables",
include_bytes!("../../dist/scripts/S01iptables"),
false,
)
.await?;
telnet_send_command( telnet_send_command(
addr, addr,
@@ -289,6 +302,13 @@ async fn setup_rayhunter(admin_ip: &str, reset_config: bool, data_dir: &str) ->
false, false,
) )
.await?; .await?;
telnet_send_command(
addr,
"chmod 755 /etc/init.d/S01iptables",
"exit code 0",
false,
)
.await?;
println!("Installation complete. Rebooting device..."); println!("Installation complete. Rebooting device...");
telnet_send_command(addr, "shutdown -r -t 1 now", "", false) telnet_send_command(addr, "shutdown -r -t 1 now", "", false)
+1 -1
View File
@@ -29,7 +29,7 @@ pub async fn install() -> Result<()> {
run_command_expect(&mut adb, "mount -o remount,rw /", "exit code 0").await?; run_command_expect(&mut adb, "mount -o remount,rw /", "exit code 0").await?;
run_command_expect(&mut adb, "mkdir -p /data/rayhunter", "exit code 0").await?; run_command_expect(&mut adb, "mkdir -p /data/rayhunter", "exit code 0").await?;
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON"); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
adb.write_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin) adb.write_file("/data/rayhunter/rayhunter-daemon", rayhunter_daemon_bin)
.await?; .await?;
adb.write_file( adb.write_file(
+1 -1
View File
@@ -48,7 +48,7 @@ async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
) )
.await?; .await?;
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON"); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
telnet_send_file( telnet_send_file(
addr, addr,
"/data/rayhunter/rayhunter-daemon", "/data/rayhunter/rayhunter-daemon",
+1 -1
View File
@@ -188,7 +188,7 @@ async fn tplink_run_install(
install_config(&mut conn, "tplink", reset_config).await?; install_config(&mut conn, "tplink", reset_config).await?;
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON"); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
telnet_send_file( telnet_send_file(
addr, addr,
+1 -1
View File
@@ -148,7 +148,7 @@ async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> {
adb_device.shell_command(&["mount", "-o", "remount,rw", "/system"], &mut buf)?; adb_device.shell_command(&["mount", "-o", "remount,rw", "/system"], &mut buf)?;
// Install rayhunter daemon binary with verification // Install rayhunter daemon binary with verification
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON"); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
install_file( install_file(
adb_device, adb_device,
"/data/rayhunter/rayhunter-daemon", "/data/rayhunter/rayhunter-daemon",
+2 -1
View File
@@ -26,6 +26,7 @@ pub async fn install(
Args { Args {
admin_ip, admin_ip,
admin_password, admin_password,
..
}: Args, }: Args,
) -> Result<()> { ) -> Result<()> {
wingtech_run_install(admin_ip, admin_password).await wingtech_run_install(admin_ip, admin_password).await
@@ -104,7 +105,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul
) )
.await?; .await?;
let rayhunter_daemon_bin = crate::get_file!("FILE_RAYHUNTER_DAEMON"); let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
telnet_send_file( telnet_send_file(
addr, addr,
"/data/rayhunter/rayhunter-daemon", "/data/rayhunter/rayhunter-daemon",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter" name = "rayhunter"
version = "0.11.0" version = "0.10.2"
edition = "2024" edition = "2024"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection" description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rootshell" name = "rootshell"
version = "0.11.0" version = "0.10.2"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+3 -3
View File
@@ -52,10 +52,10 @@ build_wifi_tools() {
fi fi
if ! command -v arm-linux-musleabihf-gcc &> /dev/null; then if ! command -v arm-linux-musleabihf-gcc &> /dev/null; then
echo "Warning: Skipping building WiFi tools due to missing C crosscompiler." echo "Error: arm-linux-musleabihf-gcc not found."
echo "arm-linux-musleabihf-gcc not found."
echo "Install with: brew install FiloSottile/musl-cross/musl-cross" echo "Install with: brew install FiloSottile/musl-cross/musl-cross"
return echo "(Required because the installer bundles wpa_supplicant, wpa_cli, and iw for orbic-family devices.)"
exit 1
fi fi
echo "Building WiFi tools..." echo "Building WiFi tools..."
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "telcom-parser" name = "telcom-parser"
version = "0.11.0" version = "0.10.2"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html