mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 10:13:35 -07:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe6afac817 | |||
| 8e708f145e | |||
| 03c00a1f19 | |||
| 64842c7140 | |||
| e108c21fc2 | |||
| 49a2108214 | |||
| 53a6cbe95a | |||
| 398997af67 | |||
| 6b109a9d76 | |||
| d9688b1796 | |||
| 7466c1c669 | |||
| 6a51050921 | |||
| 0935cf8239 | |||
| d25e9588e2 | |||
| a8ff95a07b | |||
| ac86277903 | |||
| 8e9abc718a | |||
| d92fb16c57 | |||
| f8824ce7e7 | |||
| 9694aa826b | |||
| b859dde0c8 |
@@ -36,6 +36,11 @@ lto = "fat"
|
||||
opt-level = "z"
|
||||
strip = "debuginfo"
|
||||
|
||||
[profile.firmware-devel]
|
||||
inherits = "release"
|
||||
opt-level = "s"
|
||||
lto = false
|
||||
|
||||
# optimizations to reduce the binary size of firmware binaries
|
||||
[profile.firmware]
|
||||
inherits = "release"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Files that are distributed onto the Rayhunter device always have to have
|
||||
# Unix-style line endings, even if the installer is built on Windows with
|
||||
# autocrlf enabled.
|
||||
# Using CRLF for the init scripts will make them fail to execute on TP-Link.
|
||||
# See https://github.com/EFForg/rayhunter/issues/489
|
||||
|
||||
dist/config.toml.in eol=lf
|
||||
dist/scripts/misc-daemon eol=lf
|
||||
dist/scripts/rayhunter_daemon eol=lf
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Installer Issue
|
||||
description: File an bug related to an installer issue.
|
||||
labels: ["bug", "installer"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Rayhunter Version
|
||||
placeholder: 'v0.5.0'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Device
|
||||
description: |
|
||||
What device are you trying to install Rayhunter on?
|
||||
options:
|
||||
- Orbic RC400L
|
||||
- Tplink HW7350
|
||||
- Tplink HW7310
|
||||
- Tmobile TMOHS1
|
||||
- Wingtech CT2MHS0
|
||||
- Pinephone
|
||||
- Other / I'm not sure
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Installer OS
|
||||
description: What operating system are running the installer from
|
||||
multiple: false
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Issue
|
||||
description: |
|
||||
Please describe the issue you're having installing Rayhunter.
|
||||
Include the logs outputed by the installer program. If the installer
|
||||
is crashing, please try running the installer with `RUST_BACKTRACE=1`
|
||||
environment variable set so we can see exactly where the installer is
|
||||
crashing.
|
||||
validations:
|
||||
required: true
|
||||
Generated
+7
-6
@@ -1489,7 +1489,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"aes",
|
||||
@@ -2384,7 +2384,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -2405,7 +2405,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-check"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"futures",
|
||||
@@ -2419,9 +2419,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -2536,7 +2537,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
||||
|
||||
[[package]]
|
||||
name = "rootshell"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"nix",
|
||||
]
|
||||
@@ -2918,7 +2919,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "telcom-parser"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"asn1-codecs",
|
||||
"asn1-compiler",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-check"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+3
-2
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rayhunter = { path = "../lib" }
|
||||
toml = "0.8.8"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt"] }
|
||||
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
|
||||
thiserror = "1.0.52"
|
||||
libc = "0.2.150"
|
||||
@@ -24,3 +24,4 @@ image = { version = "0.25.1", default-features = false, features = ["png", "gif
|
||||
tempfile = "3.10.1"
|
||||
async_zip = { version = "0.0.17", features = ["tokio"] }
|
||||
anyhow = "1.0.98"
|
||||
async-trait = "0.1.88"
|
||||
|
||||
+241
-123
@@ -1,3 +1,4 @@
|
||||
use std::ops::DerefMut;
|
||||
use std::pin::pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -9,135 +10,261 @@ use axum::response::{IntoResponse, Response};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use log::{debug, error, info, warn};
|
||||
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||
use rayhunter::diag::DataType;
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use rayhunter::qmdl::QmdlWriter;
|
||||
use tokio::fs::File;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
|
||||
use crate::display;
|
||||
use crate::qmdl_store::{EntryType, RecordingStore, RecordingStoreError};
|
||||
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
||||
use crate::server::ServerState;
|
||||
|
||||
pub enum DiagDeviceCtrlMessage {
|
||||
StopRecording,
|
||||
StartRecording,
|
||||
DeleteEntry {
|
||||
name: String,
|
||||
response_tx: oneshot::Sender<Result<(), RecordingStoreError>>,
|
||||
},
|
||||
DeleteAllEntries {
|
||||
response_tx: oneshot::Sender<Result<(), RecordingStoreError>>,
|
||||
},
|
||||
Exit,
|
||||
}
|
||||
|
||||
pub struct DiagTask {
|
||||
ui_update_sender: Sender<display::DisplayState>,
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
state: DiagState,
|
||||
}
|
||||
|
||||
enum DiagState {
|
||||
Recording {
|
||||
qmdl_writer: QmdlWriter<File>,
|
||||
analysis_writer: Box<AnalysisWriter>,
|
||||
},
|
||||
Stopped,
|
||||
}
|
||||
|
||||
impl DiagTask {
|
||||
fn new(
|
||||
ui_update_sender: Sender<display::DisplayState>,
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
ui_update_sender,
|
||||
analysis_sender,
|
||||
analyzer_config,
|
||||
state: DiagState::Stopped,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start recording
|
||||
async fn start(&mut self, qmdl_store: &mut RecordingStore) {
|
||||
let (qmdl_file, analysis_file) = qmdl_store
|
||||
.new_entry()
|
||||
.await
|
||||
.expect("failed creating QMDL file entry");
|
||||
self.stop_current_recording().await;
|
||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||
let analysis_writer = AnalysisWriter::new(analysis_file, &self.analyzer_config)
|
||||
.await
|
||||
.map(Box::new)
|
||||
.expect("failed to write to analysis file");
|
||||
self.state = DiagState::Recording {
|
||||
qmdl_writer,
|
||||
analysis_writer,
|
||||
};
|
||||
if let Err(e) = self
|
||||
.ui_update_sender
|
||||
.send(display::DisplayState::Recording)
|
||||
.await
|
||||
{
|
||||
warn!("couldn't send ui update message: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop recording
|
||||
async fn stop(&mut self, qmdl_store: &mut RecordingStore) {
|
||||
self.stop_current_recording().await;
|
||||
if let Some((_, entry)) = qmdl_store.get_current_entry() {
|
||||
if let Err(e) = self
|
||||
.analysis_sender
|
||||
.send(AnalysisCtrlMessage::RecordingFinished(
|
||||
entry.name.to_string(),
|
||||
))
|
||||
.await
|
||||
{
|
||||
warn!("couldn't send analysis message: {e}");
|
||||
}
|
||||
}
|
||||
if let Err(e) = qmdl_store.close_current_entry().await {
|
||||
error!("couldn't close current entry: {e}");
|
||||
}
|
||||
if let Err(e) = self
|
||||
.ui_update_sender
|
||||
.send(display::DisplayState::Paused)
|
||||
.await
|
||||
{
|
||||
warn!("couldn't send ui update message: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_entry(
|
||||
&mut self,
|
||||
qmdl_store: &mut RecordingStore,
|
||||
name: &str,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
if qmdl_store.is_current_entry(name) {
|
||||
self.stop(qmdl_store).await;
|
||||
}
|
||||
let res = qmdl_store.delete_entry(name).await;
|
||||
if let Err(e) = res.as_ref() {
|
||||
error!("Error deleting QMDL entry {e}");
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
async fn delete_all_entries(
|
||||
&mut self,
|
||||
qmdl_store: &mut RecordingStore,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
self.stop(qmdl_store).await;
|
||||
let res = qmdl_store.delete_all_entries().await;
|
||||
if let Err(e) = res.as_ref() {
|
||||
error!("Error deleting QMDL entries {e}");
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
async fn stop_current_recording(&mut self) {
|
||||
let mut state = DiagState::Stopped;
|
||||
std::mem::swap(&mut self.state, &mut state);
|
||||
if let DiagState::Recording {
|
||||
analysis_writer, ..
|
||||
} = state
|
||||
{
|
||||
analysis_writer
|
||||
.close()
|
||||
.await
|
||||
.expect("failed to close analysis writer");
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_container(
|
||||
&mut self,
|
||||
qmdl_store: &mut RecordingStore,
|
||||
container: MessagesContainer,
|
||||
) {
|
||||
if container.data_type != DataType::UserSpace {
|
||||
debug!("skipping non-userspace diag messages...");
|
||||
return;
|
||||
}
|
||||
// keep track of how many bytes were written to the QMDL file so we can read
|
||||
// a valid block of data from it in the HTTP server
|
||||
if let DiagState::Recording {
|
||||
qmdl_writer,
|
||||
analysis_writer,
|
||||
} = &mut self.state
|
||||
{
|
||||
qmdl_writer
|
||||
.write_container(&container)
|
||||
.await
|
||||
.expect("failed to write to QMDL writer");
|
||||
debug!(
|
||||
"total QMDL bytes written: {}, updating manifest...",
|
||||
qmdl_writer.total_written
|
||||
);
|
||||
let index = qmdl_store
|
||||
.current_entry
|
||||
.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
||||
qmdl_store
|
||||
.update_entry_qmdl_size(index, qmdl_writer.total_written)
|
||||
.await
|
||||
.expect("failed to update qmdl file size");
|
||||
debug!("done!");
|
||||
let heuristic_warning = analysis_writer
|
||||
.analyze(container)
|
||||
.await
|
||||
.expect("failed to analyze container");
|
||||
if heuristic_warning {
|
||||
info!("a heuristic triggered on this run!");
|
||||
self.ui_update_sender
|
||||
.send(display::DisplayState::WarningDetected)
|
||||
.await
|
||||
.expect("couldn't send ui update message: {}");
|
||||
}
|
||||
} else {
|
||||
debug!("no qmdl_writer set, continuing...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_diag_read_thread(
|
||||
task_tracker: &TaskTracker,
|
||||
mut dev: DiagDevice,
|
||||
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||
qmdl_file_tx: Sender<DiagDeviceCtrlMessage>,
|
||||
ui_update_sender: Sender<display::DisplayState>,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
let (initial_qmdl_file, initial_analysis_file) = qmdl_store_lock.write().await.new_entry().await.expect("failed creating QMDL file entry");
|
||||
let mut maybe_qmdl_writer: Option<QmdlWriter<File>> = Some(QmdlWriter::new(initial_qmdl_file));
|
||||
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
||||
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, &analyzer_config).await
|
||||
.expect("failed to create analysis writer"));
|
||||
let mut diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config);
|
||||
qmdl_file_tx
|
||||
.send(DiagDeviceCtrlMessage::StartRecording)
|
||||
.await
|
||||
.unwrap();
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = qmdl_file_rx.recv() => {
|
||||
match msg {
|
||||
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let (qmdl_file, new_analysis_file) = match qmdl_store.new_entry().await {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("couldn't create new qmdl entry: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
maybe_qmdl_writer = Some(QmdlWriter::new(qmdl_file));
|
||||
|
||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||
}
|
||||
|
||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, &analyzer_config).await
|
||||
.expect("failed to write to analysis file"));
|
||||
|
||||
if let Err(e) = ui_update_sender.send(display::DisplayState::Recording).await {
|
||||
warn!("couldn't send ui update message: {e}");
|
||||
}
|
||||
diag_task.start(qmdl_store.deref_mut()).await;
|
||||
},
|
||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
if let Some((_, entry)) = qmdl_store.get_current_entry() {
|
||||
if let Err(e) = analysis_sender
|
||||
.send(AnalysisCtrlMessage::RecordingFinished(
|
||||
entry.name.to_string(),
|
||||
))
|
||||
.await {
|
||||
warn!("couldn't send analysis message: {e}");
|
||||
}
|
||||
}
|
||||
if let Err(e) = qmdl_store.close_current_entry().await {
|
||||
error!("couldn't close current entry: {e}");
|
||||
}
|
||||
|
||||
maybe_qmdl_writer = None;
|
||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||
}
|
||||
maybe_analysis_writer = None;
|
||||
|
||||
if let Err(e) = ui_update_sender.send(display::DisplayState::Paused).await {
|
||||
warn!("couldn't send ui update message: {e}");
|
||||
}
|
||||
diag_task.stop(qmdl_store.deref_mut()).await;
|
||||
},
|
||||
// None means all the Senders have been dropped, so it's
|
||||
// time to go
|
||||
Some(DiagDeviceCtrlMessage::Exit) | None => {
|
||||
info!("Diag reader thread exiting...");
|
||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||
}
|
||||
diag_task.stop_current_recording().await;
|
||||
return Ok(())
|
||||
},
|
||||
Some(DiagDeviceCtrlMessage::DeleteEntry { name, response_tx }) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let resp = diag_task.delete_entry(qmdl_store.deref_mut(), name.as_str()).await;
|
||||
if response_tx.send(resp).is_err() {
|
||||
error!("Failed to send delete entry respons, receiver dropped");
|
||||
}
|
||||
},
|
||||
Some(DiagDeviceCtrlMessage::DeleteAllEntries { response_tx }) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let resp = diag_task.delete_all_entries(qmdl_store.deref_mut()).await;
|
||||
if response_tx.send(resp).is_err() {
|
||||
error!("Failed to send delete all entries respons, receiver dropped");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
maybe_container = diag_stream.next() => {
|
||||
match maybe_container.unwrap() {
|
||||
Ok(container) => {
|
||||
if container.data_type != DataType::UserSpace {
|
||||
debug!("skipping non-userspace diag messages...");
|
||||
continue;
|
||||
}
|
||||
// keep track of how many bytes were written to the QMDL file so we can read
|
||||
// a valid block of data from it in the HTTP server
|
||||
if let Some(qmdl_writer) = maybe_qmdl_writer.as_mut() {
|
||||
qmdl_writer.write_container(&container).await.expect("failed to write to QMDL writer");
|
||||
debug!("total QMDL bytes written: {}, updating manifest...", qmdl_writer.total_written);
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let index = qmdl_store.current_entry.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
||||
qmdl_store.update_entry_qmdl_size(index, qmdl_writer.total_written).await
|
||||
.expect("failed to update qmdl file size");
|
||||
debug!("done!");
|
||||
} else {
|
||||
debug!("no qmdl_writer set, continuing...");
|
||||
}
|
||||
|
||||
if let Some(analysis_writer) = maybe_analysis_writer.as_mut() {
|
||||
let heuristic_warning = analysis_writer.analyze(container).await
|
||||
.expect("failed to analyze container");
|
||||
if heuristic_warning {
|
||||
info!("a heuristic triggered on this run!");
|
||||
ui_update_sender.send(display::DisplayState::WarningDetected).await
|
||||
.expect("couldn't send ui update message: {}");
|
||||
}
|
||||
}
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
diag_task.process_container(qmdl_store.deref_mut(), container).await
|
||||
},
|
||||
Err(err) => {
|
||||
error!("error reading diag device: {err}");
|
||||
@@ -150,6 +277,7 @@ pub fn run_diag_read_thread(
|
||||
});
|
||||
}
|
||||
|
||||
/// Start recording API for web thread
|
||||
pub async fn start_recording(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
@@ -171,6 +299,7 @@ pub async fn start_recording(
|
||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||
}
|
||||
|
||||
/// Stop recording API for web thread
|
||||
pub async fn stop_recording(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
@@ -197,8 +326,27 @@ pub async fn delete_recording(
|
||||
if state.config.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
match qmdl_store.delete_entry(&qmdl_name).await {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
state
|
||||
.diag_device_ctrl_sender
|
||||
.send(DiagDeviceCtrlMessage::DeleteEntry {
|
||||
name: qmdl_name.clone(),
|
||||
response_tx,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send delete entry message: {e}"),
|
||||
)
|
||||
})?;
|
||||
match response_rx.await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("failed to receive delete response: {e}"),
|
||||
)
|
||||
})? {
|
||||
Ok(_) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
|
||||
Err(RecordingStoreError::NoSuchEntryError) => Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("no recording with name {qmdl_name}"),
|
||||
@@ -207,31 +355,6 @@ pub async fn delete_recording(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't delete recording: {e}"),
|
||||
)),
|
||||
Ok(entry_type) => {
|
||||
if entry_type == EntryType::Current {
|
||||
state
|
||||
.diag_device_ctrl_sender
|
||||
.send(DiagDeviceCtrlMessage::StopRecording)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send stop recording message: {e}"),
|
||||
)
|
||||
})?;
|
||||
state
|
||||
.ui_update_sender
|
||||
.send(display::DisplayState::Paused)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send ui update message: {e}"),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,34 +364,29 @@ pub async fn delete_all_recordings(
|
||||
if state.config.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
state
|
||||
.diag_device_ctrl_sender
|
||||
.send(DiagDeviceCtrlMessage::StopRecording)
|
||||
.send(DiagDeviceCtrlMessage::DeleteAllEntries { response_tx })
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send stop recording message: {e}"),
|
||||
format!("couldn't send delete all entries message: {e}"),
|
||||
)
|
||||
})?;
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
qmdl_store.delete_all_entries().await.map_err(|e| {
|
||||
match response_rx.await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't delete all recordings: {e}"),
|
||||
format!("failed to receive delete all response: {e}"),
|
||||
)
|
||||
})?;
|
||||
state
|
||||
.ui_update_sender
|
||||
.send(display::DisplayState::Paused)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send ui update message: {e}"),
|
||||
)
|
||||
})?;
|
||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||
})? {
|
||||
Ok(_) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't delete recordings: {e}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_analysis_report(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use async_trait::async_trait;
|
||||
use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::FilterType};
|
||||
use std::io::Cursor;
|
||||
use std::time::Duration;
|
||||
@@ -11,8 +12,6 @@ use tokio::sync::oneshot;
|
||||
use tokio::sync::oneshot::error::TryRecvError;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use std::thread::sleep;
|
||||
|
||||
use include_dir::{Dir, include_dir};
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -65,15 +64,13 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait GenericFramebuffer: Send + 'static {
|
||||
fn dimensions(&self) -> Dimensions;
|
||||
|
||||
fn write_buffer(
|
||||
&mut self,
|
||||
buffer: &[(u8, u8, u8)], // rgb, row-wise, left-to-right, top-to-bottom
|
||||
);
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>); // rgb, row-wise, left-to-right, top-to-bottom
|
||||
|
||||
fn write_dynamic_image(&mut self, img: DynamicImage) {
|
||||
async fn write_dynamic_image(&mut self, img: DynamicImage) {
|
||||
let dimensions = self.dimensions();
|
||||
let mut width = img.width();
|
||||
let mut height = img.height();
|
||||
@@ -94,28 +91,35 @@ pub trait GenericFramebuffer: Send + 'static {
|
||||
}
|
||||
}
|
||||
|
||||
self.write_buffer(&buf);
|
||||
self.write_buffer(buf).await
|
||||
}
|
||||
|
||||
fn draw_gif(&mut self, img_buffer: &[u8]) {
|
||||
// this is dumb and i'm sure there's a better way to loop this
|
||||
async fn draw_gif(&mut self, img_buffer: &[u8]) {
|
||||
let cursor = Cursor::new(img_buffer);
|
||||
let decoder = GifDecoder::new(cursor).unwrap();
|
||||
for maybe_frame in decoder.into_frames() {
|
||||
let frame = maybe_frame.unwrap();
|
||||
let (numerator, _) = frame.delay().numer_denom_ms();
|
||||
let img = DynamicImage::from(frame.into_buffer());
|
||||
self.write_dynamic_image(img);
|
||||
std::thread::sleep(Duration::from_millis(numerator as u64));
|
||||
if let Ok(decoder) = GifDecoder::new(cursor) {
|
||||
let frames: Vec<_> = decoder
|
||||
.into_frames()
|
||||
.filter_map(|f| f.ok())
|
||||
.map(|frame| {
|
||||
let (numerator, _) = frame.delay().numer_denom_ms();
|
||||
let img = DynamicImage::from(frame.into_buffer());
|
||||
(img, numerator as u64)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (img, delay_ms) in frames {
|
||||
self.write_dynamic_image(img).await;
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_img(&mut self, img_buffer: &[u8]) {
|
||||
async fn draw_img(&mut self, img_buffer: &[u8]) {
|
||||
let img = image::load_from_memory(img_buffer).unwrap();
|
||||
self.write_dynamic_image(img);
|
||||
self.write_dynamic_image(img).await
|
||||
}
|
||||
|
||||
fn draw_line(&mut self, color: Color, height: u32) {
|
||||
async fn draw_line(&mut self, color: Color, height: u32) {
|
||||
let width = self.dimensions().width;
|
||||
let px_num = height * width;
|
||||
let mut buffer = Vec::new();
|
||||
@@ -123,7 +127,7 @@ pub trait GenericFramebuffer: Send + 'static {
|
||||
buffer.push(color.rgb());
|
||||
}
|
||||
|
||||
self.write_buffer(&buffer);
|
||||
self.write_buffer(buffer).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +147,7 @@ pub fn update_ui(
|
||||
let colorblind_mode = config.colorblind_mode;
|
||||
let mut display_color = Color::from_state(DisplayState::Recording, colorblind_mode);
|
||||
|
||||
task_tracker.spawn_blocking(move || {
|
||||
task_tracker.spawn(async move {
|
||||
// this feels wrong, is there a more rusty way to do this?
|
||||
let mut img: Option<&[u8]> = None;
|
||||
if display_level == 2 {
|
||||
@@ -179,21 +183,21 @@ pub fn update_ui(
|
||||
}
|
||||
|
||||
match display_level {
|
||||
2 => fb.draw_gif(img.unwrap()),
|
||||
3 => fb.draw_img(img.unwrap()),
|
||||
2 => fb.draw_gif(img.unwrap()).await,
|
||||
3 => fb.draw_img(img.unwrap()).await,
|
||||
128 => {
|
||||
fb.draw_line(Color::Cyan, 128);
|
||||
fb.draw_line(Color::Pink, 102);
|
||||
fb.draw_line(Color::White, 76);
|
||||
fb.draw_line(Color::Pink, 50);
|
||||
fb.draw_line(Color::Cyan, 25);
|
||||
fb.draw_line(Color::Cyan, 128).await;
|
||||
fb.draw_line(Color::Pink, 102).await;
|
||||
fb.draw_line(Color::White, 76).await;
|
||||
fb.draw_line(Color::Pink, 50).await;
|
||||
fb.draw_line(Color::Cyan, 25).await;
|
||||
}
|
||||
// this branch id for ui_level 1, which is also the default if an
|
||||
// unknown value is used
|
||||
_ => {}
|
||||
};
|
||||
fb.draw_line(display_color, 2);
|
||||
sleep(Duration::from_millis(1000));
|
||||
fb.draw_line(display_color, 2).await;
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -11,6 +12,7 @@ const FB_PATH: &str = "/dev/fb0";
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Framebuffer;
|
||||
|
||||
#[async_trait]
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
// TODO actually poll for this, maybe w/ fbset?
|
||||
@@ -20,16 +22,16 @@ impl GenericFramebuffer for Framebuffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (*b as u16) >> 3;
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (b as u16) >> 3;
|
||||
raw_buffer.extend(rgb565.to_le_bytes());
|
||||
}
|
||||
|
||||
std::fs::write(FB_PATH, &raw_buffer).unwrap();
|
||||
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use std::fs::write;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config;
|
||||
@@ -18,12 +16,12 @@ macro_rules! led {
|
||||
($l:expr) => {{ format!("/sys/class/leds/led:{}/blink", $l) }};
|
||||
}
|
||||
|
||||
fn start_blinking(path: String) {
|
||||
write(&path, "1").ok();
|
||||
async fn start_blinking(path: String) {
|
||||
tokio::fs::write(&path, "1").await.ok();
|
||||
}
|
||||
|
||||
fn stop_blinking(path: String) {
|
||||
write(&path, "0").ok();
|
||||
async fn stop_blinking(path: String) {
|
||||
tokio::fs::write(&path, "0").await.ok();
|
||||
}
|
||||
|
||||
pub fn update_ui(
|
||||
@@ -37,7 +35,7 @@ pub fn update_ui(
|
||||
info!("Invisible mode, not spawning UI.");
|
||||
invisible = true;
|
||||
}
|
||||
task_tracker.spawn_blocking(move || {
|
||||
task_tracker.spawn(async move {
|
||||
let mut state = DisplayState::Recording;
|
||||
let mut last_state = DisplayState::Paused;
|
||||
|
||||
@@ -56,28 +54,28 @@ pub fn update_ui(
|
||||
Err(e) => error!("error receiving ui update message: {e}"),
|
||||
};
|
||||
if invisible || state == last_state {
|
||||
sleep(Duration::from_secs(1));
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
match state {
|
||||
DisplayState::Paused => {
|
||||
stop_blinking(led!("signal_blue"));
|
||||
stop_blinking(led!("signal_red"));
|
||||
start_blinking(led!("wlan_white"));
|
||||
stop_blinking(led!("signal_blue")).await;
|
||||
stop_blinking(led!("signal_red")).await;
|
||||
start_blinking(led!("wlan_white")).await;
|
||||
}
|
||||
DisplayState::Recording => {
|
||||
stop_blinking(led!("wlan_white"));
|
||||
stop_blinking(led!("signal_red"));
|
||||
start_blinking(led!("signal_blue"));
|
||||
stop_blinking(led!("wlan_white")).await;
|
||||
stop_blinking(led!("signal_red")).await;
|
||||
start_blinking(led!("signal_blue")).await;
|
||||
}
|
||||
DisplayState::WarningDetected => {
|
||||
stop_blinking(led!("wlan_white"));
|
||||
stop_blinking(led!("signal_blue"));
|
||||
start_blinking(led!("signal_red"));
|
||||
stop_blinking(led!("wlan_white")).await;
|
||||
stop_blinking(led!("signal_blue")).await;
|
||||
start_blinking(led!("signal_red")).await;
|
||||
}
|
||||
}
|
||||
last_state = state;
|
||||
sleep(Duration::from_secs(1));
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ pub fn update_ui(
|
||||
info!("Invisible mode, not spawning UI.");
|
||||
}
|
||||
|
||||
// Since this is a one-time check at startup, using sync is acceptable
|
||||
// The alternative would be to make the entire initialization async
|
||||
if fs::exists(tplink_onebit::OLED_PATH).unwrap_or_default() {
|
||||
info!("detected one-bit display");
|
||||
tplink_onebit::update_ui(task_tracker, config, ui_shutdown_rx, ui_update_rx)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use async_trait::async_trait;
|
||||
use std::os::fd::AsRawFd;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
@@ -24,6 +25,7 @@ struct fb_fillrect {
|
||||
rop: u32,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
// TODO actually poll for this, maybe w/ fbset?
|
||||
@@ -33,12 +35,12 @@ impl GenericFramebuffer for Framebuffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||
// for how to write to the buffer, consult M7350v5_en_gpl/bootable/recovery/recovery_color_oled.c
|
||||
let dimensions = self.dimensions();
|
||||
let width = dimensions.width;
|
||||
let height = buffer.len() as u32 / width;
|
||||
let mut f = File::options().write(true).open(FB_PATH).unwrap();
|
||||
let mut f = OpenOptions::new().write(true).open(FB_PATH).await.unwrap();
|
||||
let mut arg = fb_fillrect {
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
@@ -50,15 +52,16 @@ impl GenericFramebuffer for Framebuffer {
|
||||
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (*b as u16) >> 3;
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (b as u16) >> 3;
|
||||
// note: big-endian!
|
||||
raw_buffer.extend(rgb565.to_be_bytes());
|
||||
}
|
||||
|
||||
f.write_all(&raw_buffer).unwrap();
|
||||
f.write_all(&raw_buffer).await.unwrap();
|
||||
|
||||
// ioctl is a synchronous operation, but it's fast enough that it shouldn't block
|
||||
unsafe {
|
||||
let res = libc::ioctl(
|
||||
f.as_raw_fd(),
|
||||
|
||||
@@ -10,8 +10,6 @@ use tokio::sync::oneshot;
|
||||
use tokio::sync::oneshot::error::TryRecvError;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use std::fs;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
pub const OLED_PATH: &str = "/sys/class/display/oled/oled_buffer";
|
||||
@@ -122,7 +120,7 @@ pub fn update_ui(
|
||||
info!("Invisible mode, not spawning UI.");
|
||||
}
|
||||
|
||||
task_tracker.spawn_blocking(move || {
|
||||
task_tracker.spawn(async move {
|
||||
let mut pixels = STATUS_SMILING;
|
||||
|
||||
loop {
|
||||
@@ -148,12 +146,12 @@ pub fn update_ui(
|
||||
// we write the status every second because it may have been overwritten through menu
|
||||
// navigation.
|
||||
if display_level != 0 {
|
||||
if let Err(e) = fs::write(OLED_PATH, pixels) {
|
||||
if let Err(e) = tokio::fs::write(OLED_PATH, pixels).await {
|
||||
error!("failed to write to display: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(1000));
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
/// Display support for the Wingtech CT2MHS01 hotspot.
|
||||
///
|
||||
/// Tested on (from `/etc/wt_version`):
|
||||
/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP
|
||||
/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55
|
||||
/// WT_HARDWARE_VERSION=89323_1_20
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -17,6 +18,7 @@ const FB_PATH: &str = "/dev/fb0";
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Framebuffer;
|
||||
|
||||
#[async_trait]
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
Dimensions {
|
||||
@@ -25,16 +27,16 @@ impl GenericFramebuffer for Framebuffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) {
|
||||
async fn write_buffer(&mut self, buffer: Vec<(u8, u8, u8)>) {
|
||||
let mut raw_buffer = Vec::new();
|
||||
for (r, g, b) in buffer {
|
||||
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (*g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (*b as u16) >> 3;
|
||||
let mut rgb565: u16 = (r as u16 & 0b11111000) << 8;
|
||||
rgb565 |= (g as u16 & 0b11111100) << 3;
|
||||
rgb565 |= (b as u16) >> 3;
|
||||
raw_buffer.extend(rgb565.to_le_bytes());
|
||||
}
|
||||
|
||||
std::fs::write(FB_PATH, &raw_buffer).unwrap();
|
||||
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -182,7 +182,7 @@ fn run_shutdown_thread(
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<(), RayhunterError> {
|
||||
env_logger::init();
|
||||
|
||||
@@ -229,6 +229,7 @@ async fn run_with_config(
|
||||
&task_tracker,
|
||||
dev,
|
||||
diag_rx,
|
||||
diag_tx.clone(),
|
||||
ui_update_tx.clone(),
|
||||
qmdl_store_lock.clone(),
|
||||
analysis_tx.clone(),
|
||||
@@ -284,7 +285,6 @@ async fn run_with_config(
|
||||
config,
|
||||
qmdl_store_lock: qmdl_store_lock.clone(),
|
||||
diag_device_ctrl_sender: diag_tx,
|
||||
ui_update_sender: ui_update_tx,
|
||||
analysis_status_lock,
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
|
||||
|
||||
+14
-12
@@ -56,12 +56,6 @@ pub struct ManifestEntry {
|
||||
pub arch: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum EntryType {
|
||||
Current,
|
||||
Past,
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
fn new() -> Self {
|
||||
let now = Local::now();
|
||||
@@ -347,23 +341,31 @@ impl RecordingStore {
|
||||
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||
}
|
||||
|
||||
pub async fn delete_entry(&mut self, name: &str) -> Result<EntryType, RecordingStoreError> {
|
||||
pub fn is_current_entry(&self, name: &str) -> bool {
|
||||
match self.current_entry {
|
||||
Some(idx) => match self.manifest.entries.get(idx) {
|
||||
Some(entry) => entry.name == name,
|
||||
None => false,
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_entry(&mut self, name: &str) -> Result<(), RecordingStoreError> {
|
||||
let entry_to_delete_idx = self
|
||||
.manifest
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| entry.name == name)
|
||||
.ok_or(RecordingStoreError::NoSuchEntryError)?;
|
||||
let is_current = match self.current_entry {
|
||||
match self.current_entry {
|
||||
Some(current_entry) if current_entry == entry_to_delete_idx => {
|
||||
self.close_current_entry().await?;
|
||||
EntryType::Current
|
||||
}
|
||||
Some(current_entry) => {
|
||||
self.current_entry = Some(current_entry - 1);
|
||||
EntryType::Past
|
||||
}
|
||||
None => EntryType::Past,
|
||||
None => {}
|
||||
};
|
||||
let entry_to_delete = self.manifest.entries.remove(entry_to_delete_idx);
|
||||
self.write_manifest().await?;
|
||||
@@ -375,7 +377,7 @@ impl RecordingStore {
|
||||
remove_file_if_exists(&analysis_filepath)
|
||||
.await
|
||||
.map_err(RecordingStoreError::DeleteFileError)?;
|
||||
Ok(is_current)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_entries(&mut self) -> Result<(), RecordingStoreError> {
|
||||
|
||||
@@ -18,18 +18,17 @@ use tokio::sync::{RwLock, oneshot};
|
||||
use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::DiagDeviceCtrlMessage;
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||
use crate::config::Config;
|
||||
use crate::pcap::generate_pcap_data;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::{DiagDeviceCtrlMessage, display};
|
||||
|
||||
pub struct ServerState {
|
||||
pub config_path: String,
|
||||
pub config: Config,
|
||||
pub qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
pub diag_device_ctrl_sender: Sender<DiagDeviceCtrlMessage>,
|
||||
pub ui_update_sender: Sender<display::DisplayState>,
|
||||
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
||||
@@ -293,7 +292,6 @@ mod tests {
|
||||
store_lock: Arc<RwLock<crate::qmdl_store::RecordingStore>>,
|
||||
) -> Arc<ServerState> {
|
||||
let (tx, _rx) = tokio::sync::mpsc::channel(1);
|
||||
let (ui_tx, _ui_rx) = tokio::sync::mpsc::channel(1);
|
||||
let (analysis_tx, _analysis_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
let analysis_status = {
|
||||
@@ -306,7 +304,6 @@ mod tests {
|
||||
config: Config::default(),
|
||||
qmdl_store_lock: store_lock,
|
||||
diag_device_ctrl_sender: tx,
|
||||
ui_update_sender: ui_tx,
|
||||
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_tx: Arc::new(RwLock::new(None)),
|
||||
|
||||
@@ -33,45 +33,49 @@
|
||||
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
|
||||
<p>Nothing to show!</p>
|
||||
{:else}
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
<th class="p-2">Timestamp</th>
|
||||
<th class="p-2">Heuristic</th>
|
||||
<th class="p-2">Warning</th>
|
||||
<th class="p-2">Severity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each report.rows as row}
|
||||
{#if row.type === AnalysisRowType.Analysis}
|
||||
{@const parsed_date = new Date(row.packet_timestamp)}
|
||||
{#each row.events.filter((e) => e !== null) as event, i}
|
||||
{@const analyzer = analyzers[i]}
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
{#if event.type === EventType.Warning}
|
||||
{@const severity = ['Low', 'Medium', 'High'][event.severity]}
|
||||
{@const severity_class = [
|
||||
'bg-red-200',
|
||||
'bg-red-400',
|
||||
'bg-red-600',
|
||||
][event.severity]}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2 {severity_class} text-center">{severity}</td>
|
||||
{:else if event.type === EventType.Informational}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2">Info</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="overflow-x-scroll">
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
<th class="p-2">Timestamp</th>
|
||||
<th class="p-2">Heuristic</th>
|
||||
<th class="p-2">Warning</th>
|
||||
<th class="p-2">Severity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each report.rows as row}
|
||||
{#if row.type === AnalysisRowType.Analysis}
|
||||
{@const parsed_date = new Date(row.packet_timestamp)}
|
||||
{#each row.events.filter((e) => e !== null) as event, i}
|
||||
{@const analyzer = analyzers[i]}
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
{#if event.type === EventType.Warning}
|
||||
{@const severity = ['Low', 'Medium', 'High'][
|
||||
event.severity
|
||||
]}
|
||||
{@const severity_class = [
|
||||
'bg-red-200',
|
||||
'bg-red-400',
|
||||
'bg-red-600',
|
||||
][event.severity]}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2 {severity_class} text-center">{severity}</td>
|
||||
{:else if event.type === EventType.Informational}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2">Info</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if report.statistics.num_skipped_packets > 0}
|
||||
@@ -81,21 +85,23 @@
|
||||
These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a
|
||||
problem.
|
||||
</p>
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
<th scope="col" class="p-2">Total Msgs Affected</th>
|
||||
<th scope="col">Reason/Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each skipped_messages.entries() as [message, count]}
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
<td class="text-center">{count}</td>
|
||||
<td>{message}</td>
|
||||
<div class="overflow-x-scroll">
|
||||
<table class="table-auto text-left">
|
||||
<thead class="p-2">
|
||||
<tr class="bg-gray-300">
|
||||
<th scope="col" class="p-2">Total Msgs Affected</th>
|
||||
<th scope="col">Reason/Error</th>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each skipped_messages.entries() as [message, count]}
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
<td class="text-center">{count}</td>
|
||||
<td>{message}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md flex flex-row"
|
||||
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row"
|
||||
onclick={confirmDelete}
|
||||
aria-label="delete"
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<button
|
||||
class="flex flex-row {full_button
|
||||
? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md'
|
||||
? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md'
|
||||
: 'text-blue-600 underline'}"
|
||||
onclick={download}
|
||||
>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1"
|
||||
class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1 overflow-x-scroll overflow-y-hidden"
|
||||
>
|
||||
{#if current}
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
@@ -78,7 +78,7 @@
|
||||
'N/A'}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between lg:justify-end gap-2 mt-2">
|
||||
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-scroll">
|
||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
|
||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
|
||||
<DownloadLink url={entry.get_zip_url()} text="zip" full_button />
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
const recording_button_classes =
|
||||
'text-white font-bold py-2 px-4 rounded-md flex flex-row gap-1';
|
||||
'text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row gap-1';
|
||||
const stop_recording_classes = `${recording_button_classes} bg-red-500 opacity-50 cursor-not-allowed`;
|
||||
const start_recording_classes = `${recording_button_classes} bg-blue-500 opacity-50 cursor-not-allowed`;
|
||||
</script>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
+16
-2
@@ -1,5 +1,19 @@
|
||||
# Configuration
|
||||
|
||||
Rayhunter can be configured by editing `/data/rayhunter/config.toml` on the device. You can obtain a shell on [orbic](./orbic.md#obtaining-a-shell) and [tplink](./tplink-m7350.md#obtaining-a-shell) and edit the file manually. In future versions the web UI will allow you to edit the config as well.
|
||||
Rayhunter can be configured through web user interface or by editing `/data/rayhunter/config.toml` on the device.
|
||||
|
||||
View the [default configuration file on GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).
|
||||

|
||||
|
||||
Through web UI you can set:
|
||||
- **Device UI Level**, which defines what Rayhunter shows on device's built-in screen. *Device UI Level* could be:
|
||||
- *Invisible mode*: Rayhunter does not show anything on the built-in screen
|
||||
- *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
|
||||
- *Demo mode (orca gif)*, which shows image of orca fish *and* colored line
|
||||
- *EFF logo*, which shows EFF logo and *and* colored line.
|
||||
- **Device Input Mode**, which defines behaviour of built-in power button of the device. *Device Input Mode* could be:
|
||||
- *Disable button control*: built-in power button of the device is not used by Rayhunter;
|
||||
- *Double-tap power button to start/stop recording*: double clicking on a built-in power button of the device stops and immediatelly restarts the recording. This could be useful if Rayhunter's heuristichs 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.
|
||||
- **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.
|
||||
- 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 behaviour 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 new release may reduce false positives in existing heuristics as well.
|
||||
|
||||
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 a GitHub](https://github.com/EFForg/rayhunter/blob/main/dist/config.toml.in).
|
||||
|
||||
+4
-4
@@ -25,15 +25,15 @@ If you want to use a non-Verizon SIM card you will probably need an unlocked dev
|
||||
Make sure USB tethering is also enabled in the Orbic's UI, and then run the following commands:
|
||||
|
||||
```sh
|
||||
installer util shell "echo 9 > /usrdata/mode.cfg"
|
||||
installer util shell reboot
|
||||
./installer util shell "echo 9 > /usrdata/mode.cfg"
|
||||
./installer util shell reboot
|
||||
```
|
||||
|
||||
To disable tethering again:
|
||||
|
||||
```sh
|
||||
installer util shell "echo 3 > /usrdata/mode.cfg"
|
||||
installer util shell reboot
|
||||
./installer util shell "echo 3 > /usrdata/mode.cfg"
|
||||
./installer util shell reboot
|
||||
```
|
||||
|
||||
See `/data/usb/boot_hsusb_composition` for a list of USB modes and Android USB gadget settings.
|
||||
|
||||
+7
-19
@@ -1,24 +1,12 @@
|
||||
# Heuristics
|
||||
|
||||
Rayhunter includes several analyzers to detect potential IMSI catcher activity. These can be enabled and disabled in your [config.toml](./configuration.md) file.
|
||||
Rayhunter includes several analyzers to detect potential IMSI catcher activity. These can be enabled and disabled in your [configuration](./configuration.md) file.
|
||||
|
||||
## Available Analyzers
|
||||
|
||||
- **IMSI Requested**: Tests whether the eNodeB sends an IMSI Identity Request NAS message. This
|
||||
can sometimes happen under normal circumstances when the network doesn't already have a TMSI
|
||||
(Temporary Mobile Subscriber ID or GUTI in 5G terminology) for your device. This most often
|
||||
happens when you first turn the device on, especially after it has been off for a long time or
|
||||
if you are in an area where ther is absolutely no connection to your service provider. This can
|
||||
also happen if you leave your device on while on an airplane and it suddenly connects to a new
|
||||
tower after being disconnected for a long time.
|
||||
However, if you get this warning at a time when you have been steadily connected to towers and the device has been on for a while it can be treated as suspcious.
|
||||
- **Connection Release/Redirected Carrier 2G Downgrade**: Tests if a cell
|
||||
releases our connection and redirects us to a 2G cell. This heuristic only
|
||||
makes sense in the US or other countries where there are no more operating 2G base stations.
|
||||
Users in contries where 2G is still in service (such as most of EU) may want to disable it.
|
||||
See https://en.wikipedia.org/wiki/2G#Past_2G_networks for information about your country.
|
||||
- **LTE SIB6/7 Downgrade**: Tests for LTE cells broadcasting a SIB type 6 and 7
|
||||
which include 2G/3G frequencies with higher priorities
|
||||
- **Null Cipher**: Tests whether the cell suggests using a null cipher (EEA0) in the RRC layer.
|
||||
- **NAS Null Cipher**: Tests whether the security mode command at the NAS layer suggests using a null cipher (EEA0). This would usually only happen after a UE has successfully authenticated with the MME but still it shouldn't happen at all, this could be indicative of an attack though using SS7 to get key material from the HLR of the UE for a succesful authentication. It could also indicate an IMSI catcher which is connected to the mobile network MME and HLR through cooperation between government and telco. Or it could be a false positive if the telco is intending to use null ciphers (if encryption is illegal or something.)
|
||||
- **Incomplete SIB**: Tests whether the SIB1 message contains a complete SIB chain (SIB3, SIB5, etc.) A legitimate SIB1 should contain timing information for at least 2 additional sibs (sib3, 4, and 5 being the most common) but a fake base station will often not bother to send additional SIBs beyond 1 and 2. On its own this might just be a misconfigured base station (though we have only seen it in the wild under suspicious circumstances) but combined with other heuristics such as **ISMI Requested** detection it should be considered a strong indicator of malicious activity.
|
||||
- **IMSI Requested**: Tests whether the eNodeB sends an IMSI Identity Request NAS message. This can sometimes happen under normal circumstances when the network doesn't already have a TMSI (Temporary Mobile Subscriber ID or GUTI in 5G terminology) for your device. This most often happens when you first turn the device on, especially after it has been off for a long time or if you are in an area where there is absolutely no connection to your service provider. This can also happen if you leave your device on while on an airplane and it suddenly connects to a new tower after being disconnected for a long time. However, if you get this warning at a time when you have been steadily connected to towers and the device has been on for a while it can be treated as suspcious.
|
||||
- **Connection Release/Redirected Carrier 2G Downgrade**: Tests if a cell releases our connection and redirects us to a 2G cell. This heuristic mostly makes sense in the US or other countries where there are no more operating 2G base stations. In countries where 2G is still in service (such as most of EU), this heuristics may trigger a lot of false positives, so you may want to disable it. However it should be noted that many IMSI Catchers operate in a such way that they downgrade connection to 2G and also that this heuristics has been vastly improved to reduce false positive warnings. See [Wikipedia page on past 2G networks](https://en.wikipedia.org/wiki/2G#Past_2G_networks) for information about your country.
|
||||
- **LTE SIB6/7 Downgrade**: Tests for LTE cells broadcasting a SIB type 6 and 7 messages which include 2G/3G frequencies with higher priorities.
|
||||
- **Null Cipher**: Tests whether the cell suggests using a null cipher (EEA0) in the RRC layer (that means that encryption between your mobile device and base staation is turned off).
|
||||
- **NAS Null Cipher**: Tests whether the security mode command at the NAS layer suggests using a null cipher (EEA0). This would usually only happen after a UE has successfully authenticated with the MME but still it shouldn't happen at all. This could be indicative of an attack though using SS7 to get key material from the HLR of the UE for a succesful authentication. It could also indicate an IMSI catcher which is connected to the mobile network MME and HLR through cooperation between government and telecom provider. Or it could be a false positive if the telecom provider is intending to use null ciphers (if encryption is illegal or they have some misconfiguration of the network), however this should be very rare case.
|
||||
- **Incomplete SIB**: Tests whether the SIB1 message contains a complete SIB chain (SIB3, SIB5, etc.) A legitimate SIB1 mesage should contain timing information for at least 2 additional sibs (sib3, 4, and 5 being the most common) but a fake base station will often not bother to send additional SIBs beyond 1 and 2. On its own this might just be a misconfigured base station (though we have only seen it in the wild under suspicious circumstances) but combined with other heuristics such as **ISMI Requested** detection it should be considered a strong indicator of malicious activity.
|
||||
|
||||
+4
-2
@@ -35,12 +35,14 @@ The modem is fully capable of running Rayhunter, but lacks both a screen and a n
|
||||
Note that the Quectel EG25-G does not support LTE band 48 (CBRS 3500MHz), used in the US for unlicensed 4G/5G connectivity.
|
||||
|
||||
## Installing
|
||||
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:
|
||||
|
||||
```sh
|
||||
./installer pinephone
|
||||
```
|
||||
|
||||
## Accessing rayhunter
|
||||
Because the modem does not have its own display or network interface, rayhunter is only accessible on the pinephone by forwarding tcp over adb.
|
||||
## Accessing Rayhunter
|
||||
Because the modem does not have its own display or network interface, Rayhunter is only accessible on the pinephone by forwarding tcp over adb.
|
||||
|
||||
```sh
|
||||
adb forward tcp:8080 tcp:8080
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
Once installed, Rayhunter will run automatically whenever your device is running. You'll see a green line on top of the device's display to indicate that it's running and recording. [The line will turn red](./faq.md#red) once a potential IMSI catcher has been found, until the device is rebooted or a new recording is started through the web UI.
|
||||
|
||||

|
||||

|
||||
|
||||
It also serves a web UI that provides some basic controls, such as being able to start/stop recordings, download captures, delete captures, and view heuristic analyses of captures.
|
||||
|
||||
@@ -28,6 +28,4 @@ You can access this UI in one of two ways:
|
||||
|
||||
## Key shortcuts
|
||||
|
||||
As of 0.3.3, you can start a new recording by double-tapping the power button. Any current recording will be stopped and a new recording will be started, resetting the red line as well.
|
||||
|
||||
**This feature is disabled by default since 0.4.0** and needs to be enabled through [configuration](./configuration.md).
|
||||
As of Rayhunter verion 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).
|
||||
|
||||
@@ -67,3 +67,38 @@ WT_HARDWARE_VERSION=89323_1_20
|
||||
```
|
||||
|
||||
Please consider sharing the contents of your device's /etc/wt_version file here.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### My hotspot won't turn on after rebooting when installing over WiFi
|
||||
|
||||
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.
|
||||
|
||||
You do not need to run the installer again.
|
||||
|
||||
You'll likely see the following messages, where the installer is stuck at `Testing rayhunter ... `.
|
||||
|
||||
```sh
|
||||
Starting telnet ... ok
|
||||
Connecting via telnet to 192.168.1.1 ... ok
|
||||
Sending file /data/rayhunter/config.toml ... ok
|
||||
Sending file /data/rayhunter/rayhunter-daemon ... ok
|
||||
Sending file /etc/init.d/rayhunter_daemon ... ok
|
||||
Rebooting device and waiting 30 seconds for it to start up.
|
||||
Testing rayhunter ...
|
||||
```
|
||||
|
||||
If you eventually see:
|
||||
|
||||
```sh
|
||||
Testing rayhunter ...
|
||||
Failed to install rayhunter on the Wingtech CT2MHS01
|
||||
|
||||
Caused by:
|
||||
0: error sending request for url (http://192.168.1.1:8080/index.html)
|
||||
1: client error (Connect)
|
||||
2: tcp connect error: Network is unreachable (os error 101)
|
||||
3: Network is unreachable (os error 101)
|
||||
```
|
||||
|
||||
Make sure your computer is connected to the hotspot's wifi network.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::io::stdin;
|
||||
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
@@ -30,6 +33,13 @@ On macOS or windows this might be caused by another program using the Orbic.
|
||||
Please close any program that might be using your Orbic.
|
||||
If you have adb installed you may need to kill the adb daemon"#;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const WINDOWS_WARNING: &str = r#""WINDOWS IS NOT FULLY SUPPORTED
|
||||
|
||||
THIS MAY BRICK YOUR DEVICE
|
||||
|
||||
PLEASE INSTALL FROM MACOS OR LINUX INSTEAD IF POSSIBLE"#;
|
||||
|
||||
const VENDOR_ID: u16 = 0x05c6;
|
||||
const PRODUCT_ID: u16 = 0xf601;
|
||||
|
||||
@@ -41,7 +51,25 @@ const RNDIS_INTERFACE: u8 = 0;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const RNDIS_INTERFACE: u8 = 1;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn confirm() -> Result<bool> {
|
||||
println!("{}", WINDOWS_WARNING);
|
||||
echo!("Do you wish to proceed? Enter 'yes' to install> ");
|
||||
let mut input = String::new();
|
||||
stdin().read_line(&mut input)?;
|
||||
Ok(input.trim() == "yes")
|
||||
}
|
||||
|
||||
pub async fn install() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let confirmation = confirm().await?;
|
||||
if confirmation != true {
|
||||
println!("Install aborted. Your device has not been modified.");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let mut adb_device = force_debug_mode().await?;
|
||||
echo!("Installing rootshell... ");
|
||||
setup_rootshell(&mut adb_device).await?;
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Res
|
||||
.context("login did not return a token in response")?;
|
||||
|
||||
let command = client.post(&qcmap_web_cgi_endpoint)
|
||||
.body(format!("page=setFWMacFilter&cmd=add&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}"))
|
||||
.body(format!("page=setFWMacFilter&cmd=del&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}"))
|
||||
.send()
|
||||
.await?;
|
||||
if command.status() != 200 {
|
||||
@@ -135,7 +135,7 @@ async fn wingtech_run_install(admin_ip: String, admin_password: String) -> Resul
|
||||
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
|
||||
|
||||
println!("Rebooting device and waiting 30 seconds for it to start up.");
|
||||
telnet_send_command(addr, "reboot", "exit code 0").await?;
|
||||
telnet_send_command(addr, "shutdown -r -t 1 now", "exit code 0").await?;
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
echo!("Testing rayhunter ... ");
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
edition = "2024"
|
||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
pushd daemon/web
|
||||
npm run build
|
||||
popd
|
||||
cargo build --profile firmware --bin rayhunter-daemon --target="armv7-unknown-linux-musleabihf" #--features debug
|
||||
cargo build --profile firmware-devel --bin rayhunter-daemon --target="armv7-unknown-linux-musleabihf" #--features debug
|
||||
adb shell '/bin/rootshell -c "/etc/init.d/rayhunter_daemon stop"'
|
||||
adb push target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
||||
adb push target/armv7-unknown-linux-musleabihf/firmware-devel/rayhunter-daemon \
|
||||
/data/rayhunter/rayhunter-daemon
|
||||
echo "rebooting the device..."
|
||||
adb shell '/bin/rootshell -c "reboot"'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
Reference in New Issue
Block a user