,
22 | pub analyzers: AnalyzerConfig,
23 | }
24 |
25 | impl Default for Config {
26 | fn default() -> Self {
27 | Config {
28 | qmdl_store_path: "/data/rayhunter/qmdl".to_string(),
29 | port: 8080,
30 | debug_mode: false,
31 | device: Device::Orbic,
32 | ui_level: 1,
33 | colorblind_mode: false,
34 | key_input_mode: 0,
35 | analyzers: AnalyzerConfig::default(),
36 | ntfy_url: None,
37 | enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
38 | }
39 | }
40 | }
41 |
42 | pub async fn parse_config(path: P) -> Result
43 | where
44 | P: AsRef,
45 | {
46 | if let Ok(config_file) = tokio::fs::read_to_string(&path).await {
47 | Ok(toml::from_str(&config_file).map_err(RayhunterError::ConfigFileParsingError)?)
48 | } else {
49 | warn!("unable to read config file, using default config");
50 | Ok(Config::default())
51 | }
52 | }
53 |
54 | pub struct Args {
55 | pub config_path: String,
56 | }
57 |
58 | pub fn parse_args() -> Args {
59 | let args: Vec = std::env::args().collect();
60 | if args.len() != 2 {
61 | println!("Usage: {} /path/to/config/file", args[0]);
62 | std::process::exit(1);
63 | }
64 | Args {
65 | config_path: args[1].clone(),
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/components/RecordingControls.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | {#if server_is_recording}
12 |
18 | {#snippet icon()}
19 |
28 |
31 |
32 | {/snippet}
33 |
34 | {:else}
35 |
41 | {#snippet icon()}
42 |
51 |
56 |
57 | {/snippet}
58 |
59 | {/if}
60 |
61 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/analysisManager.svelte.ts:
--------------------------------------------------------------------------------
1 | import { get_report, type AnalysisReport } from './analysis.svelte';
2 | import { req } from './utils.svelte';
3 |
4 | export enum AnalysisStatus {
5 | // rayhunter is currently analyzing this entry (note that this is distinct
6 | // from the currently-recording entry)
7 | Running,
8 | // this entry is queued to be analyzed
9 | Queued,
10 | // analysis is finished, and the new report can be accessed
11 | Finished,
12 | }
13 |
14 | type AnalysisStatusJson = {
15 | running: string | null;
16 | queued: string[];
17 | finished: string[];
18 | };
19 |
20 | export type AnalysisResult = {
21 | name: string;
22 | status: AnalysisStatus;
23 | };
24 |
25 | export class AnalysisManager {
26 | public status: Map = $state(new Map());
27 | public reports: Map = $state(new Map());
28 | public set_queued_status(name: string) {
29 | this.status.set(name, AnalysisStatus.Queued);
30 | this.reports.delete(name);
31 | }
32 |
33 | public async update() {
34 | const status: AnalysisStatusJson = JSON.parse(await req('GET', '/api/analysis'));
35 | if (status.running) {
36 | this.status.set(status.running, AnalysisStatus.Running);
37 | }
38 |
39 | for (const entry of status.queued) {
40 | this.status.set(entry, AnalysisStatus.Queued);
41 | }
42 |
43 | for (const entry of status.finished) {
44 | // if entry was already finished, nothing to do
45 | if (this.status.get(entry) === AnalysisStatus.Finished) {
46 | continue;
47 | }
48 |
49 | this.status.set(entry, AnalysisStatus.Finished);
50 |
51 | // fetch the analysis report
52 | this.reports.delete(entry);
53 | get_report(entry)
54 | .then((report) => {
55 | this.reports.set(entry, report);
56 | })
57 | .catch((err) => {
58 | this.reports.set(entry, `Failed to get analysis: ${err}`);
59 | });
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/doc/orbic.md:
--------------------------------------------------------------------------------
1 | # Orbic/Kajeet RC400L
2 |
3 | The Orbic RC400L is an inexpensive LTE modem primarily designed for the US market, and the original device for which Rayhunter is developed.
4 |
5 | It is also sometimes sold under the brand Kajeet RC400L. This is the exact same hardware and can be treated the same.
6 |
7 | You can buy an Orbic [using bezos
8 | bucks](https://www.amazon.com/Orbic-Verizon-Hotspot-Connect-Enabled/dp/B08N3CHC4Y),
9 | or on [eBay](https://www.ebay.com/sch/i.html?_nkw=orbic+rc400l).
10 |
11 | [Please check whether the Orbic works in your country](https://www.frequencycheck.com/countries/), and whether the Orbic RC400L supports the right frequency bands for your purpose before buying.
12 |
13 | ## Supported Bands
14 |
15 | | Frequency | Band |
16 | | ------- | ------------------ |
17 | | 5G (wideband,midband,nationwide) | n260/n261, n77, n2/5/48/66 |
18 | | 4G | 2/4/5/12/13/48/66 |
19 | | Global & Roaming | n257/n78 |
20 | | Wifi 2.4Ghz | b/g/n |
21 | | Wifi 5Ghz | a/ac/ax |
22 | | Wifi 6 | 🮱 |
23 |
24 | ## Two kinds of installers
25 |
26 | The orbic's installation routine underwent many different changes:
27 |
28 | 1. The ADB-based shellscript prior to version 0.3.0
29 | 2. The Rust-based, ADB-based installer since version 0.3.0
30 | 3. Then, starting with 0.6.0, an alternative installer `./installer
31 | orbic-network` that is supposed to work more reliably, can run over the
32 | Orbic's WiFi connection and without the need to manually install USB drivers
33 | on Windows.
34 | 4. Starting with 0.8.0, `orbic-network` has been renamed to `orbic`, and the
35 | old `./installer orbic` is now called `./installer orbic-usb`.
36 |
37 | It's possible that many tutorials out there still refer to some of the old
38 | installation routines.
39 |
40 | ## Obtaining a shell
41 |
42 | After running the installer, there will not be a rootshell and ADB will not be
43 | enabled. Instead you can use `./installer util orbic-shell`.
44 |
45 | If you are using an installer prior to 0.7.0 or `orbic-usb` explicitly, you can
46 | obtain a root shell by running `adb shell` or `./installer util shell`. Then,
47 | inside of that shell you can run `/bin/rootshell` to obtain "fakeroot."
48 |
--------------------------------------------------------------------------------
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [alias]
2 | # Build the daemon with "firmware" profile and "ring" TLS backend.
3 | # Requires a cross-compiler (see github actions workflows) and is very slow to build.
4 | build-daemon-firmware = "build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware --no-default-features --features ring-tls"
5 | # Build the daemon with "firmware-devel" profile and "rustcrypto" backend.
6 | # Works with just the Rust toolchain, and is medium-slow to build. Binaries are slightly larger.
7 | build-daemon-firmware-devel = "build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile firmware-devel"
8 |
9 | [target.aarch64-apple-darwin]
10 | linker = "rust-lld"
11 | rustflags = ["-C", "target-feature=+crt-static"]
12 |
13 | [target.aarch64-unknown-linux-musl]
14 | linker = "rust-lld"
15 | rustflags = ["-C", "target-feature=+crt-static"]
16 |
17 | # apt install build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf
18 | [target.armv7-unknown-linux-gnueabihf]
19 | linker = "arm-linux-gnueabihf-gcc"
20 | rustflags = ["-C", "target-feature=+crt-static"]
21 |
22 | [target.armv7-unknown-linux-musleabihf]
23 | linker = "rust-lld"
24 | rustflags = ["-C", "target-feature=+crt-static"]
25 |
26 | [target.armv7-unknown-linux-musleabi]
27 | linker = "rust-lld"
28 | rustflags = ["-C", "target-feature=+crt-static"]
29 |
30 | # Disable rust-lld for x86 macOS because the linker crashers when compiling
31 | # the installer in release mode with debug info on.
32 | # [target.x86_64-apple-darwin]
33 | # linker = "rust-lld"
34 | # rustflags = ["-C", "target-feature=+crt-static"]
35 |
36 | [target.x86_64-unknown-linux-musl]
37 | linker = "rust-lld"
38 | rustflags = ["-C", "target-feature=+crt-static"]
39 |
40 | [profile.release]
41 | # keep line numbers in stack traces for non-firmware binaries
42 | debug = "limited"
43 | lto = "fat"
44 | opt-level = "z"
45 | strip = "debuginfo"
46 |
47 | [profile.firmware-devel]
48 | inherits = "release"
49 | opt-level = "s"
50 | lto = false
51 |
52 | # optimizations to reduce the binary size of firmware binaries
53 | [profile.firmware]
54 | inherits = "release"
55 | strip = true
56 | codegen-units = 1
57 | panic = "abort"
58 | debug = false
59 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/components/AnalysisView.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 | {#if entry.analysis_report === undefined}
20 |
Report unavailable, try refreshing.
21 | {:else if typeof entry.analysis_report === 'string'}
22 |
Error getting analysis report: {entry.analysis_report}
23 | {:else}
24 | {@const metadata: ReportMetadata = entry.analysis_report.metadata}
25 |
26 | {#if !current}
27 |
28 |
29 |
30 | {/if}
31 | {#if entry.analysis_report.rows.length > 0}
32 |
33 | {:else}
34 |
No warnings to display!
35 | {/if}
36 | {#if metadata !== undefined && metadata.rayhunter !== undefined}
37 |
38 |
Metadata
39 |
Analysis by Rayhunter version {metadata.rayhunter.rayhunter_version}
40 |
Device system OS: {metadata.rayhunter.system_os}
41 |
42 |
43 |
Analyzers
44 | {#each metadata.analyzers as analyzer}
45 |
{analyzer.name}: {analyzer.description}
46 | {/each}
47 |
48 | {:else}
49 |
N/A (analysis generated by an older version of rayhunter)
50 | {/if}
51 |
52 | {/if}
53 |
54 |
--------------------------------------------------------------------------------
/doc/pinephone.md:
--------------------------------------------------------------------------------
1 | # PinePhone and PinePhone Pro
2 |
3 | The PinePhone and PinePhone Pro both use a Qualcomm mdm9607 modem as part of their [Quectel EG25-G LTE module](https://www.quectel.com/product/lte-eg25-g/). The EG25-G has global LTE band support and contains a GNSS positioning module. Rayhunter does not currently make direct use of GNSS.
4 |
5 | The modem is fully capable of running Rayhunter, but lacks both a screen and a network connection. The modem exposes an AT interface that can enable adb.
6 |
7 | ## Hardware
8 | -
9 | -
10 |
11 | ## Supported bands
12 |
13 | | Band | Frequency |
14 | | ---- | ----------------- |
15 | | 1 | 2100 MHz (IMT) |
16 | | 2 | 1900 MHz (PCS) |
17 | | 3 | 1800 MHz (DCS) |
18 | | 4 | 1700 MHz (AWS-1) |
19 | | 5 | 850 MHz (CLR) |
20 | | 7 | 2600 MHz (IMT-E) |
21 | | 8 | 900 MHz (E-GSM) |
22 | | 12 | 700 MHz (LSMH) |
23 | | 13 | 700 MHz (USMH) |
24 | | 18 | 850 MHz (LSMH) |
25 | | 19 | 850 MHz (L800) |
26 | | 20 | 800 MHz (DD) |
27 | | 25 | 1900 MHz (E-PCS) |
28 | | 26 | 850 MHz (E-CLR) |
29 | | 28 | 700 MHz (APT) |
30 | | 38 | 2600 MHz (IMT-E) |
31 | | 39 | 850 MHz (E-CLR) |
32 | | 40 | 2300 MHz (S-Band) |
33 | | 41 | 2500 MHz (BRS) |
34 |
35 | Note that the Quectel EG25-G does not support LTE band 48 (CBRS 3500MHz), used in the US for unlicensed 4G/5G connectivity.
36 |
37 | ## Installing
38 | Download and extract the installer *on a shell on the PinePhone itself*. Unlike other Rayhunter installers, this has to be run on the device itself. Then run:
39 |
40 | ```sh
41 | ./installer pinephone
42 | ```
43 |
44 | ## Accessing Rayhunter
45 | Because the modem does not have its own display or network interface, Rayhunter is only accessible on the pinephone by forwarding tcp over adb.
46 |
47 | ```sh
48 | adb forward tcp:8080 tcp:8080
49 | ```
50 |
51 | ## Shell access
52 | Use this command to enable adb access:
53 |
54 | ```sh
55 | ./installer util pinephone-start-adb
56 | adb shell
57 | ```
58 |
59 | ## Power saving (disable adb)
60 | The modem won't be able to sleep (power save) with adb enabled, even if Rayhunter is stopped. Disable adb with the following command:
61 |
62 | ```sh
63 | ./installer util pinephone-stop-adb
64 | ```
65 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/analysis.svelte.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { AnalysisRowType, parse_finished_report } from './analysis.svelte';
3 | import { type NewlineDeliminatedJson } from './ndjson';
4 |
5 | const SAMPLE_V2_REPORT_NDJSON: NewlineDeliminatedJson = [
6 | {
7 | analyzers: [
8 | {
9 | name: 'Analyzer 1',
10 | description: 'A first analyzer',
11 | version: 2,
12 | },
13 | {
14 | name: 'Analyzer 2',
15 | description: 'A second analyzer',
16 | version: 2,
17 | },
18 | ],
19 | report_version: 2,
20 | },
21 | {
22 | skipped_message_reason: 'The reason why the message was skipped',
23 | },
24 | {
25 | packet_timestamp: '2024-08-19T03:33:54.318Z',
26 | events: [
27 | null,
28 | {
29 | event_type: 'Low',
30 | message: 'Something nasty happened',
31 | },
32 | ],
33 | },
34 | ];
35 |
36 | describe('analysis report parsing', () => {
37 | it('parses v2 example analysis', () => {
38 | const report = parse_finished_report(SAMPLE_V2_REPORT_NDJSON);
39 | expect(report.metadata.report_version).toEqual(2);
40 | expect(report.metadata.analyzers).toEqual([
41 | {
42 | name: 'Analyzer 1',
43 | description: 'A first analyzer',
44 | version: 2,
45 | },
46 | {
47 | name: 'Analyzer 2',
48 | description: 'A second analyzer',
49 | version: 2,
50 | },
51 | ]);
52 | expect(report.rows).toHaveLength(2);
53 | expect(report.rows[0].type).toBe(AnalysisRowType.Skipped);
54 | if (report.rows[1].type === AnalysisRowType.Analysis) {
55 | const row = report.rows[1];
56 | expect(row.events).toHaveLength(2);
57 | expect(row.events[0]).toBeNull();
58 | const event = row.events[1];
59 | const expected_timestamp = new Date('2024-08-19T03:33:54.318Z');
60 | expect(row.packet_timestamp.getTime()).toEqual(expected_timestamp.getTime());
61 | expect(event!.event_type).toEqual('Low');
62 | } else {
63 | throw 'wrong row type';
64 | }
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/lib/src/analysis/connection_redirect_downgrade.rs:
--------------------------------------------------------------------------------
1 | use std::borrow::Cow;
2 |
3 | use super::analyzer::{Analyzer, Event, EventType};
4 | use super::information_element::{InformationElement, LteInformationElement};
5 | use telcom_parser::lte_rrc::{
6 | DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions,
7 | RRCConnectionReleaseCriticalExtensions_c1, RedirectedCarrierInfo,
8 | };
9 |
10 | // Based on HITBSecConf presentation "Forcing a targeted LTE cellphone into an
11 | // eavesdropping network" by Lin Huang
12 | pub struct ConnectionRedirect2GDowngradeAnalyzer {}
13 |
14 | // TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones
15 | impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
16 | fn get_name(&self) -> Cow<'_, str> {
17 | Cow::from("Connection Release/Redirected Carrier 2G Downgrade")
18 | }
19 |
20 | fn get_description(&self) -> Cow<'_, str> {
21 | Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.")
22 | }
23 |
24 | fn get_version(&self) -> u32 {
25 | 1
26 | }
27 |
28 | fn analyze_information_element(
29 | &mut self,
30 | ie: &InformationElement,
31 | _packet_num: usize,
32 | ) -> Option {
33 | if let InformationElement::LTE(lte_ie) = ie
34 | && let LteInformationElement::DlDcch(msg_cont) = &**lte_ie
35 | && let DL_DCCH_MessageType::C1(c1) = &msg_cont.message
36 | && let DL_DCCH_MessageType_c1::RrcConnectionRelease(release) = c1
37 | && let RRCConnectionReleaseCriticalExtensions::C1(c1) = &release.critical_extensions
38 | && let RRCConnectionReleaseCriticalExtensions_c1::RrcConnectionRelease_r8(r8_ies) = c1
39 | && let Some(carrier_info) = &r8_ies.redirected_carrier_info
40 | {
41 | match carrier_info {
42 | RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event {
43 | event_type: EventType::High,
44 | message: "Detected 2G downgrade".to_owned(),
45 | }),
46 | _ => Some(Event {
47 | event_type: EventType::Informational,
48 | message: format!("RRCConnectionRelease CarrierInfo: {carrier_info:?}"),
49 | }),
50 | }
51 | } else {
52 | None
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tools/asn1grep.py:
--------------------------------------------------------------------------------
1 | import asn1tools
2 | import sys
3 |
4 | ASN_FILES = [
5 | '../telcom-parser/specs/PC5-RRC-Definitions.asn',
6 | '../telcom-parser/specs/EUTRA-RRC-Definitions.asn',
7 | ]
8 |
9 | TERMINATING_TYPE_NAMES = [
10 | 'DL-CCCH-Message',
11 | 'DL-DCCH-Message',
12 | 'UL-CCCH-Message',
13 | 'UL-DCCH-Message',
14 | 'BCCH-BCH-Message',
15 | 'BCCH-DL-SCH-Message',
16 | 'PCCH-Message',
17 | 'MCCH-Message',
18 | 'SC-MCCH-Message-r13',
19 | 'BCCH-BCH-Message-MBMS',
20 | 'BCCH-DL-SCH-Message-BR',
21 | 'BCCH-DL-SCH-Message-MBMS',
22 | 'SBCCH-SL-BCH-Message',
23 | 'SBCCH-SL-BCH-Message-V2X-r14',
24 | ]
25 |
26 | def load_asn():
27 | return asn1tools.compile_files(ASN_FILES, cache_dir=".cache")
28 |
29 | def get_terminating_types(rrc_asn):
30 | return [rrc_asn.types[name] for name in TERMINATING_TYPE_NAMES]
31 |
32 | def search_type(haystack, needle):
33 | if haystack.type_name == needle or haystack.name == needle:
34 | return [needle]
35 |
36 | result = []
37 | if 'members' in haystack.__dict__:
38 | for name, member in haystack.name_to_member.items():
39 | for member_result in search_type(member, needle):
40 | result.append(f"{haystack.name} ({haystack.type_name}).{name}\n {member_result}")
41 | elif 'root_members' in haystack.__dict__:
42 | for member in haystack.root_members:
43 | for member_result in search_type(member, needle):
44 | result.append(f"{haystack.name} ({haystack.type_name})\n {member_result}")
45 | elif 'element_type' in haystack.__dict__:
46 | for element_result in search_type(haystack.element_type, needle):
47 | result.append(f"{haystack.name}[0] ({haystack.type_name})\n {element_result}")
48 | elif 'inner' in haystack.__dict__:
49 | for inner_result in search_type(haystack.inner, needle):
50 | result.append(inner_result)
51 |
52 | return result
53 |
54 |
55 | if __name__ == "__main__":
56 | type_name = sys.argv[1]
57 | print(f"searching for {type_name}")
58 |
59 | rrc_asn = load_asn()
60 | terminating_types = get_terminating_types(rrc_asn)
61 | needle = rrc_asn.types.get(type_name)
62 | if needle == None:
63 | raise ValueError(f"couldn't find type {type}")
64 |
65 | for haystack in terminating_types:
66 | for result in search_type(haystack.type, type_name):
67 | print(result + '\n')
68 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/utils.svelte.ts:
--------------------------------------------------------------------------------
1 | import { add_error } from './action_errors.svelte';
2 | import { Manifest } from './manifest.svelte';
3 | import type { SystemStats } from './systemStats';
4 |
5 | export interface AnalyzerConfig {
6 | imsi_requested: boolean;
7 | connection_redirect_2g_downgrade: boolean;
8 | lte_sib6_and_7_downgrade: boolean;
9 | null_cipher: boolean;
10 | nas_null_cipher: boolean;
11 | incomplete_sib: boolean;
12 | test_analyzer: boolean;
13 | }
14 |
15 | export enum enabled_notifications {
16 | Warning = 'Warning',
17 | LowBattery = 'LowBattery',
18 | }
19 |
20 | export interface Config {
21 | ui_level: number;
22 | colorblind_mode: boolean;
23 | key_input_mode: number;
24 | ntfy_url: string;
25 | enabled_notifications: enabled_notifications[];
26 | analyzers: AnalyzerConfig;
27 | }
28 |
29 | export async function req(method: string, url: string): Promise {
30 | const response = await fetch(url, {
31 | method: method,
32 | });
33 | const body = await response.text();
34 | if (response.status >= 200 && response.status < 300) {
35 | return body;
36 | } else {
37 | throw new Error(body);
38 | }
39 | }
40 |
41 | // A wrapper around req that reports errors to the UI
42 | export async function user_action_req(
43 | method: string,
44 | url: string,
45 | error_msg: string
46 | ): Promise {
47 | try {
48 | return await req(method, url);
49 | } catch (error) {
50 | if (error instanceof Error) {
51 | console.log('beeeo');
52 | add_error(error, error_msg);
53 | }
54 | return undefined;
55 | }
56 | }
57 |
58 | export async function get_manifest(): Promise {
59 | const manifest_json = JSON.parse(await req('GET', '/api/qmdl-manifest'));
60 | return new Manifest(manifest_json);
61 | }
62 |
63 | export async function get_system_stats(): Promise {
64 | return JSON.parse(await req('GET', '/api/system-stats'));
65 | }
66 |
67 | export async function get_logs(): Promise {
68 | return await req('GET', '/api/log');
69 | }
70 |
71 | export async function get_config(): Promise {
72 | return JSON.parse(await req('GET', '/api/config'));
73 | }
74 |
75 | export async function set_config(config: Config): Promise {
76 | const response = await fetch('/api/config', {
77 | method: 'POST',
78 | headers: {
79 | 'Content-Type': 'application/json',
80 | },
81 | body: JSON.stringify(config),
82 | });
83 |
84 | if (!response.ok) {
85 | const error = await response.text();
86 | throw new Error(error);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/doc/using-rayhunter.md:
--------------------------------------------------------------------------------
1 | # Using Rayhunter
2 |
3 | Once installed, Rayhunter will run automatically whenever your device is running. You'll see a green line on top of the device's display to indicate that it's running and recording. [The line will turn yellow dots, orange dashes, or solid red](./faq.md#red) once a potential IMSI catcher has been found, depending on the severity of the alert, until the device is rebooted or a new recording is started through the web UI.
4 |
5 | 
6 |
7 | It also serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, delete captures, and view heuristic analyses of captures.
8 |
9 | ## The web UI
10 |
11 | You can access this UI in one of two ways:
12 |
13 | * **Connect over WiFi:** Connect your phone/laptop to your device's WiFi
14 | network and visit (orbic)
15 | or (tplink).
16 |
17 | Click past your browser warning you about the connection not being secure; Rayhunter doesn't have HTTPS yet.
18 |
19 | On the **Orbic**, you can find the WiFi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
20 | On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
21 |
22 | * **Connect over USB (Orbic):** Connect your device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit .
23 | * For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the `releases/platform-tools/` folder to somewhere else in your path or you can install it manually.
24 | * You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet).
25 | * On MacOS, the easiest way to install ADB is with Homebrew: First [install Homebrew](https://brew.sh/), then run `brew install android-platform-tools`.
26 |
27 | * **Connect over USB (TP-Link):** Plug in the TP-Link and use USB tethering to establish a network connection. ADB support can be enabled on the device, but the installer won't do it for you.
28 |
29 | ## Key shortcuts
30 |
31 | As of Rayhunter version 0.3.3, you can start a new recording by double-tapping the power button. Any current recording will be stopped and a new recording will be started, resetting the red line as well. This feature is disabled by default since Rayhunter version 0.4.0 and needs to be enabled through [configuration](./configuration.md).
32 |
--------------------------------------------------------------------------------
/installer/src/orbic_auth.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{Context, Result};
2 | use base64_light::base64_encode;
3 | use serde::{Deserialize, Serialize};
4 |
5 | /// Helper function to swap characters in a string
6 | fn swap_chars(s: &str, pos1: usize, pos2: usize) -> String {
7 | let mut chars: Vec = s.chars().collect();
8 | if pos1 < chars.len() && pos2 < chars.len() {
9 | chars.swap(pos1, pos2);
10 | }
11 | chars.into_iter().collect()
12 | }
13 |
14 | /// Apply character swapping based on secret (unchanged from original algorithm)
15 | fn apply_secret_swapping(mut text: String, secret_num: u32) -> String {
16 | for i in 0..4 {
17 | let byte = (secret_num >> (i * 8)) & 0xff;
18 | let pos1 = (byte as usize) % text.len();
19 | let pos2 = i % text.len();
20 | text = swap_chars(&text, pos1, pos2);
21 | }
22 | text
23 | }
24 |
25 | /// Encode password using Orbic's custom algorithm
26 | ///
27 | /// This function is a lot simpler than the original JavaScript because it always uses the same
28 | /// character set regardless of "password type", and any randomly generated values are hardcoded.
29 | pub fn encode_password(
30 | password: &str,
31 | secret: &str,
32 | timestamp: &str,
33 | timestamp_start: u64,
34 | ) -> Result {
35 | let current_time = std::time::SystemTime::now()
36 | .duration_since(std::time::UNIX_EPOCH)
37 | .unwrap()
38 | .as_secs();
39 |
40 | // MD5 hash the password and use fixed prefix "a7" instead of random chars
41 | let password_md5 = format!("{:x}", md5::compute(password));
42 | let mut spliced_password = format!("a7{}", password_md5);
43 |
44 | let secret_num = u32::from_str_radix(secret, 16).context("Failed to parse secret as hex")?;
45 |
46 | spliced_password = apply_secret_swapping(spliced_password, secret_num);
47 |
48 | let timestamp_hex =
49 | u32::from_str_radix(timestamp, 16).context("Failed to parse timestamp as hex")?;
50 | let time_delta = format!(
51 | "{:x}",
52 | timestamp_hex + (current_time - timestamp_start) as u32
53 | );
54 |
55 | // Use fixed hex "6137" instead of hex encoding of random values
56 | let message = format!("6137x{}:{}", time_delta, spliced_password);
57 |
58 | let result = base64_encode(&message);
59 | let result = apply_secret_swapping(result, secret_num);
60 |
61 | Ok(result)
62 | }
63 |
64 | #[derive(Debug, Serialize)]
65 | pub struct LoginRequest {
66 | pub username: String,
67 | pub password: String,
68 | }
69 |
70 | #[derive(Debug, Deserialize)]
71 | pub struct LoginInfo {
72 | pub retcode: u32,
73 | #[serde(rename = "priKey")]
74 | pub pri_key: String,
75 | }
76 |
77 | #[derive(Debug, Deserialize)]
78 | pub struct LoginResponse {
79 | pub retcode: u32,
80 | }
81 |
--------------------------------------------------------------------------------
/lib/src/analysis/test_analyzer.rs:
--------------------------------------------------------------------------------
1 | use std::borrow::Cow;
2 |
3 | use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1};
4 |
5 | use super::analyzer::{Analyzer, Event, EventType};
6 | use super::information_element::{InformationElement, LteInformationElement};
7 | use deku::bitvec::*;
8 |
9 | pub struct TestAnalyzer {}
10 |
11 | impl Analyzer for TestAnalyzer {
12 | fn get_name(&self) -> Cow<'_, str> {
13 | Cow::from("Test Analyzer")
14 | }
15 |
16 | fn get_description(&self) -> Cow<'_, str> {
17 | Cow::from(
18 | "This is an analyzer which can be used to test that your rayhunter is working. It will generate an alert for every SIB1 message (a beacon from the cell tower) that it sees. Do not leave this on when you are hunting or it will be very noisy.",
19 | )
20 | }
21 |
22 | fn get_version(&self) -> u32 {
23 | 1
24 | }
25 |
26 | fn analyze_information_element(
27 | &mut self,
28 | ie: &InformationElement,
29 | _packet_num: usize,
30 | ) -> Option {
31 | if let InformationElement::LTE(lte_ie) = ie
32 | && let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie
33 | && let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message
34 | && let BCCH_DL_SCH_MessageType_c1::SystemInformationBlockType1(sib1) = c1
35 | {
36 | let cid = sib1
37 | .cell_access_related_info
38 | .cell_identity
39 | .0
40 | .as_bitslice()
41 | .load_be::();
42 | let plmn = &sib1.cell_access_related_info.plmn_identity_list.0;
43 | let mcc_string: String;
44 |
45 | // MCC are always 3 digits
46 | if let Some(mcc) = &plmn[0].plmn_identity.mcc {
47 | mcc_string = format!("{}{}{}", mcc.0[0].0, mcc.0[1].0, mcc.0[2].0);
48 | } else {
49 | mcc_string = "nomcc".to_string();
50 | }
51 | let mnc = &plmn[0].plmn_identity.mnc;
52 | let mnc_string: String;
53 | // MNC can be 2 or 3 digits
54 | if mnc.0.len() == 3 {
55 | mnc_string = format!("{}{}{}", mnc.0[0].0, mnc.0[1].0, mnc.0[2].0);
56 | } else if mnc.0.len() == 2 {
57 | mnc_string = format!("{}{}", mnc.0[0].0, mnc.0[1].0);
58 | } else {
59 | mnc_string = format!("{:?}", mnc.0);
60 | }
61 |
62 | return Some(Event {
63 | event_type: EventType::Low,
64 | message: format!(
65 | "SIB1 received CID: {}, PLMN: {}-{}",
66 | cid, mcc_string, mnc_string
67 | ),
68 | });
69 | }
70 | None
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/dist/scripts/misc-daemon:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | case "$1" in
6 | start)
7 | echo -n "Starting miscellaneous daemons: "
8 | search_dir="/sys/bus/msm_subsys/devices/"
9 | for entry in `ls $search_dir`
10 | do
11 | subsys_temp=`cat $search_dir/$entry/name`
12 | if [ "$subsys_temp" == "modem" ]
13 | then
14 | break
15 | fi
16 | done
17 | counter=0
18 | while [ ${counter} -le 10 ]
19 | do
20 | msstate=`cat $search_dir/$entry/state`
21 | if [ "$msstate" == "ONLINE" ]
22 | then
23 | break
24 | fi
25 | counter=$(( $counter + 1 ))
26 | sleep 1
27 | done
28 |
29 | if [ -f /etc/init.d/init_qcom_audio ]
30 | then
31 | /etc/init.d/init_qcom_audio start
32 | fi
33 |
34 | if [ -f /sbin/reboot-daemon ]
35 | then
36 | /sbin/reboot-daemon &
37 | fi
38 |
39 | if [ -f /etc/init.d/start_atfwd_daemon ]
40 | then
41 | /etc/init.d/start_atfwd_daemon start
42 | fi
43 |
44 | if [ -f /etc/init.d/rayhunter_daemon ]
45 | then
46 | /etc/init.d/rayhunter_daemon start
47 | fi
48 |
49 | if [ -f /etc/init.d/start_stop_qti_ppp_le ]
50 | then
51 | /etc/init.d/start_stop_qti_ppp_le start
52 | fi
53 |
54 | if [ -f /etc/init.d/start_loc_launcher ]
55 | then
56 | /etc/init.d/start_loc_launcher start
57 | fi
58 |
59 | echo -n "Completed starting miscellaneous daemons"
60 | ;;
61 | stop)
62 | echo -n "Stopping miscellaneous daemons: "
63 |
64 |
65 | if [ -f /etc/init.d/start_atfwd_daemon ]
66 | then
67 | /etc/init.d/start_atfwd_daemon stop
68 | fi
69 |
70 | if [ -f /etc/init.d/start_loc_launcher ]
71 | then
72 | /etc/init.d/start_loc_launcher stop
73 | fi
74 |
75 | if [ -f /etc/init.d/rayhunter_daemon ]
76 | then
77 | /etc/init.d/rayhunter_daemon stop
78 | fi
79 |
80 | if [ -f /etc/init.d/init_qcom_audio ]
81 | then
82 | /etc/init.d/init_qcom_audio stop
83 | fi
84 |
85 | if [ -f /etc/init.d/start_stop_qti_ppp_le ]
86 | then
87 | /etc/init.d/start_stop_qti_ppp_le stop
88 | fi
89 |
90 | echo -n "Completed stopping miscellaneous daemons"
91 | ;;
92 | restart)
93 | $0 stop
94 | $0 start
95 | ;;
96 | *)
97 | echo "Usage misc-daemon { start | stop | restart}" >&2
98 | exit 1
99 | ;;
100 | esac
101 |
102 | exit 0
103 |
--------------------------------------------------------------------------------
/doc/tmobile-tmohs1.md:
--------------------------------------------------------------------------------
1 | # Tmobile TMOHS1
2 |
3 | The Tmobile TMOHS1 hotspot is a Qualcomm mdm9607-based device with many similarities to the Wingtech CT2MHS01 hotspot. The TMOHS1 has no screen, only 5 LEDs, two of which are RGB.
4 |
5 | ## Hardware
6 | Cheap used versions of the device can be found easily on Ebay, and also from these sellers:
7 | -
8 | -
9 | -
10 |
11 | Rayhunter has been tested on:
12 |
13 | ```sh
14 | WT_INNER_VERSION=SW_Q89527AA1_V045_M11_TMO_USR_MP
15 | WT_PRODUCTION_VERSION=TMOHS1_00.05.20
16 | WT_HARDWARE_VERSION=89527_1_11
17 | ```
18 |
19 | Please consider sharing the contents of your device's /etc/wt_version file here.
20 |
21 | ## Supported bands
22 |
23 | The TMOHS1 is primarily an ITU Region 2 device, although Bands 5 (CLR) and 41 (BRS) may be suitable for roaming in Region 3.
24 |
25 | According to FCC ID 2APXW-TMOHS1 Test Report No. I20Z61602-WMD02 ([part 1](https://fcc.report/FCC-ID/2APXW-TMOHS1/4987033.pdf), [part 2](https://fcc.report/FCC-ID/2APXW-TMOHS1/4987034.pdf)), the TMOHS1 supports the following LTE bands:
26 |
27 | | Band | Frequency |
28 | | ---- | ---------------- |
29 | | 2 | 1900 MHz (PCS) |
30 | | 4 | 1700 MHz (AWS-1) |
31 | | 5 | 850 MHz (CLR) |
32 | | 12 | 700 MHz (LSMH) |
33 | | 25 | 1900 MHz (E-PCS) |
34 | | 26 | 850 MHz (E-CLR) |
35 | | 41 | 2500 MHz (BRS) |
36 | | 66 | 1700 MHz (E-AWS) |
37 | | 71 | 600 MHz (USDD) |
38 |
39 | ## Installing
40 | Connect to the TMOHS1's network over WiFi or USB tethering.
41 |
42 | The device will not accept web requests until after the default password is changed.
43 | If you have not previously logged in, log in using the default password printed under the battery and change the admin password.
44 |
45 | Then run the installer:
46 |
47 | ```sh
48 | ./installer tmobile --admin-password Admin0123! # replace with your own password
49 | ```
50 |
51 | ## LED modes
52 | | Rayhunter state | LED indicator |
53 | | ---------------- | ------------------------------ |
54 | | Recording | Signal LED slowly blinks blue. |
55 | | Paused | WiFi LED blinks white. |
56 | | Warning Detected | Signal LED slowly blinks red. |
57 |
58 | ## Obtaining a shell
59 | Even when rayhunter is running, for security reasons the TMOHS1 will not have telnet or adb enabled during normal operation.
60 |
61 | Use either command below to enable telnet or adb access:
62 |
63 | ```sh
64 | ./installer util tmobile-start-telnet --admin-password Admin0123!
65 | telnet 192.168.0.1
66 | ```
67 |
68 | ```sh
69 | ./installer util tmobile-start-adb --admin-password Admin0123!
70 | adb shell
71 | ```
72 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/components/ManifestTableRow.svelte:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 | {entry.name}
41 | {date_formatter.format(entry.start_time)}
42 | {(entry.last_message_time && date_formatter.format(entry.last_message_time)) || 'N/A'}
45 | {entry.get_readable_qmdl_size()}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
56 | {#if current}
57 |
58 | {:else}
59 |
60 |
65 |
66 | {/if}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/daemon/src/display/tplink_framebuffer.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use std::os::fd::AsRawFd;
3 | use tokio::fs::OpenOptions;
4 | use tokio::io::AsyncWriteExt;
5 | use tokio_util::sync::CancellationToken;
6 |
7 | use crate::config;
8 | use crate::display::DisplayState;
9 | use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
10 |
11 | use tokio::sync::mpsc::Receiver;
12 | use tokio_util::task::TaskTracker;
13 |
14 | const FB_PATH: &str = "/dev/fb0";
15 |
16 | struct Framebuffer;
17 |
18 | #[repr(C)]
19 | struct fb_fillrect {
20 | dx: u32,
21 | dy: u32,
22 | width: u32,
23 | height: u32,
24 | color: u32,
25 | rop: u32,
26 | }
27 |
28 | #[async_trait]
29 | impl GenericFramebuffer for Framebuffer {
30 | fn dimensions(&self) -> Dimensions {
31 | // TODO actually poll for this, maybe w/ fbset?
32 | Dimensions {
33 | height: 128,
34 | width: 128,
35 | }
36 | }
37 |
38 | async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
39 | // for how to write to the buffer, consult M7350v5_en_gpl/bootable/recovery/recovery_color_oled.c
40 | let dimensions = self.dimensions();
41 | let width = dimensions.width;
42 | let height = buffer.len() as u32 / width;
43 | let mut f = OpenOptions::new().write(true).open(FB_PATH).await.unwrap();
44 | let mut arg = fb_fillrect {
45 | dx: 0,
46 | dy: 0,
47 | width,
48 | height,
49 | color: 0xffff, // not sure what this is
50 | rop: 0,
51 | };
52 |
53 | let mut raw_buffer = Vec::new();
54 | for (r, g, b) in buffer {
55 | let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
56 | rgb565 |= (g as u16 & 0b11111100) << 3;
57 | rgb565 |= (b as u16) >> 3;
58 | // note: big-endian!
59 | raw_buffer.extend(rgb565.to_be_bytes());
60 | }
61 |
62 | f.write_all(&raw_buffer).await.unwrap();
63 |
64 | // ioctl is a synchronous operation, but it's fast enough that it shouldn't block
65 | unsafe {
66 | let res = libc::ioctl(
67 | f.as_raw_fd(),
68 | 0x4619, // FBIORECT_DISPLAY
69 | &mut arg as *mut _,
70 | std::mem::size_of::(),
71 | );
72 |
73 | if res < 0 {
74 | panic!("failed to send FBIORECT_DISPLAY ioctl, {res}");
75 | }
76 | }
77 | }
78 | }
79 |
80 | pub fn update_ui(
81 | task_tracker: &TaskTracker,
82 | config: &config::Config,
83 | shutdown_token: CancellationToken,
84 | ui_update_rx: Receiver,
85 | ) {
86 | generic_framebuffer::update_ui(
87 | task_tracker,
88 | config,
89 | Framebuffer,
90 | shutdown_token,
91 | ui_update_rx,
92 | )
93 | }
94 |
--------------------------------------------------------------------------------
/daemon/src/display/tmobile.rs:
--------------------------------------------------------------------------------
1 | /// Display module for Tmobile TMOHS1, blink LEDs on the front of the device.
2 | /// DisplayState::Recording => Signal LED slowly blinks blue.
3 | /// DisplayState::Paused => WiFi LED blinks white.
4 | /// DisplayState::WarningDetected { .. } => Signal LED slowly blinks red.
5 | use log::{error, info};
6 | use tokio::sync::mpsc;
7 | use tokio_util::sync::CancellationToken;
8 | use tokio_util::task::TaskTracker;
9 |
10 | use std::time::Duration;
11 |
12 | use crate::config;
13 | use crate::display::DisplayState;
14 |
15 | macro_rules! led {
16 | ($l:expr) => {{ format!("/sys/class/leds/led:{}/blink", $l) }};
17 | }
18 |
19 | async fn start_blinking(path: String) {
20 | tokio::fs::write(&path, "1").await.ok();
21 | }
22 |
23 | async fn stop_blinking(path: String) {
24 | tokio::fs::write(&path, "0").await.ok();
25 | }
26 |
27 | pub fn update_ui(
28 | task_tracker: &TaskTracker,
29 | config: &config::Config,
30 | shutdown_token: CancellationToken,
31 | mut ui_update_rx: mpsc::Receiver,
32 | ) {
33 | let mut invisible: bool = false;
34 | if config.ui_level == 0 {
35 | info!("Invisible mode, not spawning UI.");
36 | invisible = true;
37 | }
38 | task_tracker.spawn(async move {
39 | let mut state = DisplayState::Recording;
40 | let mut last_state = DisplayState::Paused;
41 |
42 | loop {
43 | if shutdown_token.is_cancelled() {
44 | info!("received UI shutdown");
45 | break;
46 | }
47 | match ui_update_rx.try_recv() {
48 | Ok(new_state) => state = new_state,
49 | Err(mpsc::error::TryRecvError::Empty) => {}
50 | Err(e) => error!("error receiving ui update message: {e}"),
51 | };
52 | if invisible || state == last_state {
53 | tokio::time::sleep(Duration::from_secs(1)).await;
54 | continue;
55 | }
56 | match state {
57 | DisplayState::Paused => {
58 | stop_blinking(led!("signal_blue")).await;
59 | stop_blinking(led!("signal_red")).await;
60 | start_blinking(led!("wlan_white")).await;
61 | }
62 | DisplayState::Recording => {
63 | stop_blinking(led!("wlan_white")).await;
64 | stop_blinking(led!("signal_red")).await;
65 | start_blinking(led!("signal_blue")).await;
66 | }
67 | DisplayState::WarningDetected { .. } => {
68 | stop_blinking(led!("wlan_white")).await;
69 | stop_blinking(led!("signal_blue")).await;
70 | start_blinking(led!("signal_red")).await;
71 | }
72 | }
73 | last_state = state;
74 | tokio::time::sleep(Duration::from_secs(1)).await;
75 | }
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/daemon/src/display/uz801.rs:
--------------------------------------------------------------------------------
1 | /// Display module for Uz801, light LEDs on the front of the device.
2 | /// DisplayState::Recording => Green LED is solid.
3 | /// DisplayState::Paused => Signal LED is solid blue (wifi LED).
4 | /// DisplayState::WarningDetected => Signal LED is solid red.
5 | use log::{error, info};
6 | use tokio::sync::mpsc;
7 | use tokio_util::sync::CancellationToken;
8 | use tokio_util::task::TaskTracker;
9 |
10 | use std::time::Duration;
11 |
12 | use crate::config;
13 | use crate::display::DisplayState;
14 |
15 | macro_rules! led {
16 | ($l:expr) => {{ format!("/sys/class/leds/{}/brightness", $l) }};
17 | }
18 |
19 | async fn led_on(path: String) {
20 | tokio::fs::write(&path, "1").await.ok();
21 | }
22 |
23 | async fn led_off(path: String) {
24 | tokio::fs::write(&path, "0").await.ok();
25 | }
26 |
27 | pub fn update_ui(
28 | task_tracker: &TaskTracker,
29 | config: &config::Config,
30 | shutdown_token: CancellationToken,
31 | mut ui_update_rx: mpsc::Receiver,
32 | ) {
33 | let mut invisible: bool = false;
34 | if config.ui_level == 0 {
35 | info!("Invisible mode, not spawning UI.");
36 | invisible = true;
37 | }
38 | task_tracker.spawn(async move {
39 | let mut state = DisplayState::Recording;
40 | let mut last_state = DisplayState::Paused;
41 | let mut last_update = std::time::Instant::now();
42 |
43 | loop {
44 | if shutdown_token.is_cancelled() {
45 | info!("received UI shutdown");
46 | break;
47 | }
48 | match ui_update_rx.try_recv() {
49 | Ok(new_state) => state = new_state,
50 | Err(mpsc::error::TryRecvError::Empty) => {}
51 | Err(e) => error!("error receiving ui update message: {e}"),
52 | };
53 |
54 | // Update LEDs if state changed or if 5 seconds have passed since last update
55 | let now = std::time::Instant::now();
56 | let should_update = !invisible
57 | && (state != last_state
58 | || now.duration_since(last_update) >= Duration::from_secs(5));
59 |
60 | if should_update {
61 | match state {
62 | DisplayState::Paused => {
63 | led_off(led!("red")).await;
64 | led_off(led!("green")).await;
65 | led_on(led!("wifi")).await;
66 | }
67 | DisplayState::Recording => {
68 | led_off(led!("red")).await;
69 | led_off(led!("wifi")).await;
70 | led_on(led!("green")).await;
71 | }
72 | DisplayState::WarningDetected { .. } => {
73 | led_off(led!("green")).await;
74 | led_off(led!("wifi")).await;
75 | led_on(led!("red")).await;
76 | }
77 | }
78 | last_state = state;
79 | last_update = now;
80 | }
81 |
82 | tokio::time::sleep(Duration::from_secs(1)).await;
83 | }
84 | });
85 | }
86 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/components/ApiRequestButton.svelte:
--------------------------------------------------------------------------------
1 |
71 |
72 |
78 | {is_requesting && loadingLabel ? loadingLabel : label}
79 | {#if is_requesting}
80 |
86 |
88 |
93 |
94 | {:else if icon}
95 | {@render icon()}
96 | {/if}
97 |
98 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/components/LogView.svelte:
--------------------------------------------------------------------------------
1 |
44 |
45 | {#if shown}
46 |
50 |
51 |
Log
52 |
(shown = false)} aria-label="close">
53 |
61 |
67 |
68 |
69 |
70 |
73 |
74 | {/if}
75 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute to Rayhunter
2 |
3 | ## Filing issues and starting discussions
4 |
5 | Our issue tracker is [on GitHub](https://github.com/EFForg/rayhunter/issues).
6 |
7 | - If your rayhunter has found an IMSI-catcher, we strongly encourage you to
8 | [send us that information
9 | privately.](https://efforg.github.io/rayhunter/faq.html#help-rayhunters-line-is-redorangeyellowdotteddashed-what-should-i-do) via Signal.
10 |
11 | - Issues should be actionable. If you don't have a
12 | specific feature request or bug report, consider [creating a
13 | discussion](https://github.com/EFForg/rayhunter/discussions) or [joining our Mattermost](https://efforg.github.io/rayhunter/support-feedback-community.html) instead.
14 |
15 | Example of a good bug report:
16 |
17 | - "Installer broken on TP-Link M7350 v3.0"
18 | - "Display does not update to green after finding"
19 | - "The documentation is wrong" (though we encourage you to file a pull request directly)
20 |
21 | Example of a good feature request:
22 |
23 | - "Use LED on device XYZ for showing recording status"
24 |
25 | Example of something that belongs into discussion:
26 |
27 | - "In region XYZ, do I need an activated SIM?"
28 | - "Where to buy this device in region XYZ?"
29 | - "Can this device be supported?" While this is a valid feature
30 | request, we just get this request too often, and without some exploratory
31 | work done upfront it's often unclear initially if that device can be
32 | supported at all.
33 |
34 | - The issue templates are mostly there to give you a clue what kind of
35 | information is needed from you, and whether your request belongs into the issue
36 | tracker. Fill them out to be on the safe side, but they are not mandatory.
37 |
38 | ## Contributing patches
39 |
40 | To edit documentation or fix a bug, make a pull request. If you're about to
41 | write a substantial amount of code or implement a new feature, we strongly
42 | encourage you to talk to us before implementing it or check if any issues have
43 | been opened for it already. Otherwise there is a chance we will reject your
44 | contribution after you have spent time on it.
45 |
46 | On the other hand, for small documentation fixes you can file a PR without
47 | filing an issue.
48 |
49 | Otherwise:
50 |
51 | - Refer to [installing from
52 | source](https://efforg.github.io/rayhunter/installing-from-source.html) for
53 | how to build Rayhunter from the git repository.
54 |
55 | - Ensure that `cargo fmt` and `cargo clippy` have been run.
56 |
57 | - If you add new features, please do your best to both write tests for and also
58 | manually test them. Our test coverage isn't great, but as new features are
59 | added we are trying to prevent it from becoming worse.
60 |
61 | If you have any questions [feel free to open a discussion or chat with us on Mattermost.](https://efforg.github.io/rayhunter/support-feedback-community.html)
62 |
63 | ## Making releases
64 |
65 | This one is for maintainers of Rayhunter.
66 |
67 | 1. Make a PR changing the versions in `Cargo.toml` and other files.
68 | This could be automated better but right now it's manual. You can do this easily with sed:
69 | `sed -i "" -E 's/x.x.x/y.y.y/g' */Cargo.toml`
70 |
71 | 2. Merge PR and make a tag.
72 |
73 | 3. [Run release workflow.](https://github.com/EFForg/rayhunter/actions/workflows/release.yml)
74 |
75 | 4. Write changelog, edit it into the release, announce on mattermost.
76 |
--------------------------------------------------------------------------------
/doc/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | Rayhunter can be configured through web user interface or by editing `/data/rayhunter/config.toml` on the device.
4 |
5 | 
6 |
7 | Through web UI you can set:
8 | - **Device UI Level**, which defines what Rayhunter shows on device's built-in screen. *Device UI Level* could be:
9 | - *Invisible mode*: Rayhunter does not show anything on the built-in screen
10 | - *Subtle mode (colored line)*: Rayhunter shows green line if there are no warnings, red line if there are warnings (warnings could be checked through web UI) and white line if Rayhunter is not recording.
11 | - *Demo mode (orca gif)*, which shows image of orcas *and* colored line.
12 | - *EFF logo*, which shows EFF logo and *and* colored line.
13 | - **Device Input Mode**, which defines behavior of built-in power button of the device. *Device Input Mode* could be:
14 | - *Disable button control*: built-in power button of the device is not used by Rayhunter.
15 | - *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediately restarts the recording. This could be useful if Rayhunter's heuristics is triggered and you get the red line, and you want to "reset" the past warnings. Normally you can do that through web UI, but sometimes it is easier to double tap on power button.
16 | - **Colorblind Mode** enables color blind mode (blue line is shown instead of green line, red line remains red). Please note that this does not cover all types of color blindness, but switching green to blue should be about enough to differentiate the color change for most types of color blindness.
17 | - **ntfy URL**, which allows setting a [ntfy](https://ntfy.sh/) URL to which notifications of new detections will be sent. The topic should be unique to your device, e.g., `https://ntfy.sh/rayhunter_notifications_ba9di7ie` or `https://myserver.example.com/rayhunter_notifications_ba9di7ie`. The ntfy Android and iOS apps can then be used to receive notifications. More information can be found in the [ntfy docs](https://docs.ntfy.sh/).
18 | - **Enabled Notification Types** allows enabling or disabling the following types of notifications:
19 | - *Warnings*, which will alert when a heuristic is triggered. Alerts will be sent at most once every five minutes.
20 | - *Low Battery*, which will alert when the device's battery is low. Notifications may not be supported for all devices—you can check if your device is supported by looking at whether the battery level indicator is functioning on the System Information section of the Rayhunter UI.
21 | - With **Analyzer Heuristic Settings** you can switch on or off built-in [Rayhunter heuristics](heuristics.md). Some heuristics are experimental or can trigger a lot of false positive warnings in some networks (our tests have shown that some heuristics have different behavior in US or European networks). In that case you can decide whether you would like to have the heuristics that trigger a lot of false positives on or off. Please note that we are constantly improving and adding new heuristics, so a new release may reduce false positives in existing heuristics as well.
22 |
23 | 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).
24 |
--------------------------------------------------------------------------------
/installer/src/tmobile.rs:
--------------------------------------------------------------------------------
1 | /// Installer for the TMobile TMOHS1 hotspot.
2 | ///
3 | /// Tested on (from `/etc/wt_version`):
4 | /// WT_INNER_VERSION=SW_Q89527AA1_V045_M11_TMO_USR_MP
5 | /// WT_PRODUCTION_VERSION=TMOHS1_00.05.20
6 | /// WT_HARDWARE_VERSION=89527_1_11
7 | use std::net::SocketAddr;
8 | use std::str::FromStr;
9 | use std::time::Duration;
10 |
11 | use anyhow::Result;
12 | use tokio::time::sleep;
13 |
14 | use crate::TmobileArgs as Args;
15 | use crate::output::{print, println};
16 | use crate::util::{http_ok_every, telnet_send_command, telnet_send_file};
17 | use crate::wingtech::start_telnet;
18 |
19 | pub async fn install(
20 | Args {
21 | admin_ip,
22 | admin_password,
23 | }: Args,
24 | ) -> Result<()> {
25 | run_install(admin_ip, admin_password).await
26 | }
27 |
28 | async fn run_install(admin_ip: String, admin_password: String) -> Result<()> {
29 | print!("Starting telnet ... ");
30 | start_telnet(&admin_ip, &admin_password).await?;
31 | sleep(Duration::from_millis(200)).await;
32 | println!("ok");
33 |
34 | print!("Connecting via telnet to {admin_ip} ... ");
35 | let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
36 | telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0", true).await?;
37 | println!("ok");
38 |
39 | telnet_send_command(addr, "mount -o remount,rw /", "exit code 0", true).await?;
40 |
41 | telnet_send_file(
42 | addr,
43 | "/data/rayhunter/config.toml",
44 | crate::CONFIG_TOML
45 | .replace("#device = \"orbic\"", "device = \"tmobile\"")
46 | .as_bytes(),
47 | true,
48 | )
49 | .await?;
50 |
51 | let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
52 | telnet_send_file(
53 | addr,
54 | "/data/rayhunter/rayhunter-daemon",
55 | rayhunter_daemon_bin,
56 | true,
57 | )
58 | .await?;
59 | telnet_send_command(
60 | addr,
61 | "chmod 755 /data/rayhunter/rayhunter-daemon",
62 | "exit code 0",
63 | true,
64 | )
65 | .await?;
66 | telnet_send_file(
67 | addr,
68 | "/etc/init.d/misc-daemon",
69 | include_bytes!("../../dist/scripts/misc-daemon"),
70 | true,
71 | )
72 | .await?;
73 | telnet_send_command(
74 | addr,
75 | "chmod 755 /etc/init.d/misc-daemon",
76 | "exit code 0",
77 | true,
78 | )
79 | .await?;
80 | telnet_send_file(
81 | addr,
82 | "/etc/init.d/rayhunter_daemon",
83 | crate::RAYHUNTER_DAEMON_INIT.as_bytes(),
84 | true,
85 | )
86 | .await?;
87 | telnet_send_command(
88 | addr,
89 | "chmod 755 /etc/init.d/rayhunter_daemon",
90 | "exit code 0",
91 | true,
92 | )
93 | .await?;
94 |
95 | println!("Rebooting device and waiting 30 seconds for it to start up.");
96 | telnet_send_command(addr, "reboot", "exit code 0", true).await?;
97 | sleep(Duration::from_secs(30)).await;
98 |
99 | print!("Testing rayhunter ... ");
100 | let max_failures = 10;
101 | http_ok_every(
102 | format!("http://{admin_ip}:8080/index.html"),
103 | Duration::from_secs(3),
104 | max_failures,
105 | )
106 | .await?;
107 | println!("ok");
108 | println!("rayhunter is running at http://{admin_ip}:8080");
109 |
110 | Ok(())
111 | }
112 |
--------------------------------------------------------------------------------
/daemon/src/pcap.rs:
--------------------------------------------------------------------------------
1 | use crate::ServerState;
2 |
3 | use anyhow::Error;
4 | use axum::body::Body;
5 | use axum::extract::{Path, State};
6 | use axum::http::StatusCode;
7 | use axum::http::header::CONTENT_TYPE;
8 | use axum::response::{IntoResponse, Response};
9 | use log::error;
10 | use rayhunter::diag::DataType;
11 | use rayhunter::gsmtap_parser;
12 | use rayhunter::pcap::GsmtapPcapWriter;
13 | use rayhunter::qmdl::QmdlReader;
14 | use std::sync::Arc;
15 | use tokio::io::{AsyncRead, AsyncWrite, duplex};
16 | use tokio_util::io::ReaderStream;
17 |
18 | // Streams a pcap file chunk-by-chunk to the client by reading the QMDL data
19 | // written so far. This is done by spawning a thread which streams chunks of
20 | // pcap data to a channel that's piped to the client.
21 | pub async fn get_pcap(
22 | State(state): State>,
23 | Path(mut qmdl_name): Path,
24 | ) -> Result {
25 | let qmdl_store = state.qmdl_store_lock.read().await;
26 | if qmdl_name.ends_with("pcapng") {
27 | qmdl_name = qmdl_name.trim_end_matches(".pcapng").to_string();
28 | }
29 | let (entry_index, entry) = qmdl_store.entry_for_name(&qmdl_name).ok_or((
30 | StatusCode::NOT_FOUND,
31 | format!("couldn't find manifest entry with name {qmdl_name}"),
32 | ))?;
33 | if entry.qmdl_size_bytes == 0 {
34 | return Err((
35 | StatusCode::SERVICE_UNAVAILABLE,
36 | "QMDL file is empty, try again in a bit!".to_string(),
37 | ));
38 | }
39 | let qmdl_size_bytes = entry.qmdl_size_bytes;
40 | let qmdl_file = qmdl_store
41 | .open_entry_qmdl(entry_index)
42 | .await
43 | .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
44 | // the QMDL reader should stop at the last successfully written data chunk
45 | // (entry.size_bytes)
46 | let (reader, writer) = duplex(1024);
47 |
48 | tokio::spawn(async move {
49 | if let Err(e) = generate_pcap_data(writer, qmdl_file, qmdl_size_bytes).await {
50 | error!("failed to generate PCAP: {e:?}");
51 | }
52 | });
53 |
54 | let headers = [(CONTENT_TYPE, "application/vnd.tcpdump.pcap")];
55 | let body = Body::from_stream(ReaderStream::new(reader));
56 | Ok((headers, body).into_response())
57 | }
58 |
59 | pub async fn generate_pcap_data(
60 | writer: W,
61 | qmdl_file: R,
62 | qmdl_size_bytes: usize,
63 | ) -> Result<(), Error>
64 | where
65 | W: AsyncWrite + Unpin + Send,
66 | R: AsyncRead + Unpin,
67 | {
68 | let mut pcap_writer = GsmtapPcapWriter::new(writer).await?;
69 | pcap_writer.write_iface_header().await?;
70 |
71 | let mut reader = QmdlReader::new(qmdl_file, Some(qmdl_size_bytes));
72 | while let Some(container) = reader.get_next_messages_container().await? {
73 | if container.data_type != DataType::UserSpace {
74 | continue;
75 | }
76 |
77 | for maybe_msg in container.into_messages() {
78 | match maybe_msg {
79 | Ok(msg) => {
80 | let maybe_gsmtap_msg = gsmtap_parser::parse(msg)?;
81 | if let Some((timestamp, gsmtap_msg)) = maybe_gsmtap_msg {
82 | pcap_writer
83 | .write_gsmtap_message(gsmtap_msg, timestamp)
84 | .await?;
85 | }
86 | }
87 | Err(e) => error!("error parsing message: {e:?}"),
88 | }
89 | }
90 | }
91 |
92 | Ok(())
93 | }
94 |
--------------------------------------------------------------------------------
/installer/src/output.rs:
--------------------------------------------------------------------------------
1 | //! Output handling for the installer
2 | //!
3 | //! This module provides custom print macros that can be intercepted by setting
4 | //! a callback function. This is essential for FFI usage where stdout/stderr
5 | //! redirection doesn't work reliably (especially on Android).
6 |
7 | use std::io::Write;
8 | use std::sync::Mutex;
9 |
10 | /// Type for the output callback function
11 | type OutputCallbackFn = Box;
12 |
13 | /// Global output callback storage
14 | static OUTPUT_CALLBACK: Mutex> = Mutex::new(None);
15 |
16 | /// Set the global output callback
17 | ///
18 | /// All output from `println!` and `eprintln!` will be sent to this callback.
19 | /// If no callback is set, output goes to stdout/stderr as normal.
20 | ///
21 | /// Returns a guard that when dropped, resets the callback.
22 | pub(crate) fn set_output_callback(callback: F) -> OutputCallbackGuard
23 | where
24 | F: Fn(&str) + Send + Sync + 'static,
25 | {
26 | *OUTPUT_CALLBACK.lock().unwrap() = Some(Box::new(callback));
27 | OutputCallbackGuard
28 | }
29 |
30 | pub struct OutputCallbackGuard;
31 |
32 | impl Drop for OutputCallbackGuard {
33 | fn drop(&mut self) {
34 | clear_output_callback();
35 | }
36 | }
37 |
38 | /// Clear the global output callback
39 | pub(crate) fn clear_output_callback() {
40 | *OUTPUT_CALLBACK.lock().unwrap() = None;
41 | }
42 |
43 | /// Write a line to the output (either callback or stdout)
44 | pub(crate) fn write_output_line(s: &str) {
45 | if let Ok(guard) = OUTPUT_CALLBACK.lock()
46 | && let Some(ref callback) = *guard
47 | {
48 | callback(s);
49 | callback("\n");
50 | return;
51 | }
52 | // Fallback to stdout if no callback or lock failed
53 | std::println!("{}", s);
54 | let _ = std::io::stdout().flush();
55 | }
56 |
57 | /// Write an error line to the output (either callback or stderr)
58 | pub(crate) fn write_error_line(s: &str) {
59 | if let Ok(guard) = OUTPUT_CALLBACK.lock()
60 | && let Some(ref callback) = *guard
61 | {
62 | callback(s);
63 | callback("\n");
64 | return;
65 | }
66 | // Fallback to stderr if no callback or lock failed
67 | std::eprintln!("{}", s);
68 | let _ = std::io::stderr().flush();
69 | }
70 |
71 | /// Write raw output without newline (either callback or stdout)
72 | pub(crate) fn write_output_raw(s: &str) {
73 | if let Ok(guard) = OUTPUT_CALLBACK.lock()
74 | && let Some(ref callback) = *guard
75 | {
76 | callback(s);
77 | return;
78 | }
79 | // Fallback to stdout if no callback or lock failed
80 | std::print!("{}", s);
81 | let _ = std::io::stdout().flush();
82 | }
83 |
84 | /// Shadow println! macro to respect the output callback
85 | macro_rules! println {
86 | () => {
87 | $crate::output::write_output_line("")
88 | };
89 | ($($arg:tt)*) => {{
90 | $crate::output::write_output_line(&format!($($arg)*))
91 | }};
92 | }
93 | pub(crate) use println;
94 |
95 | /// Shadow eprintln! macro to respect the output callback
96 | macro_rules! eprintln {
97 | () => {
98 | $crate::output::write_error_line("")
99 | };
100 | ($($arg:tt)*) => {{
101 | $crate::output::write_error_line(&format!($($arg)*))
102 | }};
103 | }
104 | pub(crate) use eprintln;
105 |
106 | /// Shadow print! macro to respect the output callback
107 | macro_rules! print {
108 | ($($arg:tt)*) => {{
109 | $crate::output::write_output_raw(&format!($($arg)*))
110 | }};
111 | }
112 | pub(crate) use print;
113 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/components/AnalysisStatus.svelte:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
51 | {#if entry.analysis_status === AnalysisStatus.Queued || entry.analysis_status === AnalysisStatus.Running || (entry.analysis_status === AnalysisStatus.Finished && entry.analysis_report === undefined)}
52 |
58 |
66 |
71 |
72 | {/if}
73 | {summary}
74 |
75 |
84 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/doc/faq.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 |
3 | ### Do I need an active SIM card to use Rayhunter?
4 |
5 | **It Depends**. Operation of Rayhunter does require the insertion of a SIM card into the device, but that sim card does not have to be actively registered with a service plan. If you want to use the device as a hotspot in addition to a research device, or get [notifications](./configuration.md), an active plan would of course be necessary.
6 |
7 | ### How can I test that my device is working?
8 | You can enable the `Test Heuristic` under `Analyzer Heuristic Settings` in the config section on your web dashboard. This will cause an alert to trigger every time your device sees a cell tower, you might need to reboot your device or move around a bit to get this one to trigger, but it will be very noisy once it does. People have also tested it by building IMSI catchers at home, but we don't recommend that, since it violates FCC regulations and will probably upset your neighbors.
9 |
10 |
11 |
12 | ### Help, Rayhunter's line is red/orange/yellow/dotted/dashed! What should I do?
13 |
14 | Unfortunately, the circumstances that might lead to a positive cell site simulator (CSS) signal are quite varied, so we don't have a universal recommendation for how to deal with the a positive signal. Depending on your circumstances and threat model, you may want to turn off your phone until you are out of the area and tell your friends to do the same!
15 |
16 | If you've received a Rayhunter warning and would like to help us with our research, please send your Rayhunter data captures (Zip file downloaded from the web interface) to us at our [Signal](https://signal.org/) username [**ElectronicFrontierFoundation.90**](https://signal.me/#eu/HZbPPED5LyMkbTxJsG2PtWc2TXxPUR1OxBMcJGLOPeeCDGPuaTpOi5cfGRY6RrGf) with the following information: capture date, capture location, device, device model, and Rayhunter version. If you're unfamiliar with Signal, feel free to check out our [Security Self Defense guide on it](https://ssd.eff.org/module/how-to-use-signal).
17 |
18 | Please note that this file may contain sensitive information such as your IMSI and the unique IDs of cell towers you were near which could be used to ascertain your location at the time.
19 |
20 |
21 | ### Should I get a locked or unlocked orbic device? What is the difference?
22 |
23 | If you want to use a non-Verizon SIM card you will probably need an unlocked device. But it's not clear which devices are locked nor how to unlock them, we welcome any experimentation and information regarding the use of unlocked devices. So far most verizon branded orbic devices we have encountered are actually unlocked.
24 |
25 | ### How do I re-enable USB tethering after installing Rayhunter?
26 |
27 | Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
28 |
29 | ```sh
30 | ./installer util shell "echo 9 > /usrdata/mode.cfg"
31 | ./installer util shell reboot
32 | ```
33 |
34 | To disable tethering again:
35 |
36 | ```sh
37 | ./installer util shell "echo 3 > /usrdata/mode.cfg"
38 | ./installer util shell reboot
39 | ```
40 |
41 | See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
42 |
43 |
44 | ### How do I disable the WiFi hotspot on the Orbic RC400L?
45 |
46 | To disable both WiFi bands:
47 |
48 | ```sh
49 | adb shell
50 | /bin/rootshell -c "sed -i 's/1<\/state>/0<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot"
51 | ```
52 |
53 | To re-enable WiFi:
54 |
55 | ```sh
56 | adb shell
57 | /bin/rootshell -c "sed -i 's/0<\/state>/1<\/state>/g' /usrdata/data/usr/wlan/wlan_conf_6174.xml && reboot"
58 | ```
59 |
--------------------------------------------------------------------------------
/lib/src/log_codes.rs:
--------------------------------------------------------------------------------
1 | //! Enumerates some relevant diag log codes. Copied from QCSuper
2 |
3 | // These are 2G-related log types.
4 |
5 | pub const LOG_GSM_RR_SIGNALING_MESSAGE_C: u32 = 0x512f;
6 |
7 | pub const DCCH: u32 = 0x00;
8 | pub const BCCH: u32 = 0x01;
9 | pub const L2_RACH: u32 = 0x02;
10 | pub const CCCH: u32 = 0x03;
11 | pub const SACCH: u32 = 0x04;
12 | pub const SDCCH: u32 = 0x05;
13 | pub const FACCH_F: u32 = 0x06;
14 | pub const FACCH_H: u32 = 0x07;
15 | pub const L2_RACH_WITH_NO_DELAY: u32 = 0x08;
16 |
17 | // These are GPRS-related log types.
18 |
19 | pub const LOG_GPRS_MAC_SIGNALLING_MESSAGE_C: u32 = 0x5226;
20 |
21 | pub const PACCH_RRBP_CHANNEL: u32 = 0x03;
22 | pub const UL_PACCH_CHANNEL: u32 = 0x04;
23 | pub const DL_PACCH_CHANNEL: u32 = 0x83;
24 |
25 | pub const PACKET_CHANNEL_REQUEST: u32 = 0x20;
26 |
27 | // These are 5G-related log types.
28 |
29 | pub const LOG_NR_RRC_OTA_MSG_LOG_C: u32 = 0xb821;
30 |
31 | // These are 4G-related log types.
32 |
33 | pub const LOG_LTE_RRC_OTA_MSG_LOG_C: u32 = 0xb0c0;
34 | pub const LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C: u32 = 0xb0e2;
35 | pub const LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C: u32 = 0xb0e3;
36 | pub const LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C: u32 = 0xb0ec;
37 | pub const LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C: u32 = 0xb0ed;
38 |
39 | pub const LTE_BCCH_BCH_V0: u32 = 1;
40 | pub const LTE_BCCH_DL_SCH_V0: u32 = 2;
41 | pub const LTE_MCCH_V0: u32 = 3;
42 | pub const LTE_PCCH_V0: u32 = 4;
43 | pub const LTE_DL_CCCH_V0: u32 = 5;
44 | pub const LTE_DL_DCCH_V0: u32 = 6;
45 | pub const LTE_UL_CCCH_V0: u32 = 7;
46 | pub const LTE_UL_DCCH_V0: u32 = 8;
47 |
48 | pub const LTE_BCCH_BCH_V14: u32 = 1;
49 | pub const LTE_BCCH_DL_SCH_V14: u32 = 2;
50 | pub const LTE_MCCH_V14: u32 = 4;
51 | pub const LTE_PCCH_V14: u32 = 5;
52 | pub const LTE_DL_CCCH_V14: u32 = 6;
53 | pub const LTE_DL_DCCH_V14: u32 = 7;
54 | pub const LTE_UL_CCCH_V14: u32 = 8;
55 | pub const LTE_UL_DCCH_V14: u32 = 9;
56 |
57 | pub const LTE_BCCH_BCH_V9: u32 = 8;
58 | pub const LTE_BCCH_DL_SCH_V9: u32 = 9;
59 | pub const LTE_MCCH_V9: u32 = 10;
60 | pub const LTE_PCCH_V9: u32 = 11;
61 | pub const LTE_DL_CCCH_V9: u32 = 12;
62 | pub const LTE_DL_DCCH_V9: u32 = 13;
63 | pub const LTE_UL_CCCH_V9: u32 = 14;
64 | pub const LTE_UL_DCCH_V9: u32 = 15;
65 |
66 | pub const LTE_BCCH_BCH_V19: u32 = 1;
67 | pub const LTE_BCCH_DL_SCH_V19: u32 = 3;
68 | pub const LTE_MCCH_V19: u32 = 6;
69 | pub const LTE_PCCH_V19: u32 = 7;
70 | pub const LTE_DL_CCCH_V19: u32 = 8;
71 | pub const LTE_DL_DCCH_V19: u32 = 9;
72 | pub const LTE_UL_CCCH_V19: u32 = 10;
73 | pub const LTE_UL_DCCH_V19: u32 = 11;
74 |
75 | pub const LTE_BCCH_BCH_NB: u32 = 45;
76 | pub const LTE_BCCH_DL_SCH_NB: u32 = 46;
77 | pub const LTE_PCCH_NB: u32 = 47;
78 | pub const LTE_DL_CCCH_NB: u32 = 48;
79 | pub const LTE_DL_DCCH_NB: u32 = 49;
80 | pub const LTE_UL_CCCH_NB: u32 = 50;
81 | pub const LTE_UL_DCCH_NB: u32 = 52;
82 |
83 | // These are 3G-related log types.
84 |
85 | pub const RRCLOG_SIG_UL_CCCH: u32 = 0;
86 | pub const RRCLOG_SIG_UL_DCCH: u32 = 1;
87 | pub const RRCLOG_SIG_DL_CCCH: u32 = 2;
88 | pub const RRCLOG_SIG_DL_DCCH: u32 = 3;
89 | pub const RRCLOG_SIG_DL_BCCH_BCH: u32 = 4;
90 | pub const RRCLOG_SIG_DL_BCCH_FACH: u32 = 5;
91 | pub const RRCLOG_SIG_DL_PCCH: u32 = 6;
92 | pub const RRCLOG_SIG_DL_MCCH: u32 = 7;
93 | pub const RRCLOG_SIG_DL_MSCH: u32 = 8;
94 | pub const RRCLOG_EXTENSION_SIB: u32 = 9;
95 | pub const RRCLOG_SIB_CONTAINER: u32 = 10;
96 |
97 | // 3G layer 3 packets:
98 |
99 | pub const WCDMA_SIGNALLING_MESSAGE: u32 = 0x412f;
100 |
101 | // Upper layers
102 |
103 | pub const LOG_DATA_PROTOCOL_LOGGING_C: u32 = 0x11eb;
104 |
105 | pub const LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C: u32 = 0x713a;
106 |
--------------------------------------------------------------------------------
/doc/tplink-m7350.md:
--------------------------------------------------------------------------------
1 | # TP-Link M7350
2 |
3 | Supported in Rayhunter since version 0.3.0.
4 |
5 | The TP-Link M7350 supports many more frequency bands than Orbic and therefore works in Europe and also in some Asian and African countries.
6 |
7 | ## Supported Bands
8 |
9 | | Technology | Bands |
10 | | ---------- | ----- |
11 | | 4G LTE | B1/B3/B7/B8/B20 (2100/1800/2600/900/800 MHz) |
12 | | 3G | B1/B8 (2100/900 MHz) |
13 | | 2G | 850/900/1800/1900 MHz |
14 |
15 | *Source: [TP-Link Official Product Page](https://www.tp-link.com/baltic/service-provider/lte-3g/m7350/)*
16 |
17 | ## Hardware versions
18 |
19 | The TP-Link comes in many different *hardware versions*. Support for installation varies:
20 |
21 | * `1.0`, `2.0`: **Not supported**, devs are not able to obtain a device
22 | * `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: **Tested, no known issues since 0.3.0.**
23 | * `6.2`: **One user reported it is working, not tested**
24 | * `4.0`: **Manual firmware downgrade required** ([issue](https://github.com/EFForg/rayhunter/issues/332))
25 | * `9.0`: **Working since 0.3.2.**
26 |
27 | TP-Link versions newer than `3.0` have cyan packaging and a color display. Version `3.0` has a one-bit display and white packaging.
28 |
29 | You can find the exact hardware version of each device under the battery or next to the barcode on the outer packaging, for example `V3.0` or `V5.2`.
30 |
31 | When filing bug reports, particularly with the installer, please always specify the exact hardware version.
32 |
33 | You can get your TP-Link M7350 from:
34 |
35 | * First check for used offers on local sites, sometimes it's much cheaper there.
36 | * [Geizhals price comparison](https://geizhals.eu/?fs=tp-link+m7350).
37 | * [Ebay](https://www.ebay.com/sch/i.html?_nkw=tp-link+m7350&_sacat=0&_from=R40&_trksid=p4432023.m570.l1313).
38 |
39 | ## Installation & Usage
40 |
41 | Follow the [release installation guide](./installing-from-release.md). Substitute `./installer orbic` for `./installer tplink` in other documentation. The Rayhunter UI will be available at .
42 |
43 | ## Obtaining a shell
44 |
45 | You can obtain a root shell with the following command:
46 |
47 | ```sh
48 | ./installer util tplink-shell
49 | ```
50 |
51 | ## Display states
52 |
53 | If your device has a color display, Rayhunter will show the same red/green/white line at the top of the display as it does on Orbic, each color meaning "warning"/"recording"/"paused" respectively. See [Using Rayhunter](./using-rayhunter.md).
54 |
55 | If your device has a one-bit (black-and-white) display, Rayhunter will instead show an emoji to indicate status:
56 |
57 | * `!` means "warning (potential IMSI catcher)"
58 | * `:)` (smiling) means "recording"
59 | * `:` (face with no mouth) means "paused"
60 |
61 | ## Power-saving mode/sleep
62 |
63 | By default the device will go to sleep after N minutes of no devices being connected. In that mode it will also turn off connections to cell phone towers.
64 | In order for Rayhunter to record continuously, you have to turn off this sleep mode in TP-Link's admin panel (go to **Advanced** - **Power Saving**) or keep e.g. your phone connected on the TP-Link's WiFi.
65 |
66 | ## Port triggers
67 |
68 | On hardware revisions starting with v4.0, the installer will modify settings to
69 | add two port triggers. You can look at `Settings > NAT Settings > Port
70 | Triggers` in TP-Link's admin UI to see them.
71 |
72 | 1. One port trigger "rayhunter-root" to launch the telnet shell. This is only needed for installation, and can be removed after upgrade. You can reinstall it using `./installer util tplink-shell`.
73 | 2. One port trigger "rayhunter-daemon" to auto-start Rayhunter on boot. If you remove this, Rayhunter will have to be started manually from shell.
74 |
75 | ## Other links
76 |
77 | For more information on the device and instructions on how to install Rayhunter without an installer (i.e. manually), please see [rayhunter-tplink-m7350](https://github.com/m0veax/rayhunter-tplink-m7350/)
78 |
--------------------------------------------------------------------------------
/daemon/web/src/lib/manifest.svelte.ts:
--------------------------------------------------------------------------------
1 | import { get_report, type AnalysisReport } from './analysis.svelte';
2 | import { AnalysisStatus, type AnalysisManager } from './analysisManager.svelte';
3 |
4 | interface JsonManifest {
5 | entries: JsonManifestEntry[];
6 | current_entry: JsonManifestEntry | null;
7 | }
8 |
9 | interface JsonManifestEntry {
10 | name: string;
11 | start_time: string;
12 | last_message_time: string;
13 | qmdl_size_bytes: number;
14 | }
15 |
16 | export class Manifest {
17 | public entries: ManifestEntry[] = [];
18 | public current_entry: ManifestEntry | undefined;
19 |
20 | constructor(json: JsonManifest) {
21 | for (const entry of json.entries) {
22 | this.entries.push(new ManifestEntry(entry));
23 | }
24 | if (json.current_entry !== null) {
25 | this.current_entry = new ManifestEntry(json['current_entry']);
26 | }
27 |
28 | // sort entries in reverse chronological order
29 | this.entries.reverse();
30 | }
31 |
32 | async set_analysis_status(manager: AnalysisManager) {
33 | for (const entry of this.entries) {
34 | entry.analysis_status = manager.status.get(entry.name);
35 | entry.analysis_report = manager.reports.get(entry.name);
36 | }
37 |
38 | if (this.current_entry) {
39 | try {
40 | this.current_entry.analysis_report = await get_report(this.current_entry.name);
41 | } catch (err) {
42 | this.current_entry.analysis_report = `Err: failed to get analysis report: ${err}`;
43 | }
44 |
45 | // the current entry should always be considered "finished", as its
46 | // analysis report is always available
47 | this.current_entry.analysis_status = AnalysisStatus.Finished;
48 | }
49 | }
50 | }
51 |
52 | export class ManifestEntry {
53 | public name = $state('');
54 | public start_time: Date;
55 | public last_message_time: Date | undefined = $state(undefined);
56 | public qmdl_size_bytes = $state(0);
57 | public analysis_size_bytes = $state(0);
58 | public analysis_status: AnalysisStatus | undefined = $state(undefined);
59 | public analysis_report: AnalysisReport | string | undefined = $state(undefined);
60 |
61 | constructor(json: JsonManifestEntry) {
62 | this.name = json.name;
63 | this.qmdl_size_bytes = json.qmdl_size_bytes;
64 | this.start_time = new Date(json.start_time);
65 | if (json.last_message_time) {
66 | this.last_message_time = new Date(json.last_message_time);
67 | }
68 | }
69 |
70 | get_readable_qmdl_size(): string {
71 | if (this.qmdl_size_bytes === 0) return '0 Bytes';
72 | const k = 1024;
73 | const dm = 2;
74 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
75 | const i = Math.floor(Math.log(this.qmdl_size_bytes) / Math.log(k));
76 | return `${Number.parseFloat((this.qmdl_size_bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
77 | }
78 |
79 | get_num_warnings(): number | undefined {
80 | if (this.analysis_report === undefined || typeof this.analysis_report === 'string') {
81 | return undefined;
82 | }
83 | return this.analysis_report.statistics.num_warnings;
84 | }
85 |
86 | get_pcap_url(): string {
87 | return `/api/pcap/${this.name}.pcapng`;
88 | }
89 |
90 | get_qmdl_url(): string {
91 | return `/api/qmdl/${this.name}.qmdl`;
92 | }
93 |
94 | get_zip_url(): string {
95 | return `/api/zip/${this.name}.zip`;
96 | }
97 |
98 | get_analysis_report_url(): string {
99 | return `/api/analysis-report/${this.name}`;
100 | }
101 |
102 | get_delete_url(): string {
103 | return `/api/delete-recording/${this.name}`;
104 | }
105 |
106 | get_reanalyze_url(): string {
107 | return `/api/analysis/${this.name}`;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/doc/installing-from-release.md:
--------------------------------------------------------------------------------
1 | # Installing from the latest release
2 |
3 | Make sure you've got one of Rayhunter's [supported devices](./supported-devices.md). These instructions have only been tested on macOS and Ubuntu 24.04. If they fail, you will need to [install Rayhunter from source](./installing-from-source.md).
4 |
5 | 1. **For the TP-Link only,** insert a FAT-formatted SD card. This will be used to store all recordings.
6 | 2. Download the latest `rayhunter-vX.X.X-PLATFORM.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases) for your platform:
7 | - for Linux on x64 architecture: `linux-x64`
8 | - for Linux on ARM64 architecture: `linux-aarch64`
9 | - for Linux on armv7/v8 (32-bit) architecture: `linux-armv7`
10 | - for MacOS on Intel (old macbooks) architecture: `macos-intel`
11 | - for MacOS on ARM (M1/M2 etc.) architecture: `macos-arm`
12 | - for Windows: `windows-x86_64`
13 |
14 | 3. Decompress the `rayhunter-vX.X.X-PLATFORM.zip` archive. Open the terminal and navigate to the folder. (Be sure to replace X.X.X with the correct version number!)
15 |
16 | ```bash
17 | unzip ~/Downloads/rayhunter-vX.X.X-PLATFORM.zip
18 | cd ~/Downloads/rayhunter-vX.X.X-PLATFORM
19 | ```
20 |
21 | On Windows you can decompress using the file browser, then navigate to the
22 | folder that contains `installer.exe`, **hold Shift**, Right-Click inside the
23 | folder, then click "Open in PowerShell".
24 |
25 | 4. **Connect to your device.**
26 |
27 | First turn on your device by holding the power button on the front.
28 |
29 | Then connect to the device using either WiFi or USB tethering.
30 |
31 | You know you are in the right network when you can access
32 | (Orbic) or (TP-Link) and see the
33 | hardware's own admin menu.
34 |
35 | 5. **On MacOS only**, you have to run `xattr -d
36 | com.apple.quarantine installer` to allow execution of
37 | the binary.
38 |
39 | 6. **Run the installer.**
40 |
41 | ```bash
42 | # For Orbic:
43 | ./installer orbic --admin-password 'mypassword'
44 | # Or install over USB if you want ADB and a root shell (not recommended for most users)
45 | ./installer orbic-usb
46 |
47 | # For TP-Link:
48 | ./installer tplink
49 | ```
50 |
51 | * On Verizon Orbic, the password is the WiFi password.
52 | * On Kajeet/Smartspot devices, the default password is `$m@rt$p0tc0nf!g`
53 | * On Moxee-brand devices, check under the battery for the password.
54 | * You can reset the password by pressing the button under the back case until the unit restarts.
55 |
56 | TP-Link does not require an `--admin-password` parameter.
57 |
58 | For other devices, check `./installer --help` or the
59 | respective page in the sidebar under "Supported
60 | Devices."
61 |
62 | 7. The installer will eventually tell you it's done, and the device will reboot.
63 |
64 | 8. Rayhunter should now be running! You can verify this by [viewing Rayhunter's web UI](./using-rayhunter.md). You should also see a green line flash along the top of top the display on the device.
65 |
66 | ## Troubleshooting
67 |
68 | * If you are having trouble installing Rayhunter and you're connecting to your device over USB, try using a different USB cable to connect the device to your computer. If you are using a USB hub, try using a different one or directly connecting the device to a USB port on your computer. A faulty USB connection can cause the Rayhunter installer to fail.
69 |
70 | * You can test your device by enabling the test heuristic. This will be very noisy and fire an alert every time you see a new tower. Be sure to turn it off when you are done testing.
71 |
72 | * On MacOS if you encounter an error that says "No Orbic device found," it may because you have the "Allow accessories to connect" security setting set to "Ask for approval." You may need to temporarily change it to "Always" for the script to run. Make sure to change it back to a more secure setting when you're done.
73 |
74 | ```bash
75 | ./installer --help
76 | ./installer util --help
77 | ```
78 |
--------------------------------------------------------------------------------
/lib/src/hdlc.rs:
--------------------------------------------------------------------------------
1 | //! HDLC stands for "High-level Data Link Control", which the diag protocol uses
2 | //! to encapsulate its messages. QCSuper's docs describe this in more detail
3 | //! here:
4 | //!
5 |
6 | use bytes::Buf;
7 | use crc::Crc;
8 | use thiserror::Error;
9 |
10 | use crate::diag::{
11 | ESCAPED_MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR, MESSAGE_ESCAPE_CHAR,
12 | MESSAGE_TERMINATOR,
13 | };
14 |
15 | #[derive(Debug, Clone, Error, PartialEq)]
16 | pub enum HdlcError {
17 | #[error("Invalid checksum (expected {0}, got {1})")]
18 | InvalidChecksum(u16, u16),
19 | #[error("Invalid HDLC escape sequence: [0x7d, {0}]")]
20 | InvalidEscapeSequence(u8),
21 | #[error("No trailing character found (expected 0x7e, got {0}))")]
22 | NoTrailingCharacter(u8),
23 | #[error("Missing checksum")]
24 | MissingChecksum,
25 | #[error("Data too short to be HDLC encapsulated")]
26 | TooShort,
27 | }
28 |
29 | pub fn hdlc_encapsulate(data: &[u8], crc: &Crc) -> Vec {
30 | let mut result: Vec = Vec::with_capacity(data.len());
31 |
32 | for &b in data {
33 | match b {
34 | MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]),
35 | MESSAGE_ESCAPE_CHAR => {
36 | result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR])
37 | }
38 | _ => result.push(b),
39 | }
40 | }
41 |
42 | for b in crc.checksum(data).to_le_bytes() {
43 | match b {
44 | MESSAGE_TERMINATOR => result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_TERMINATOR]),
45 | MESSAGE_ESCAPE_CHAR => {
46 | result.extend([MESSAGE_ESCAPE_CHAR, ESCAPED_MESSAGE_ESCAPE_CHAR])
47 | }
48 | _ => result.push(b),
49 | }
50 | }
51 |
52 | result.push(MESSAGE_TERMINATOR);
53 | result
54 | }
55 |
56 | pub fn hdlc_decapsulate(data: &[u8], crc: &Crc) -> Result, HdlcError> {
57 | if data.len() < 3 {
58 | return Err(HdlcError::TooShort);
59 | }
60 |
61 | if data[data.len() - 1] != MESSAGE_TERMINATOR {
62 | return Err(HdlcError::NoTrailingCharacter(data[data.len() - 1]));
63 | }
64 |
65 | let mut unescaped = Vec::with_capacity(data.len());
66 | let mut escaping = false;
67 | for &b in &data[..data.len() - 1] {
68 | if escaping {
69 | match b {
70 | ESCAPED_MESSAGE_TERMINATOR => unescaped.push(MESSAGE_TERMINATOR),
71 | ESCAPED_MESSAGE_ESCAPE_CHAR => unescaped.push(MESSAGE_ESCAPE_CHAR),
72 | _ => return Err(HdlcError::InvalidEscapeSequence(b)),
73 | }
74 | escaping = false;
75 | } else if b == MESSAGE_ESCAPE_CHAR {
76 | escaping = true
77 | } else {
78 | unescaped.push(b);
79 | }
80 | }
81 |
82 | // pop off the u16 checksum, check it against what we calculated
83 | let checksum_hi = unescaped.pop().ok_or(HdlcError::MissingChecksum)?;
84 | let checksum_lo = unescaped.pop().ok_or(HdlcError::MissingChecksum)?;
85 | let checksum = [checksum_lo, checksum_hi].as_slice().get_u16_le();
86 | if checksum != crc.checksum(&unescaped) {
87 | return Err(HdlcError::InvalidChecksum(
88 | checksum,
89 | crc.checksum(&unescaped),
90 | ));
91 | }
92 |
93 | Ok(unescaped)
94 | }
95 |
96 | #[cfg(test)]
97 | mod tests {
98 | use super::*;
99 |
100 | #[test]
101 | fn test_hdlc_encapsulate() {
102 | let crc = Crc::::new(&crate::diag::CRC_CCITT_ALG);
103 | let data = vec![0x01, 0x02, 0x03, 0x04];
104 | let expected = vec![1, 2, 3, 4, 145, 57, 126];
105 | let encapsulated = hdlc_encapsulate(&data, &crc);
106 | assert_eq!(&encapsulated, &expected);
107 | assert_eq!(hdlc_decapsulate(&encapsulated, &crc), Ok(data));
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/doc/wingtech-ct2mhs01.md:
--------------------------------------------------------------------------------
1 | # Wingtech CT2MHS01
2 |
3 | Supported in Rayhunter since version 0.4.0.
4 |
5 | The Wingtech CT2MHS01 hotspot is a Qualcomm mdm9650-based device with a screen available for US$15-35. This device is often used as a base platform for white labeled versions like the T-Mobile TMOHS1. AT&T branded versions of the hotspot seem to be the most abundant.
6 |
7 | ## Supported bands
8 |
9 | There are likely variants of the device for all three ITU regions.
10 |
11 | According to FCC ID 2APXW-CT2MHS01 Test Report No. [I20N02441-RF-LTE](https://fcc.report/FCC-ID/2APXW-CT2MHS01/4957451), the ITU Region 2 American version of the device supports the following LTE bands:
12 |
13 | | Band | Frequency |
14 | | ---- | ---------------- |
15 | | 2 | 1900 MHz (PCS) |
16 | | 5 | 850 MHz (CLR) |
17 | | 12 | 700 MHz (LSMH) |
18 | | 14 | 700 MHz (USMH) |
19 | | 30 | 2300 MHz (WCS) |
20 | | 66 | 1700 MHz (E-AWS) |
21 |
22 | Note that Band 5 (850 MHz, CLR) is suitable for roaming in ITU regions 2 and 3.
23 |
24 | ## Hardware
25 | Wingtechs are abundant on ebay and can also be found on Amazon:
26 | -
27 | -
28 | -
29 | -
30 |
31 | ## Installing
32 | Connect to the Wingtech's network over WiFi or USB tethering, then run the installer:
33 |
34 | ```sh
35 | ./installer wingtech --admin-password 12345678 # replace with your own password
36 | ```
37 |
38 | ## Obtaining a shell
39 | Even when Rayhunter is running, for security reasons the Wingtech will not have telnet or adb enabled during normal operation.
40 |
41 | Use either command below to enable telnet or adb access:
42 |
43 | ```sh
44 | ./installer util wingtech-start-telnet --admin-password 12345678
45 | telnet 192.168.1.1
46 | ```
47 |
48 | ```sh
49 | ./installer util wingtech-start-adb --admin-password 12345678
50 | adb shell
51 | ```
52 |
53 | ## Developing
54 | The device has a framebuffer-driven screen at /dev/fb0 that behaves
55 | similarly to the Orbic RC400L, although the userspace program
56 | `displaygui` refreshes the screen significantly more often than on the
57 | Orbic. This causes the green line on the screen to subtly flicker and
58 | only be displayed during some frames. Subsequent work to fully control
59 | the display without removing the OEM interface is desired.
60 |
61 | Rayhunter has been tested on:
62 |
63 | ```sh
64 | WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
65 | WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
66 | WT_HARDWARE_VERSION=89323_1_20
67 | ```
68 |
69 | Please consider sharing the contents of your device's /etc/wt_version file here.
70 |
71 | ## Troubleshooting
72 |
73 | ### My hotspot won't turn on after rebooting when installing over WiFi
74 |
75 | Reinsert the battery and turn the device back on, Rayhunter should be installed and running. Sometimes the Wingtech hotspot gets stuck off and ignores the power button after a reboot until the battery is reseated.
76 |
77 | You do not need to run the installer again.
78 |
79 | You'll likely see the following messages, where the installer is stuck at `Testing rayhunter ... `.
80 |
81 | ```sh
82 | Starting telnet ... ok
83 | Connecting via telnet to 192.168.1.1 ... ok
84 | Sending file /data/rayhunter/config.toml ... ok
85 | Sending file /data/rayhunter/rayhunter-daemon ... ok
86 | Sending file /etc/init.d/rayhunter_daemon ... ok
87 | Rebooting device and waiting 30 seconds for it to start up.
88 | Testing rayhunter ...
89 | ```
90 |
91 | If you eventually see:
92 |
93 | ```sh
94 | Testing rayhunter ...
95 | Failed to install rayhunter on the Wingtech CT2MHS01
96 |
97 | Caused by:
98 | 0: error sending request for url (http://192.168.1.1:8080/index.html)
99 | 1: client error (Connect)
100 | 2: tcp connect error: Network is unreachable (os error 101)
101 | 3: Network is unreachable (os error 101)
102 | ```
103 |
104 | Make sure your computer is connected to the hotspot's WiFi network.
105 |
--------------------------------------------------------------------------------