Compare commits

..

4 Commits

Author SHA1 Message Date
Cooper Quintin afeda3875d fmt 2026-05-01 14:34:11 -07:00
Cooper Quintin ced4090be5 appease clippy 2026-05-01 13:43:26 -07:00
Cooper Quintin 1471bb6f0b cargo fmt 2026-05-01 12:34:38 -07:00
Cooper Quintin ebc0ddb6b3 first pass at false positive removal 2026-05-01 12:31:50 -07:00
29 changed files with 612 additions and 794 deletions
+7 -10
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"
@@ -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: |
Generated
+174 -466
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]
+2 -2
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"] }
+9 -3
View File
@@ -46,8 +46,12 @@ 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>>,
/// Optional WebDAV upload configuration. When unset, no upload worker runs.
pub webdav: Option<WebdavConfig>,
} }
/// Configuration for uploading finished QMDL recordings to a WebDAV server. /// Configuration for uploading finished QMDL recordings to a WebDAV server.
@@ -105,7 +109,9 @@ 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,
webdav: None,
} }
} }
} }
+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
View File
@@ -5,6 +5,7 @@ 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;
+4 -2
View File
@@ -5,6 +5,7 @@ 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;
@@ -287,13 +288,14 @@ 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() { if let Some(webdav_config) = config.webdav.clone() {
run_webdav_upload_worker( run_webdav_upload_worker(
&task_tracker, &task_tracker,
shutdown_token.clone(), shutdown_token.clone(),
qmdl_store_lock.clone(), qmdl_store_lock.clone(),
config.webdav.clone().into(), webdav_config.into(),
); );
} }
+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 => {
+1 -1
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",
-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 {
+14 -1
View File
@@ -43,6 +43,18 @@ 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"]
# Device Security
# Restrict outbound traffic to essential services only (DHCP, DNS,
# HTTPS, and replies to inbound connections). Applies to all outbound
# interfaces (WiFi and cellular). Loopback and hotspot bridge traffic
# are always allowed. Defaults to true (recommended).
firewall_restrict_outbound = true
# Additional TCP ports to allow outbound when the firewall is active.
# DHCP (67-68), DNS (53), and HTTPS (443) are always allowed.
# Example: allow HTTP (80) and SSH (22).
# firewall_allowed_ports = [80, 22]
# WebDAV Upload # WebDAV Upload
# If a [webdav] section is present, finished recordings (both the raw .qmdl file # If a [webdav] section is present, finished recordings (both the raw .qmdl file
# and its .ndjson analysis output) are uploaded in the background to a WebDAV # and its .ndjson analysis output) are uploaded in the background to a WebDAV
@@ -52,7 +64,8 @@ wifi_enabled = false
# worker runs. # worker runs.
# #
# [webdav] # [webdav]
# url = "https://dav.example.com/rayhunter" # host = "https://dav.example.com"
# remote_path = "/rayhunter"
# # HTTP Basic auth. Both fields are optional; a password without a username is # # HTTP Basic auth. Both fields are optional; a password without a username is
# # rejected and the request is sent unauthenticated. # # rejected and the request is sent unauthenticated.
# username = "user" # username = "user"
+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
+4
View File
@@ -51,6 +51,10 @@ 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'
``` ```
## Device Security
- **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 ## WebDAV Upload
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. 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.
+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]
+7
View File
@@ -185,8 +185,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?;
+14
View File
@@ -267,6 +267,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 +296,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
@@ -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"
+127 -9
View File
@@ -7,8 +7,13 @@ use super::analyzer::{Analyzer, Event, EventType};
use super::information_element::{InformationElement, LteInformationElement}; use super::information_element::{InformationElement, LteInformationElement};
use log::debug; use log::debug;
use pycrate_rs::nas::generated::emm::emm_attach_reject::EMMCauseEMMCause as AttachRejectEMMCause;
use pycrate_rs::nas::generated::emm::emm_attach_request::TAI;
use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1};
use telcom_parser::lte_rrc::{MCC_MNC_Digit, PLMN_Identity, PLMN_IdentityList};
use telcom_parser::lte_rrc::{ use telcom_parser::lte_rrc::{
DL_DCCH_MessageType, DL_DCCH_MessageType_c1, UL_CCCH_MessageType, UL_CCCH_MessageType_c1, /* DL_DCCH_MessageType, DL_DCCH_MessageType_c1,*/ UL_CCCH_MessageType,
UL_CCCH_MessageType_c1,
}; };
const TIMEOUT_THRESHHOLD: usize = 50; const TIMEOUT_THRESHHOLD: usize = 50;
@@ -26,6 +31,8 @@ pub struct ImsiRequestedAnalyzer {
state: State, state: State,
timeout_counter: usize, timeout_counter: usize,
flag: Option<Event>, flag: Option<Event>,
likely_enb_plmn: String,
likely_ue_plmn: String,
} }
impl Default for ImsiRequestedAnalyzer { impl Default for ImsiRequestedAnalyzer {
@@ -40,6 +47,10 @@ impl ImsiRequestedAnalyzer {
state: State::Unattached, state: State::Unattached,
timeout_counter: 0, timeout_counter: 0,
flag: None, flag: None,
// You will likely wonder why this isn't an Option<PLMN{mcc: u32, mnc: u32}>
// The answer is that I like strings.
likely_enb_plmn: "Unknown".to_string(),
likely_ue_plmn: "Unknown".to_string(),
} }
} }
@@ -72,10 +83,20 @@ impl ImsiRequestedAnalyzer {
// IMSI to Disconnect without AuthAccept // IMSI to Disconnect without AuthAccept
(State::IdentityRequest, State::Disconnect) => { (State::IdentityRequest, State::Disconnect) => {
self.flag = Some(Event { if self.likely_enb_plmn == self.likely_ue_plmn {
event_type: EventType::High, self.flag = Some(Event {
message: "Disconnected after Identity Request without Auth Accept".to_string(), event_type: EventType::High,
}); message: "Disconnected after Identity Request without Auth Accept on home network!".to_string(),
});
} else {
self.flag = Some(Event {
event_type: EventType::Low,
message: format!(
"Disconnected after Identity Request without Auth Accept, but this could be a false positive roaming issue - Tower PLMN: {}, UE PLMN: {}",
self.likely_enb_plmn, self.likely_ue_plmn
),
});
}
} }
(_, State::IdentityRequest) => { (_, State::IdentityRequest) => {
@@ -92,6 +113,71 @@ impl ImsiRequestedAnalyzer {
} }
self.state = next_state; self.state = next_state;
} }
// Sometimes an ENB can have multiple PLMNS
fn format_plmn_list(&mut self, plmn_list: &PLMN_IdentityList) -> String {
plmn_list
.0
.iter()
.map(|info| self.plmn_identity_to_str(&info.plmn_identity))
.collect::<Vec<_>>()
.join(", ")
}
// PLMN is represented in two very different ways in the LTE spec so we need
// two very different functions to decode them. I hate this.
fn plmn_identity_to_str(&mut self, plmn: &PLMN_Identity) -> String {
let mcc_digits: String = plmn
.mcc
.as_ref()
.map(|mcc| {
mcc.0
.iter()
.map(|MCC_MNC_Digit(n)| n.to_string())
.collect::<String>()
})
.unwrap_or_default();
let mnc_digits: String = plmn
.mnc
.0
.iter()
.map(|MCC_MNC_Digit(n)| n.to_string())
.collect::<String>();
format!("{}-{}", mcc_digits, mnc_digits)
}
fn plmn_vec_to_str(&mut self, bytes: &[u8]) -> String {
let mcc_digit1 = bytes[0] & 0x0F;
let mcc_digit2 = (bytes[0] >> 4) & 0x0F;
let mcc_digit3 = bytes[1] & 0x0F;
let mnc_digit1 = bytes[2] & 0x0F;
let mnc_digit2 = (bytes[2] >> 4) & 0x0F;
let mnc_digit3 = (bytes[1] >> 4) & 0x0F;
let mcc = mcc_digit1 as u32 * 100 + mcc_digit2 as u32 * 10 + mcc_digit3 as u32;
let mcc_str = format!("{:03}", mcc);
let mnc_str = if mnc_digit3 == 0xF {
format!("{:02}", mnc_digit1 * 10 + mnc_digit2)
} else {
format!(
"{:03}",
mnc_digit1 as u32 * 100 + mnc_digit2 as u32 * 10 + mnc_digit3 as u32
)
};
format!("{}-{}", mcc_str, mnc_str)
}
fn extract_plmn(&mut self, old_tai: &Option<TAI>) -> String {
match old_tai {
Some(t) => self.plmn_vec_to_str(&t.plmn),
None => "Unknown".to_string(),
}
}
} }
impl Analyzer for ImsiRequestedAnalyzer { impl Analyzer for ImsiRequestedAnalyzer {
@@ -106,7 +192,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
} }
fn get_version(&self) -> u32 { fn get_version(&self) -> u32 {
3 4
} }
fn analyze_information_element( fn analyze_information_element(
@@ -114,11 +200,29 @@ impl Analyzer for ImsiRequestedAnalyzer {
ie: &InformationElement, ie: &InformationElement,
packet_num: usize, packet_num: usize,
) -> Option<Event> { ) -> Option<Event> {
// Set the enodeb plmn to the last sib1 we got, we should improve this once we have PCI data, this
// is a naive approach.
if let InformationElement::LTE(lte_ie) = ie
&& let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
&& let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
&& let BCCH_DL_SCH_MessageType_c1::SystemInformationBlockType1(sib1) = c1
{
let plmn = &sib1.cell_access_related_info.plmn_identity_list;
self.likely_enb_plmn = self.format_plmn_list(plmn);
return None;
}
if let InformationElement::LTE(inner) = ie { if let InformationElement::LTE(inner) = ie {
match &**inner { match &**inner {
LteInformationElement::NAS(payload) => match payload { LteInformationElement::NAS(payload) => match payload {
NASMessage::EMMMessage(EMMMessage::EMMExtServiceRequest(_)) NASMessage::EMMMessage(EMMMessage::EMMAttachRequest(request)) => {
| NASMessage::EMMMessage(EMMMessage::EMMAttachRequest(_)) => { if self.likely_ue_plmn == "Unknown" {
self.likely_ue_plmn = self.extract_plmn(&request.old_tai.inner);
}
self.transition(State::AttachRequest, packet_num);
}
NASMessage::EMMMessage(EMMMessage::EMMExtServiceRequest(_)) => {
self.transition(State::AttachRequest, packet_num); self.transition(State::AttachRequest, packet_num);
} }
NASMessage::EMMMessage(EMMMessage::EMMIdentityRequest(_)) => { NASMessage::EMMMessage(EMMMessage::EMMIdentityRequest(_)) => {
@@ -129,12 +233,22 @@ impl Analyzer for ImsiRequestedAnalyzer {
self.transition(State::AuthAccept, packet_num); self.transition(State::AuthAccept, packet_num);
} }
NASMessage::EMMMessage(EMMMessage::EMMServiceReject(_)) NASMessage::EMMMessage(EMMMessage::EMMServiceReject(_))
| NASMessage::EMMMessage(EMMMessage::EMMAttachReject(_))
| NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMO(_)) | NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMO(_))
| NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMT(_)) | NASMessage::EMMMessage(EMMMessage::EMMDetachRequestMT(_))
| NASMessage::EMMMessage(EMMMessage::EMMTrackingAreaUpdateReject(_)) => { | NASMessage::EMMMessage(EMMMessage::EMMTrackingAreaUpdateReject(_)) => {
self.transition(State::Disconnect, packet_num); self.transition(State::Disconnect, packet_num);
} }
NASMessage::EMMMessage(EMMMessage::EMMAttachReject(reject)) => {
self.transition(State::Disconnect, packet_num);
if reject.emm_cause.inner
== AttachRejectEMMCause::EPSServicesAndNonEPSServicesNotAllowed
{
self.flag = Some(Event {
event_type: EventType::Low,
message: "Identity requested without authentication but its likely a false positive unless your SIM card has an active plan".to_string(),
});
}
}
_ => {} _ => {}
}, },
@@ -148,6 +262,9 @@ impl Analyzer for ImsiRequestedAnalyzer {
_ => {} _ => {}
}, },
// This causes two messages in the event of a false positive when we should always get an attach reject anyway so
// I'm commentingit out until I figure out a smarter way to deal with it.
/*
LteInformationElement::DlDcch(rrc_payload) => { LteInformationElement::DlDcch(rrc_payload) => {
if let DL_DCCH_MessageType::C1(DL_DCCH_MessageType_c1::RrcConnectionRelease( if let DL_DCCH_MessageType::C1(DL_DCCH_MessageType_c1::RrcConnectionRelease(
_, _,
@@ -156,6 +273,7 @@ impl Analyzer for ImsiRequestedAnalyzer {
self.transition(State::Disconnect, packet_num) self.transition(State::Disconnect, packet_num)
} }
} }
*/
_ => {} _ => {}
} }
}; };
+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
+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