Compare commits

..

1 Commits

Author SHA1 Message Date
Sashanoraa c2226071d2 Dynamically determine frame buffer dimensions and format
This commit also refactors a majority of the device specific frame
buffer code into common code into generic_framebuffer.
2025-07-30 22:46:26 -04:00
27 changed files with 555 additions and 575 deletions
-9
View File
@@ -1,9 +0,0 @@
# 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
-47
View File
@@ -1,47 +0,0 @@
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
+6 -6
View File
@@ -1489,7 +1489,7 @@ dependencies = [
[[package]]
name = "installer"
version = "0.5.1"
version = "0.5.0"
dependencies = [
"adb_client",
"aes",
@@ -2384,7 +2384,7 @@ dependencies = [
[[package]]
name = "rayhunter"
version = "0.5.1"
version = "0.5.0"
dependencies = [
"bytes",
"chrono",
@@ -2405,7 +2405,7 @@ dependencies = [
[[package]]
name = "rayhunter-check"
version = "0.5.1"
version = "0.5.0"
dependencies = [
"clap",
"futures",
@@ -2419,7 +2419,7 @@ dependencies = [
[[package]]
name = "rayhunter-daemon"
version = "0.5.1"
version = "0.5.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2537,7 +2537,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
[[package]]
name = "rootshell"
version = "0.5.1"
version = "0.5.0"
dependencies = [
"nix",
]
@@ -2919,7 +2919,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "telcom-parser"
version = "0.5.1"
version = "0.5.0"
dependencies = [
"asn1-codecs",
"asn1-compiler",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter-check"
version = "0.5.1"
version = "0.5.0"
edition = "2024"
[dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter-daemon"
version = "0.5.1"
version = "0.5.0"
edition = "2024"
[dependencies]
+123 -241
View File
@@ -1,4 +1,3 @@
use std::ops::DerefMut;
use std::pin::pin;
use std::sync::Arc;
@@ -10,261 +9,135 @@ use axum::response::{IntoResponse, Response};
use futures::{StreamExt, TryStreamExt};
use log::{debug, error, info, warn};
use rayhunter::analysis::analyzer::AnalyzerConfig;
use rayhunter::diag::{DataType, MessagesContainer};
use rayhunter::diag::DataType;
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::{RecordingStore, RecordingStoreError};
use crate::qmdl_store::{EntryType, 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 diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config);
qmdl_file_tx
.send(DiagDeviceCtrlMessage::StartRecording)
.await
.unwrap();
let mut maybe_analysis_writer = Some(AnalysisWriter::new(initial_analysis_file, &analyzer_config).await
.expect("failed to create analysis writer"));
loop {
tokio::select! {
msg = qmdl_file_rx.recv() => {
match msg {
Some(DiagDeviceCtrlMessage::StartRecording) => {
let mut qmdl_store = qmdl_store_lock.write().await;
diag_task.start(qmdl_store.deref_mut()).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}");
}
},
Some(DiagDeviceCtrlMessage::StopRecording) => {
let mut qmdl_store = qmdl_store_lock.write().await;
diag_task.stop(qmdl_store.deref_mut()).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}");
}
},
// None means all the Senders have been dropped, so it's
// time to go
Some(DiagDeviceCtrlMessage::Exit) | None => {
info!("Diag reader thread exiting...");
diag_task.stop_current_recording().await;
if let Some(analysis_writer) = maybe_analysis_writer {
analysis_writer.close().await.expect("failed to close analysis writer");
}
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) => {
let mut qmdl_store = qmdl_store_lock.write().await;
diag_task.process_container(qmdl_store.deref_mut(), container).await
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: {}");
}
}
},
Err(err) => {
error!("error reading diag device: {err}");
@@ -277,7 +150,6 @@ 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)> {
@@ -299,7 +171,6 @@ 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)> {
@@ -326,27 +197,8 @@ pub async fn delete_recording(
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::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())),
let mut qmdl_store = state.qmdl_store_lock.write().await;
match qmdl_store.delete_entry(&qmdl_name).await {
Err(RecordingStoreError::NoSuchEntryError) => Err((
StatusCode::BAD_REQUEST,
format!("no recording with name {qmdl_name}"),
@@ -355,6 +207,31 @@ 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()))
}
}
}
@@ -364,29 +241,34 @@ 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::DeleteAllEntries { response_tx })
.send(DiagDeviceCtrlMessage::StopRecording)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send delete all entries message: {e}"),
format!("couldn't send stop recording message: {e}"),
)
})?;
match response_rx.await.map_err(|e| {
let mut qmdl_store = state.qmdl_store_lock.write().await;
qmdl_store.delete_all_entries().await.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to receive delete all response: {e}"),
format!("couldn't delete all recordings: {e}"),
)
})? {
Ok(_) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't delete recordings: {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()))
}
pub async fn get_analysis_report(
+302
View File
@@ -1,7 +1,11 @@
use async_trait::async_trait;
use fb_utils::{determine_format, get_var_screeninfo};
use image::{AnimationDecoder, DynamicImage, codecs::gif::GifDecoder, imageops::FilterType};
use std::io::Cursor;
use std::os::fd::{AsFd, BorrowedFd};
use std::time::Duration;
use tokio::fs::File;
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
use crate::config;
use crate::display::DisplayState;
@@ -20,6 +24,20 @@ pub struct Dimensions {
pub width: u32,
}
#[derive(Copy, Clone, Debug)]
pub enum FbFormat {
ARGB888,
ABGR888,
RGB888,
BGR888,
RGB666,
RGB565,
BGR565,
RGB555,
BGR555,
RGB444,
}
#[allow(dead_code)]
#[derive(Copy, Clone)]
pub enum Color {
@@ -131,6 +149,109 @@ pub trait GenericFramebuffer: Send + 'static {
}
}
/// Attempt to determine the FB dimensions from FB vinfo.
pub fn read_fb_dimentions(fb: BorrowedFd<'_>) -> std::io::Result<Dimensions> {
let vinfo = get_var_screeninfo(fb)?;
Ok(Dimensions {
height: vinfo.yres,
width: vinfo.xres,
})
}
/// Attempt to determine the FBs format
///
/// Returns `Ok(None)` if the format cannot be determined
pub fn read_fb_format(fb: BorrowedFd<'_>) -> std::io::Result<Option<FbFormat>> {
let vinfo = get_var_screeninfo(fb)?;
Ok(determine_format(vinfo))
}
pub fn buffer_to_fb_format(
buffer: &Vec<(u8, u8, u8)>,
format: &FbFormat,
big_endian: bool,
) -> Vec<u8> {
let mut raw_buffer = Vec::new();
for (r, g, b) in buffer {
match format {
FbFormat::RGB565 => {
let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8;
rgb565 |= (*g as u16 & 0b11111100) << 3;
rgb565 |= (*b as u16) >> 3;
if big_endian {
raw_buffer.extend(rgb565.to_be_bytes());
} else {
raw_buffer.extend(rgb565.to_le_bytes());
}
}
other => panic!("This display uses a format we haven't implemneted yet {other:?}"),
}
}
raw_buffer
}
pub type CallBack = Box<dyn FnMut(&mut FbInner, &[(u8, u8, u8)]) + Send + 'static>;
pub struct FramebufferDevice {
data: FbInner,
pre_write_fn: Option<CallBack>,
post_write_fn: Option<CallBack>,
}
pub struct FbInner {
pub fd: File,
pub dims: Dimensions,
pub format: FbFormat,
}
impl FramebufferDevice {
pub fn new(
path: &str,
pre_write_fn: Option<CallBack>,
post_write_fn: Option<CallBack>,
) -> Self {
// This is done as a blocking call to prevent all of the UI init code from having to
// be made async, making it more verbose. This is a single syscall that would have been
// done via spawn_blocking anyway, and it's done once on startup.
let fb = std::fs::File::create(path).expect("Failed to open /dev/fb0");
let dims = read_fb_dimentions(fb.as_fd()).expect("Failed to read FB dimensions");
let format = read_fb_format(fb.as_fd())
.expect("Failed to read FB format")
.expect("FB retruned unexpected format");
Self {
data: FbInner {
fd: File::from_std(fb),
dims,
format,
},
pre_write_fn,
post_write_fn,
}
}
}
#[async_trait]
impl GenericFramebuffer for FramebufferDevice {
fn dimensions(&self) -> Dimensions {
self.data.dims
}
async fn write_buffer(
&mut self,
buffer: Vec<(u8, u8, u8)>, // rgb, row-wise, left-to-right, top-to-bottom
) {
if let Some(func) = self.pre_write_fn.as_mut() {
func(&mut self.data, &buffer);
}
let raw_buffer = buffer_to_fb_format(&buffer, &self.data.format, false);
self.data.fd.write_all(&raw_buffer).await.unwrap();
self.data.fd.rewind().await.unwrap();
if let Some(func) = self.post_write_fn.as_mut() {
func(&mut self.data, &buffer);
}
}
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
@@ -201,3 +322,184 @@ pub fn update_ui(
}
});
}
mod fb_utils {
use std::io::{Error, Result};
use std::os::fd::{AsRawFd, BorrowedFd};
use libc::ioctl;
use super::FbFormat;
const FBIOGET_VSCREENINFO: libc::c_ulong = 0x4600;
// const FBIOGET_FSCREENINFO: libc::c_ulong = 0x4602;
/// Bitfield which is a part of VarScreeninfo.
#[repr(C)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Bitfield {
pub offset: u32,
pub length: u32,
pub msb_right: u32,
}
/// Struct as defined in /usr/include/linux/fb.h
#[repr(C)]
#[derive(Clone, Debug, Default)]
pub struct VarScreeninfo {
pub xres: u32,
pub yres: u32,
pub xres_virtual: u32,
pub yres_virtual: u32,
pub xoffset: u32,
pub yoffset: u32,
pub bits_per_pixel: u32,
pub grayscale: u32,
pub red: Bitfield,
pub green: Bitfield,
pub blue: Bitfield,
pub transp: Bitfield,
pub nonstd: u32,
pub activate: u32,
pub height: u32,
pub width: u32,
pub accel_flags: u32,
pub pixclock: u32,
pub left_margin: u32,
pub right_margin: u32,
pub upper_margin: u32,
pub lower_margin: u32,
pub hsync_len: u32,
pub vsync_len: u32,
pub sync: u32,
pub vmode: u32,
pub rotate: u32,
pub colorspace: u32,
pub reserved: [u32; 4],
}
// /// Struct as defined in /usr/include/linux/fb.h
// /// Note: type is a keyword in Rust and therefore has been changed to fb_type.
// #[repr(C)]
// #[derive(Clone, Debug, Default)]
// pub struct FixScreeninfo {
// pub id: [u8; 16],
// pub smem_start: usize,
// pub smem_len: u32,
// pub fb_type: u32,
// pub type_aux: u32,
// pub visual: u32,
// pub xpanstep: u16,
// pub ypanstep: u16,
// pub ywrapstep: u16,
// pub line_length: u32,
// pub mmio_start: usize,
// pub mmio_len: u32,
// pub accel: u32,
// pub capabilities: u16,
// pub reserved: [u16; 2],
// }
// pub fn get_fix_screeninfo(fb: BorrowedFd<'_>) -> Result<FixScreeninfo> {
// let mut info: FixScreeninfo = Default::default();
// let result = unsafe { ioctl(fb.as_raw_fd(), FBIOGET_FSCREENINFO as _, &mut info) };
// match result {
// -1 => Err(Error::last_os_error()),
// _ => Ok(info),
// }
// }
pub fn get_var_screeninfo(fb: BorrowedFd<'_>) -> Result<VarScreeninfo> {
let mut info: VarScreeninfo = Default::default();
let result = unsafe { ioctl(fb.as_raw_fd(), FBIOGET_VSCREENINFO as _, &mut info) };
match result {
-1 => Err(Error::last_os_error()),
_ => Ok(info),
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct RgbaBitfield {
red: Bitfield,
green: Bitfield,
blue: Bitfield,
transp: Bitfield,
}
impl From<&VarScreeninfo> for RgbaBitfield {
fn from(value: &VarScreeninfo) -> Self {
Self {
red: value.red.clone(),
green: value.green.clone(),
blue: value.blue.clone(),
transp: value.transp.clone(),
}
}
}
type BitfieldShort = (u32, u32);
type FbInfoShort = (BitfieldShort, BitfieldShort, BitfieldShort, BitfieldShort);
const fn tuple_to_bitfield(v: BitfieldShort) -> Bitfield {
let (offset, length) = v;
// None of formats we support have msb_right set.
Bitfield {
offset,
length,
msb_right: 0,
}
}
/// Takes a tuple of 4 tuples `(r, g, b, a)`. Each color tuple is a tuple of `(offset, length)`.
const fn rgba_bitfield(v: FbInfoShort) -> RgbaBitfield {
let (r, g, b, a) = v;
RgbaBitfield {
red: tuple_to_bitfield(r),
green: tuple_to_bitfield(g),
blue: tuple_to_bitfield(b),
transp: tuple_to_bitfield(a),
}
}
// Logic borrowed from QT https://github.com/qt/qtbase/blob/498ae026e98ed181d1480fe5f6f2f1453a725e78/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp
const ARGB888: RgbaBitfield = rgba_bitfield(((16, 8), (8, 8), (0, 8), (24, 8)));
const ABGR888: RgbaBitfield = rgba_bitfield(((0, 8), (8, 8), (16, 8), (24, 8)));
const RGB888: RgbaBitfield = rgba_bitfield(((16, 8), (8, 8), (0, 8), (0, 0)));
const BGR888: RgbaBitfield = rgba_bitfield(((0, 8), (8, 8), (16, 8), (0, 0)));
const RGB666: RgbaBitfield = rgba_bitfield(((12, 6), (6, 6), (0, 6), (0, 0)));
const RGB565: RgbaBitfield = rgba_bitfield(((11, 5), (5, 6), (0, 5), (0, 0)));
const BGR565: RgbaBitfield = rgba_bitfield(((0, 5), (5, 6), (11, 5), (0, 0)));
const RGB555: RgbaBitfield = rgba_bitfield(((10, 5), (5, 5), (0, 5), (0, 0)));
const BGR555: RgbaBitfield = rgba_bitfield(((0, 5), (5, 5), (10, 5), (0, 0)));
const RGB444: RgbaBitfield = rgba_bitfield(((8, 4), (4, 4), (0, 4), (0, 0)));
fn determine_depth(vinfo: &VarScreeninfo) -> u32 {
let depth = vinfo.red.length + vinfo.green.length + vinfo.blue.length;
match vinfo.bits_per_pixel {
24 if depth == 0 => 24,
16 if depth == 0 => 16,
24 | 16 => depth,
v => v,
}
}
pub fn determine_format(vinfo: VarScreeninfo) -> Option<FbFormat> {
let rgba = RgbaBitfield::from(&vinfo);
let depth = determine_depth(&vinfo);
match (depth, rgba) {
(32, ARGB888) => Some(FbFormat::ARGB888),
(32, ABGR888) => Some(FbFormat::ABGR888),
(24, RGB888) => Some(FbFormat::RGB888),
(24, BGR888) => Some(FbFormat::BGR888),
(18, RGB666) => Some(FbFormat::RGB666),
(16, RGB565) => Some(FbFormat::RGB565),
(16, BGR565) => Some(FbFormat::BGR565),
(15, RGB555) => Some(FbFormat::RGB555),
(15, BGR555) => Some(FbFormat::BGR555),
(12, RGB444) => Some(FbFormat::RGB444),
_ => None,
}
}
}
+4 -29
View File
@@ -1,40 +1,15 @@
use crate::config;
use crate::display::DisplayState;
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
use async_trait::async_trait;
use crate::display::generic_framebuffer;
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
use super::generic_framebuffer::FramebufferDevice;
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?
Dimensions {
height: 128,
width: 128,
}
}
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;
raw_buffer.extend(rgb565.to_le_bytes());
}
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
}
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
@@ -44,7 +19,7 @@ pub fn update_ui(
generic_framebuffer::update_ui(
task_tracker,
config,
Framebuffer,
FramebufferDevice::new(FB_PATH, None, None),
ui_shutdown_rx,
ui_update_rx,
)
+24 -53
View File
@@ -1,19 +1,16 @@
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;
use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer};
use crate::display::generic_framebuffer;
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
const FB_PATH: &str = "/dev/fb0";
use super::generic_framebuffer::{FbInner, FramebufferDevice};
struct Framebuffer;
const FB_PATH: &str = "/dev/fb0";
#[repr(C)]
struct fb_fillrect {
@@ -25,54 +22,28 @@ struct fb_fillrect {
rop: u32,
}
#[async_trait]
impl GenericFramebuffer for Framebuffer {
fn dimensions(&self) -> Dimensions {
// TODO actually poll for this, maybe w/ fbset?
Dimensions {
height: 128,
width: 128,
}
}
fn update_display(fb: &mut FbInner, buffer: &[(u8, u8, u8)]) {
let width = fb.dims.width;
let height = buffer.len() as u32 / width;
let mut arg = fb_fillrect {
dx: 0,
dy: 0,
width,
height,
color: 0xffff, // not sure what this is
rop: 0,
};
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 = OpenOptions::new().write(true).open(FB_PATH).await.unwrap();
let mut arg = fb_fillrect {
dx: 0,
dy: 0,
width,
height,
color: 0xffff, // not sure what this is
rop: 0,
};
unsafe {
let res = libc::ioctl(
fb.fd.as_raw_fd(),
0x4619, // FBIORECT_DISPLAY
&mut arg as *mut _,
std::mem::size_of::<fb_fillrect>(),
);
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;
// note: big-endian!
raw_buffer.extend(rgb565.to_be_bytes());
}
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(),
0x4619, // FBIORECT_DISPLAY
&mut arg as *mut _,
std::mem::size_of::<fb_fillrect>(),
);
if res < 0 {
panic!("failed to send FBIORECT_DISPLAY ioctl, {res}");
}
if res < 0 {
panic!("failed to send FBIORECT_DISPLAY ioctl, {res}");
}
}
}
@@ -86,7 +57,7 @@ pub fn update_ui(
generic_framebuffer::update_ui(
task_tracker,
config,
Framebuffer,
FramebufferDevice::new(FB_PATH, None, Some(Box::new(update_display))),
ui_shutdown_rx,
ui_update_rx,
)
+6 -30
View File
@@ -1,45 +1,21 @@
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 async_trait::async_trait;
use crate::config;
use crate::display::DisplayState;
use crate::display::generic_framebuffer;
use tokio::sync::mpsc::Receiver;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
use super::generic_framebuffer::FramebufferDevice;
const FB_PATH: &str = "/dev/fb0";
#[derive(Copy, Clone, Default)]
struct Framebuffer;
#[async_trait]
impl GenericFramebuffer for Framebuffer {
fn dimensions(&self) -> Dimensions {
Dimensions {
height: 128,
width: 160,
}
}
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;
raw_buffer.extend(rgb565.to_le_bytes());
}
tokio::fs::write(FB_PATH, &raw_buffer).await.unwrap();
}
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
@@ -49,7 +25,7 @@ pub fn update_ui(
generic_framebuffer::update_ui(
task_tracker,
config,
Framebuffer,
FramebufferDevice::new(FB_PATH, None, None),
ui_shutdown_rx,
ui_update_rx,
)
+1 -1
View File
@@ -229,7 +229,6 @@ async fn run_with_config(
&task_tracker,
dev,
diag_rx,
diag_tx.clone(),
ui_update_tx.clone(),
qmdl_store_lock.clone(),
analysis_tx.clone(),
@@ -285,6 +284,7 @@ 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))),
+12 -14
View File
@@ -56,6 +56,12 @@ pub struct ManifestEntry {
pub arch: Option<String>,
}
#[derive(PartialEq, Eq)]
pub enum EntryType {
Current,
Past,
}
impl ManifestEntry {
fn new() -> Self {
let now = Local::now();
@@ -341,31 +347,23 @@ impl RecordingStore {
Some((entry_index, &self.manifest.entries[entry_index]))
}
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> {
pub async fn delete_entry(&mut self, name: &str) -> Result<EntryType, RecordingStoreError> {
let entry_to_delete_idx = self
.manifest
.entries
.iter()
.position(|entry| entry.name == name)
.ok_or(RecordingStoreError::NoSuchEntryError)?;
match self.current_entry {
let is_current = 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 => {}
None => EntryType::Past,
};
let entry_to_delete = self.manifest.entries.remove(entry_to_delete_idx);
self.write_manifest().await?;
@@ -377,7 +375,7 @@ impl RecordingStore {
remove_file_if_exists(&analysis_filepath)
.await
.map_err(RecordingStoreError::DeleteFileError)?;
Ok(())
Ok(is_current)
}
pub async fn delete_all_entries(&mut self) -> Result<(), RecordingStoreError> {
+4 -1
View File
@@ -18,17 +18,18 @@ 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<()>>>>,
@@ -292,6 +293,7 @@ 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 = {
@@ -304,6 +306,7 @@ 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,49 +33,45 @@
{#if report.statistics.num_warnings === 0 && report.statistics.num_informational_logs === 0}
<p>Nothing to show!</p>
{:else}
<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>
<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>
{/if}
</div>
{#if report.statistics.num_skipped_packets > 0}
@@ -85,23 +81,21 @@
These are due to a limitation or bug in Rayhunter's parser, and aren't ususally a
problem.
</p>
<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>
<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>
</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>
</tr>
{/each}
</tbody>
</table>
</div>
{/each}
</tbody>
</table>
</div>
{/if}
@@ -18,7 +18,7 @@
</script>
<button
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-2 sm:px-4 rounded-md flex flex-row"
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 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-2 sm:px-4 rounded-md'
? 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 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 overflow-x-scroll overflow-y-hidden"
class="{status_row_color} {status_border_color} drop-shadow p-4 flex flex-col gap-2 border rounded-md flex-1"
>
{#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-1 mt-2 overflow-x-scroll">
<div class="flex flex-row justify-between lg:justify-end gap-2 mt-2">
<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-2 sm:px-4 rounded-md flex flex-row gap-1';
'text-white font-bold py-2 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>
+4 -4
View File
@@ -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.
+2 -4
View File
@@ -35,14 +35,12 @@ 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
-35
View File
@@ -67,38 +67,3 @@ 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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "installer"
version = "0.5.1"
version = "0.5.0"
edition = "2024"
[dependencies]
-28
View File
@@ -1,6 +1,3 @@
#[cfg(target_os = "windows")]
use std::io::stdin;
use std::io::{ErrorKind, Write};
use std::path::Path;
use std::time::Duration;
@@ -33,13 +30,6 @@ 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;
@@ -51,25 +41,7 @@ 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?;
+2 -2
View File
@@ -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=del&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}"))
.body(format!("page=setFWMacFilter&cmd=add&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, "shutdown -r -t 1 now", "exit code 0").await?;
telnet_send_command(addr, "reboot", "exit code 0").await?;
sleep(Duration::from_secs(30)).await;
echo!("Testing rayhunter ... ");
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rayhunter"
version = "0.5.1"
version = "0.5.0"
edition = "2024"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rootshell"
version = "0.5.1"
version = "0.5.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "telcom-parser"
version = "0.5.1"
version = "0.5.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html