diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index d525b99..88a8e3d 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -3,6 +3,8 @@ name: Build Release on: push: branches: [main, "release-*"] + pull_request: + branches: [ "main" ] env: CARGO_TERM_COLOR: always @@ -36,7 +38,7 @@ jobs: name: rayhunter-check-${{ matrix.platform.os }} path: ./target/release/rayhunter-check if-no-files-found: error - build_rootshell_and_rayhunter: + build_rootshell: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -55,25 +57,44 @@ jobs: name: rootshell path: target/armv7-unknown-linux-gnueabihf/release/rootshell if-no-files-found: error + build_rayhunter: + strategy: + matrix: + device: + - name: tplink + - name: orbic + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: armv7-unknown-linux-gnueabihf + - name: Install cross-compilation dependencies + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: build-essential libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf + version: 1.0 - name: Build rayhunter-daemon (arm32) - run: cargo build --bin rayhunter-daemon --target armv7-unknown-linux-gnueabihf --release + run: cargo build --bin rayhunter-daemon --target armv7-unknown-linux-gnueabihf --release --no-default-features --features ${{ matrix.device.name }} - uses: actions/upload-artifact@v4 with: - name: rayhunter-daemon + name: rayhunter-daemon-${{ matrix.device.name }} path: target/armv7-unknown-linux-gnueabihf/release/rayhunter-daemon if-no-files-found: error build_release_zip: needs: - build_serial_and_check - - build_rootshell_and_rayhunter + - build_rootshell + - build_rayhunter runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 - name: Fix executable permissions on binaries - run: chmod +x serial-*/serial rayhunter-check-*/rayhunter-check rayhunter-daemon/rayhunter-daemon + run: chmod +x serial-*/serial rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon - name: Setup release directory - run: mv rayhunter-daemon/rayhunter-daemon rootshell/rootshell serial-* dist + run: mv rayhunter-daemon-* rootshell/rootshell serial-* dist - name: Archive release directory run: tar -cvf release.tar -C dist . # TODO: have this create a release directly diff --git a/.github/workflows/check-and-test.yml b/.github/workflows/check-and-test.yml index 052a588..e84e324 100644 --- a/.github/workflows/check-and-test.yml +++ b/.github/workflows/check-and-test.yml @@ -11,10 +11,16 @@ env: jobs: check_and_test: + strategy: + matrix: + device: + - name: tplink + - name: orbic + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Check - run: cargo check --verbose + run: cargo check --verbose --no-default-features --features=${{ matrix.device.name }} - name: Run tests - run: cargo test --verbose + run: cargo test --verbose --no-default-features --features=${{ matrix.device.name }} diff --git a/Cargo.lock b/Cargo.lock index 7b727e5..42d5423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1290,6 +1290,7 @@ dependencies = [ "futures-macro", "image", "include_dir", + "libc", "log", "mime_guess", "rayhunter", diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 93e0389..083f6aa 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -3,6 +3,13 @@ name = "rayhunter-daemon" version = "0.2.6" edition = "2021" +[features] +# These feature flags are mutually exclusive, and exactly one must be enabled. +orbic = ["rayhunter/orbic"] +tplink = ["rayhunter/tplink"] + +default = ["orbic"] + [[bin]] name = "rayhunter-daemon" path = "src/daemon.rs" @@ -19,6 +26,7 @@ tokio = { version = "1.35.1", features = ["full"] } axum = "0.7.3" futures-core = "0.3.30" thiserror = "1.0.52" +libc = "0.2.150" log = "0.4.20" env_logger = "0.10.1" tokio-util = { version = "0.7.10", features = ["rt"] } diff --git a/bin/src/daemon.rs b/bin/src/daemon.rs index 2ce836d..fdde56a 100644 --- a/bin/src/daemon.rs +++ b/bin/src/daemon.rs @@ -6,7 +6,7 @@ mod server; mod stats; mod qmdl_store; mod diag; -mod framebuffer; +mod display; mod dummy_analyzer; use crate::config::{parse_config, parse_args}; @@ -16,7 +16,6 @@ use crate::server::{ServerState, get_qmdl, serve_static}; use crate::pcap::get_pcap; use crate::stats::get_system_stats; use crate::error::RayhunterError; -use crate::framebuffer::Framebuffer; use analysis::{get_analysis_status, run_analysis_thread, start_analysis, AnalysisCtrlMessage, AnalysisStatus}; use axum::response::Redirect; @@ -27,17 +26,13 @@ use rayhunter::diag_device::DiagDevice; use axum::routing::{get, post}; use axum::Router; use stats::get_qmdl_manifest; -use tokio::sync::mpsc::{self, Sender, Receiver}; -use tokio::sync::oneshot::error::TryRecvError; +use tokio::sync::mpsc::{self, Sender}; use tokio::task::JoinHandle; use tokio_util::task::TaskTracker; use std::net::SocketAddr; -use std::thread::sleep; -use std::time::Duration; use tokio::net::TcpListener; use tokio::sync::{RwLock, oneshot}; use std::sync::Arc; -use include_dir::{include_dir, Dir}; // Runs the axum server, taking all the elements needed to build up our // ServerState and a oneshot Receiver that'll fire when it's time to shutdown @@ -146,69 +141,6 @@ fn run_ctrl_c_thread( }) } -fn update_ui(task_tracker: &TaskTracker, config: &config::Config, mut ui_shutdown_rx: oneshot::Receiver<()>, mut ui_update_rx: Receiver) -> JoinHandle<()> { - static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/"); - let mut display_color: framebuffer::Color565; - let display_level = config.ui_level; - if display_level == 0 { - info!("Invisible mode, not spawning UI."); - } - - if config.colorblind_mode { - display_color = framebuffer::Color565::Blue; - } else { - display_color = framebuffer::Color565::Green; - } - - task_tracker.spawn_blocking(move || { - let mut fb: Framebuffer = Framebuffer::new(); - // this feels wrong, is there a more rusty way to do this? - let mut img: Option<&[u8]> = None; - if display_level == 2 { - img = Some(IMAGE_DIR.get_file("orca.gif").expect("failed to read orca.gif").contents()); - } else if display_level == 3 { - img = Some(IMAGE_DIR.get_file("eff.png").expect("failed to read eff.png").contents()); - } - loop { - match ui_shutdown_rx.try_recv() { - Ok(_) => { - info!("received UI shutdown"); - break; - }, - Err(TryRecvError::Empty) => {}, - Err(e) => panic!("error receiving shutdown message: {e}") - } - match ui_update_rx.try_recv() { - Ok(state) => { - display_color = state.into(); - }, - Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}, - Err(e) => error!("error receiving framebuffer update message: {e}") - } - - match display_level { - 2 => { - fb.draw_gif(img.unwrap()); - }, - 3 => { - fb.draw_img(img.unwrap()) - }, - 128 => { - fb.draw_line(framebuffer::Color565::Cyan, 128); - fb.draw_line(framebuffer::Color565::Pink, 102); - fb.draw_line(framebuffer::Color565::White, 76); - fb.draw_line(framebuffer::Color565::Pink, 50); - fb.draw_line(framebuffer::Color565::Cyan, 25); - }, - _ => { // 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)); - } - }) -} #[tokio::main] async fn main() -> Result<(), RayhunterError> { @@ -224,7 +156,7 @@ async fn main() -> Result<(), RayhunterError> { let qmdl_store_lock = Arc::new(RwLock::new(init_qmdl_store(&config).await?)); let (tx, rx) = mpsc::channel::(1); - let (ui_update_tx, ui_update_rx) = mpsc::channel::(1); + let (ui_update_tx, ui_update_rx) = mpsc::channel::(1); let (analysis_tx, analysis_rx) = mpsc::channel::(5); let mut maybe_ui_shutdown_tx = None; if !config.debug_mode { @@ -238,7 +170,7 @@ async fn main() -> Result<(), RayhunterError> { info!("Starting Diag Thread"); run_diag_read_thread(&task_tracker, dev, rx, ui_update_tx.clone(), qmdl_store_lock.clone(), config.enable_dummy_analyzer); info!("Starting UI"); - update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); + display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); } let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>(); info!("create shutdown thread"); diff --git a/bin/src/diag.rs b/bin/src/diag.rs index b4b43e0..5b3899f 100644 --- a/bin/src/diag.rs +++ b/bin/src/diag.rs @@ -17,7 +17,7 @@ use tokio_util::io::ReaderStream; use tokio_util::task::TaskTracker; use futures::{StreamExt, TryStreamExt}; -use crate::framebuffer; +use crate::display; use crate::qmdl_store::{RecordingStore, RecordingStoreError}; use crate::server::ServerState; use crate::analysis::AnalysisWriter; @@ -32,7 +32,7 @@ pub fn run_diag_read_thread( task_tracker: &TaskTracker, mut dev: DiagDevice, mut qmdl_file_rx: Receiver, - ui_update_sender: Sender, + ui_update_sender: Sender, qmdl_store_lock: Arc>, enable_dummy_analyzer: bool, ) { @@ -99,7 +99,7 @@ pub fn run_diag_read_thread( let (analysis_file_len, heuristic_warning) = analysis_output; if heuristic_warning { info!("a heuristic triggered on this run!"); - ui_update_sender.send(framebuffer::DisplayState::WarningDetected).await + ui_update_sender.send(display::DisplayState::WarningDetected).await .expect("couldn't send ui update message: {}"); } let mut qmdl_store = qmdl_store_lock.write().await; @@ -131,9 +131,9 @@ pub async fn start_recording(State(state): State>) -> Result<(S .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send stop recording message: {}", e)))?; let display_state = if state.colorblind_mode { - framebuffer::DisplayState::RecordingCBM + display::DisplayState::RecordingCBM } else { - framebuffer::DisplayState::Recording + display::DisplayState::Recording }; state.ui_update_sender.send(display_state).await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't send ui update message: {}", e)))?; @@ -150,7 +150,7 @@ pub async fn stop_recording(State(state): State>) -> Result<(St .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't close current qmdl entry: {}", e)))?; 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(framebuffer::DisplayState::Paused).await + 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())) } @@ -170,7 +170,7 @@ pub async fn delete_recording( } 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(framebuffer::DisplayState::Paused).await + 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())) } @@ -184,7 +184,7 @@ pub async fn delete_all_recordings(State(state): State>) -> Res .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't delete all recordings: {}", e)))?; 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(framebuffer::DisplayState::Paused).await + 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())) } diff --git a/bin/src/display/generic_framebuffer.rs b/bin/src/display/generic_framebuffer.rs new file mode 100644 index 0000000..1c7666f --- /dev/null +++ b/bin/src/display/generic_framebuffer.rs @@ -0,0 +1,196 @@ +use image::{codecs::gif::GifDecoder, imageops::FilterType, AnimationDecoder, DynamicImage}; +use std::time::Duration; +use std::io::Cursor; + +use crate::config; +use crate::display::DisplayState; + +use log::{error, info}; +use tokio::sync::oneshot; +use tokio::sync::mpsc::Receiver; +use tokio_util::task::TaskTracker; +use tokio::sync::oneshot::error::TryRecvError; + +use std::thread::sleep; + +use include_dir::{include_dir, Dir}; + +#[derive(Copy, Clone)] +pub struct Dimensions { + pub height: u32, + pub width: u32, +} + + +#[allow(dead_code)] +#[derive(Copy, Clone)] +pub enum Color { + Red , + Green , + Blue , + White , + Black , + Cyan , + Yellow , + Pink, +} + +impl Color { + fn rgb(self) -> (u8, u8, u8) { + match self { + Color::Red => (0xff, 0, 0), + Color::Green => (0, 0xff, 0), + Color::Blue => (0, 0, 0xff), + Color::White => (0xff, 0xff, 0xff), + Color::Black => (0, 0, 0), + Color::Cyan => (0, 0xff, 0xff), + Color::Yellow => (0xff, 0xff, 0), + Color::Pink => (0xfe, 0x24, 0xff), + } + } +} + +impl From for Color{ + fn from(state: DisplayState) -> Self { + match state { + DisplayState::Paused => Color::White, + DisplayState::Recording => Color::Green, + DisplayState::RecordingCBM => Color::Blue, + DisplayState::WarningDetected => Color::Red, + } + } +} + +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 + ); + + fn write_dynamic_image(&mut self, img: DynamicImage) { + let dimensions = self.dimensions(); + let mut width = img.width(); + let mut height = img.height(); + let resized_img: DynamicImage; + if height > dimensions.height || + width > dimensions.width { + resized_img = img.resize( dimensions.width, dimensions.height, FilterType::CatmullRom); + width = dimensions.width.min(resized_img.width()); + height = dimensions.height.min(resized_img.height()); + } else { + resized_img = img; + } + let img_rgba8 = resized_img.as_rgba8().unwrap(); + let mut buf = Vec::new(); + for y in 0..height { + for x in 0..width { + let px = img_rgba8.get_pixel(x, y); + buf.push((px[0], px[1], px[2])); + } + } + + self.write_buffer( + &buf, + ); + } + + fn draw_gif(&mut self, img_buffer: &[u8]) { + // this is dumb and i'm sure there's a better way to loop this + 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)); + } + } + + fn draw_img(&mut self, img_buffer: &[u8]) { + let img = image::load_from_memory(img_buffer).unwrap(); + self.write_dynamic_image(img); + } + + fn draw_line(&mut self, color: Color, height: u32) { + let width = self.dimensions().width; + let px_num = height * width; + let mut buffer = Vec::new(); + for _ in 0..px_num { + buffer.push(color.rgb()); + } + + self.write_buffer(&buffer); + } +} + +pub fn update_ui( + task_tracker: &TaskTracker, + config: &config::Config, + mut fb: impl GenericFramebuffer, + mut ui_shutdown_rx: oneshot::Receiver<()>, + mut ui_update_rx: Receiver +) { + static IMAGE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static/images/"); + let mut display_color: Color; + let display_level = config.ui_level; + if display_level == 0 { + info!("Invisible mode, not spawning UI."); + } + + if config.colorblind_mode { + display_color = Color::Blue; + } else { + display_color = Color::Green; + } + + task_tracker.spawn_blocking(move || { + // this feels wrong, is there a more rusty way to do this? + let mut img: Option<&[u8]> = None; + if display_level == 2 { + img = Some(IMAGE_DIR.get_file("orca.gif").expect("failed to read orca.gif").contents()); + } else if display_level == 3 { + img = Some(IMAGE_DIR.get_file("eff.png").expect("failed to read eff.png").contents()); + } + loop { + match ui_shutdown_rx.try_recv() { + Ok(_) => { + info!("received UI shutdown"); + break; + }, + Err(TryRecvError::Empty) => {}, + Err(e) => panic!("error receiving shutdown message: {e}") + } + match ui_update_rx.try_recv() { + Ok(state) => { + display_color = state.into(); + }, + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}, + Err(e) => error!("error receiving framebuffer update message: {e}") + } + + match display_level { + 2 => { + fb.draw_gif(img.unwrap()); + }, + 3 => { + fb.draw_img(img.unwrap()) + }, + 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); + }, + _ => { // 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)); + } + }); +} diff --git a/bin/src/display/mod.rs b/bin/src/display/mod.rs new file mode 100644 index 0000000..175ccd6 --- /dev/null +++ b/bin/src/display/mod.rs @@ -0,0 +1,29 @@ +mod generic_framebuffer; + +#[cfg(feature = "tplink")] +mod tplink; +#[cfg(feature = "tplink")] +mod tplink_framebuffer; +#[cfg(feature = "tplink")] +mod tplink_onebit; + +#[cfg(feature = "tplink")] +pub use tplink::update_ui; + +#[cfg(feature = "orbic")] +mod orbic; +#[cfg(feature = "orbic")] +pub use orbic::update_ui; + +pub enum DisplayState { + Recording, + Paused, + WarningDetected, + RecordingCBM, +} + +#[cfg(all(feature = "orbic", feature = "tplink"))] +compile_error!("cannot compile for many devices at once"); + +#[cfg(not(any(feature = "orbic", feature = "tplink")))] +compile_error!("cannot compile for no device at all"); diff --git a/bin/src/display/orbic.rs b/bin/src/display/orbic.rs new file mode 100644 index 0000000..84ab399 --- /dev/null +++ b/bin/src/display/orbic.rs @@ -0,0 +1,52 @@ +use crate::config; +use crate::display::DisplayState; +use crate::display::generic_framebuffer::{self, GenericFramebuffer, Dimensions}; + +use tokio::sync::oneshot; +use tokio::sync::mpsc::Receiver; +use tokio_util::task::TaskTracker; + +const FB_PATH: &str = "/dev/fb0"; + +#[derive(Copy, Clone, Default)] +struct Framebuffer; + +impl GenericFramebuffer for Framebuffer { + fn dimensions(&self) -> Dimensions { + // TODO actually poll for this, maybe w/ fbset? + Dimensions { + height: 128, + width: 128, + } + } + + fn write_buffer( + &mut self, + buffer: &[(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()); + } + + std::fs::write(FB_PATH, &raw_buffer).unwrap(); + } +} + +pub fn update_ui( + task_tracker: &TaskTracker, + config: &config::Config, + ui_shutdown_rx: oneshot::Receiver<()>, + ui_update_rx: Receiver +) { + generic_framebuffer::update_ui( + task_tracker, + config, + Framebuffer, + ui_shutdown_rx, + ui_update_rx, + ) +} diff --git a/bin/src/display/tplink.rs b/bin/src/display/tplink.rs new file mode 100644 index 0000000..5216f31 --- /dev/null +++ b/bin/src/display/tplink.rs @@ -0,0 +1,39 @@ +use log::info; +use tokio::sync::mpsc::Receiver; +use tokio::sync::oneshot; +use tokio_util::task::TaskTracker; + +use crate::config; +use crate::display::{DisplayState, tplink_onebit, tplink_framebuffer}; + +use std::fs; + +pub fn update_ui( + task_tracker: &TaskTracker, + config: &config::Config, + ui_shutdown_rx: oneshot::Receiver<()>, + ui_update_rx: Receiver, +) { + let display_level = config.ui_level; + if display_level == 0 { + info!("Invisible mode, not spawning UI."); + } + + 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 + ) + } else { + info!("fallback to framebuffer"); + tplink_framebuffer::update_ui( + task_tracker, + config, + ui_shutdown_rx, + ui_update_rx + ) + } +} diff --git a/bin/src/display/tplink_framebuffer.rs b/bin/src/display/tplink_framebuffer.rs new file mode 100644 index 0000000..ea3badd --- /dev/null +++ b/bin/src/display/tplink_framebuffer.rs @@ -0,0 +1,93 @@ +use std::fs::File; +use std::io::Write; +use std::os::fd::AsRawFd; + +use crate::config; +use crate::display::DisplayState; +use crate::display::generic_framebuffer::{self, GenericFramebuffer, Dimensions}; + +use tokio::sync::oneshot; +use tokio::sync::mpsc::Receiver; +use tokio_util::task::TaskTracker; + +const FB_PATH: &str = "/dev/fb0"; + +struct Framebuffer; + +#[repr(C)] +struct fb_fillrect { + dx: u32, + dy: u32, + width: u32, + height: u32, + color: u32, + rop: u32, +} + +impl GenericFramebuffer for Framebuffer { + fn dimensions(&self) -> Dimensions { + // TODO actually poll for this, maybe w/ fbset? + Dimensions { + height: 128, + width: 128, + } + } + + fn write_buffer( + &mut self, + buffer: &[(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 arg = fb_fillrect { + dx: 0, + dy: 0, + width, + height, + color: 0xffff, // not sure what this is + rop: 0, + }; + + 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).unwrap(); + + unsafe { + let res = libc::ioctl( + f.as_raw_fd(), + 0x4619, // FBIORECT_DISPLAY + &mut arg as *mut _, + std::mem::size_of::(), + ); + + if res < 0 { + panic!("failed to send FBIORECT_DISPLAY ioctl, {}", res); + } + } + } +} + +pub fn update_ui( + task_tracker: &TaskTracker, + config: &config::Config, + ui_shutdown_rx: oneshot::Receiver<()>, + ui_update_rx: Receiver +) { + generic_framebuffer::update_ui( + task_tracker, + config, + Framebuffer, + ui_shutdown_rx, + ui_update_rx, + ) +} diff --git a/bin/src/display/tplink_onebit.rs b/bin/src/display/tplink_onebit.rs new file mode 100644 index 0000000..32f0dd7 --- /dev/null +++ b/bin/src/display/tplink_onebit.rs @@ -0,0 +1,188 @@ +/// Display module for the TP-Link M7350 oled one-bit display. +/// +/// https://github.com/m0veax/tplink_m7350/tree/main/oled +use crate::config; +use crate::display::DisplayState; + +use log::{info, error}; +use tokio::sync::mpsc::Receiver; +use tokio::sync::oneshot; +use tokio_util::task::TaskTracker; +use tokio::sync::oneshot::error::TryRecvError; + +use std::fs; +use std::thread::sleep; +use std::time::Duration; + +pub const OLED_PATH: &str = "/sys/class/display/oled/oled_buffer"; + +// those coordinates were mainly chosen for a spot that doesn't get regularly updated by the main +// oledd service. otherwise we'd have to write to the display more than once per second to prevent +// the icon from flickering. +const STATUS_X: u8 = 104; +const STATUS_Y: u8 = 40; +const STATUS_W: u8 = 16; +const STATUS_H: u8 = 16; + +const STATUS_HEADER: [u8; 4] = [STATUS_X, STATUS_Y, STATUS_W, STATUS_H]; + +macro_rules! pixel { + (x) => { 0 }; + (_) => { 1 }; +} + +macro_rules! pixelart { + ($($tt:tt)*) => {{ + // could be improved to be const expr or at least to compile to something that doesn't + // allocate. but the macro is easier to write this way. + let mut bytes = Vec::new(); + let mut i = 0; + let mut byte = 0; + $( + byte |= pixel!($tt); + if i == 7 { + bytes.push(byte); + i = 0; + byte = 0; + } else { + i += 1; + byte <<= 1; + } + )* + + // last byte is bogus, discard it to silence warnings + let _byte = byte; + + assert_eq!(i % 8, 0); + bytes + }} +} + +fn paused() -> Vec { + let mut command = STATUS_HEADER.to_vec(); + + command.extend(pixelart! { + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + _ _ _ x x x x x x x x x x _ _ _ + _ x x _ _ _ _ _ _ _ _ _ _ x x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ x _ _ _ _ x _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x x _ _ _ _ _ _ _ _ _ _ x x _ + _ _ _ x x x x x x x x x x _ _ _ + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + }); + command +} + +fn smiling() -> Vec { + let mut command = STATUS_HEADER.to_vec(); + + command.extend(pixelart! { + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + _ _ _ x x x x x x x x x x _ _ _ + _ x x _ _ _ _ _ _ _ _ _ _ x x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ x _ _ _ _ x _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ x _ _ _ _ x _ _ _ x _ + _ x _ _ _ x _ _ _ _ x _ _ _ x _ + _ x _ _ _ x x x x x x _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x x _ _ _ _ _ _ _ _ _ _ x x _ + _ _ _ x x x x x x x x x x _ _ _ + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + }); + command +} + +fn frowning() -> Vec { + let mut command = STATUS_HEADER.to_vec(); + + command.extend( + pixelart! { + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + _ _ _ x x x x x x x x x x _ _ _ + _ x x _ _ _ _ _ _ _ _ _ _ x x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ x _ _ _ _ x _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ x x x x x x _ _ _ x _ + _ x _ _ _ x _ _ _ _ x _ _ _ x _ + _ x _ _ _ x _ _ _ _ x _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x _ _ _ _ _ _ _ _ _ _ _ _ x _ + _ x x _ _ _ _ _ _ _ _ _ _ x x _ + _ _ _ x x x x x x x x x x _ _ _ + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + } + ); + command +} + +pub fn update_ui( + task_tracker: &TaskTracker, + config: &config::Config, + mut ui_shutdown_rx: oneshot::Receiver<()>, + mut ui_update_rx: Receiver, +) { + let display_level = config.ui_level; + if display_level == 0 { + info!("Invisible mode, not spawning UI."); + } + + + task_tracker.spawn_blocking(move || { + let mut pixels = smiling(); + + loop { + match ui_shutdown_rx.try_recv() { + Ok(_) => { + info!("received UI shutdown"); + break; + }, + Err(TryRecvError::Empty) => {}, + Err(e) => panic!("error receiving shutdown message: {e}") + } + + match ui_update_rx.try_recv() { + Ok(DisplayState::Paused) => pixels = paused(), + Ok(DisplayState::Recording) => pixels = smiling(), + Ok(DisplayState::RecordingCBM) => pixels = smiling(), + Ok(DisplayState::WarningDetected) => pixels = frowning(), + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}, + Err(e) => { + error!("error receiving framebuffer update message: {e}"); + } + }; + + // 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) { + error!("failed to write to display: {e}"); + } + } + + sleep(Duration::from_millis(1000)); + } + }); +} + +#[test] +fn test_pixels() { + let pixels = frowning(); + assert_eq!(pixels, [104, 40, 16, 16, 255, 255, 224, 7, 159, 249, 191, 253, 191, 253, 187, 221, 191, 253, 191, 253, 184, 29, 187, 221, 187, 221, 191, 253, 191, 253, 159, 249, 224, 7, 255, 255]); +} diff --git a/bin/src/framebuffer.rs b/bin/src/framebuffer.rs deleted file mode 100644 index 6e1ecd1..0000000 --- a/bin/src/framebuffer.rs +++ /dev/null @@ -1,111 +0,0 @@ -use image::{codecs::gif::GifDecoder, imageops::FilterType, AnimationDecoder, DynamicImage}; -use std::{io::Cursor, time::Duration}; - -const FB_PATH:&str = "/dev/fb0"; - -#[derive(Copy, Clone)] -// TODO actually poll for this, maybe w/ fbset? -struct Dimensions { - height: u32, - width: u32, -} - -#[allow(dead_code)] -#[derive(Copy, Clone)] -pub enum Color565 { - Red = 0b1111100000000000, - Green = 0b0000011111100000, - Blue = 0b0000000000011111, - White = 0b1111111111111111, - Black = 0b0000000000000000, - Cyan = 0b0000011111111111, - Yellow = 0b1111111111100000, - Pink = 0b1111010010011111, -} - -pub enum DisplayState { - Recording, - Paused, - WarningDetected, - RecordingCBM, -} - -impl From for Color565 { - fn from(state: DisplayState) -> Self { - match state { - DisplayState::Paused => Color565::White, - DisplayState::Recording => Color565::Green, - DisplayState::RecordingCBM => Color565::Blue, - DisplayState::WarningDetected => Color565::Red, - } - } -} - -#[derive(Copy, Clone)] -pub struct Framebuffer<'a> { - dimensions: Dimensions, - path: &'a str, -} - -impl Framebuffer<'_>{ - pub const fn new() -> Self { - Framebuffer{ - dimensions: Dimensions{height: 128, width: 128}, - path: FB_PATH, - } - } - - fn write(&mut self, img: DynamicImage) { - let mut width = img.width(); - let mut height = img.height(); - let resized_img: DynamicImage; - if height > self.dimensions.height || - width > self.dimensions.width { - resized_img = img.resize( self.dimensions.width, self.dimensions.height, FilterType::CatmullRom); - width = self.dimensions.width.min(resized_img.width()); - height = self.dimensions.height.min(resized_img.height()); - } else { - resized_img = img; - } - let img_rgba8 = resized_img.as_rgba8().unwrap(); - let mut buf = Vec::new(); - for y in 0..height { - for x in 0..width { - let px = img_rgba8.get_pixel(x, y); - let mut rgb565: u16 = (px[0] as u16 & 0b11111000) << 8; - rgb565 |= (px[1] as u16 & 0b11111100) << 3; - rgb565 |= (px[2] as u16) >> 3; - buf.extend(rgb565.to_le_bytes()); - } - } - std::fs::write(self.path, &buf).unwrap(); - } - - pub fn draw_gif(&mut self, img_buffer: &[u8]) { - // this is dumb and i'm sure there's a better way to loop this - 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(img); - std::thread::sleep(Duration::from_millis(numerator as u64)); - } - } - - pub fn draw_img(&mut self, img_buffer: &[u8]) { - let img = image::load_from_memory(img_buffer).unwrap(); - self.write(img); - } - - pub fn draw_line(&mut self, color: Color565, height: u32){ - let px_num= height * self.dimensions.width; - let color: u16 = color as u16; - let mut buffer: Vec = Vec::new(); - for _ in 0..px_num { - buffer.extend(color.to_le_bytes()); - } - std::fs::write(self.path, &buffer).unwrap(); - } -} \ No newline at end of file diff --git a/bin/src/server.rs b/bin/src/server.rs index c9a531a..4835ea3 100644 --- a/bin/src/server.rs +++ b/bin/src/server.rs @@ -12,14 +12,14 @@ use tokio::sync::RwLock; use tokio_util::io::ReaderStream; use include_dir::{include_dir, Dir}; -use crate::{framebuffer, DiagDeviceCtrlMessage}; +use crate::{display, DiagDeviceCtrlMessage}; use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus}; use crate::qmdl_store::RecordingStore; pub struct ServerState { pub qmdl_store_lock: Arc>, pub diag_device_ctrl_sender: Sender, - pub ui_update_sender: Sender, + pub ui_update_sender: Sender, pub analysis_status_lock: Arc>, pub analysis_sender: Sender, pub debug_mode: bool, diff --git a/dist/config.toml.example b/dist/config.toml.example index 6cd2c7f..a7c3a2f 100644 --- a/dist/config.toml.example +++ b/dist/config.toml.example @@ -5,8 +5,14 @@ debug_mode = false enable_dummy_analyzer = false colorblind_mode = false # UI Levels: +# +# Orbic and TP-Link with color display: # 0 = invisible mode, no indicator that rayhunter is running -# 1 = Subtle mode, display a green line at the top of the screen when rayhunter is running +# 1 = Subtle mode, display a colored line at the top of the screen when rayhunter is running (green=running, white=paused, red=warnings) # 2 = Demo Mode, display a fun orca gif # 3 = display the EFF logo +# +# TP-Link with one-bit display: +# 0 = invisible mode +# 1..3 = show emoji for status. :) for running, :( for warnings, no mouth for paused. ui_level = 1 diff --git a/dist/install.sh b/dist/install.sh index ff9347c..025ead1 100755 --- a/dist/install.sh +++ b/dist/install.sh @@ -54,7 +54,7 @@ setup_rayhunter() { _at_syscmd "mkdir -p /data/rayhunter" _adb_push config.toml.example /tmp/config.toml _at_syscmd "mv /tmp/config.toml /data/rayhunter" - _adb_push rayhunter-daemon /tmp/rayhunter-daemon + _adb_push rayhunter-daemon-orbic/rayhunter-daemon /tmp/rayhunter-daemon _at_syscmd "mv /tmp/rayhunter-daemon /data/rayhunter" _adb_push scripts/rayhunter_daemon /tmp/rayhunter_daemon _at_syscmd "mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index ecde8cf..8213150 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -9,6 +9,11 @@ description = "Realtime cellular data decoding and analysis for IMSI catcher det name = "rayhunter" path = "src/lib.rs" +[features] +default = [] +orbic = [] +tplink = [] + [dependencies] bytes = "1.5.0" chrono = "0.4.31" diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs index 117f33e..fd2da25 100644 --- a/lib/src/diag_device.rs +++ b/lib/src/diag_device.rs @@ -6,7 +6,7 @@ use std::io::ErrorKind; use std::os::fd::AsRawFd; use futures_core::TryStream; use thiserror::Error; -use log::{info, warn, error}; +use log::{info, error}; use deku::prelude::*; use tokio::fs::File; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -57,7 +57,7 @@ pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [ ]; const BUFFER_LEN: usize = 1024 * 1024 * 10; -const MEMORY_DEVICE_MODE: i32 = 2; +const MEMORY_DEVICE_MODE: u32 = 2; #[cfg(target_arch = "arm")] const DIAG_IOCTL_REMOTE_DEV: u32 = 32; @@ -108,16 +108,17 @@ impl DiagDevice { async fn get_next_messages_container(&mut self) -> Result { let mut bytes_read = 0; - while bytes_read == 0 { + while bytes_read <= 8 { bytes_read = self.file.read(&mut self.read_buf).await .map_err(DiagDeviceError::DeviceReadFailed)?; } - let ((leftover_bytes, _), container) = MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0)) - .map_err(DiagDeviceError::ParseMessagesContainerError)?; - if !leftover_bytes.is_empty() { - warn!("warning: {} leftover bytes when parsing MessagesContainer", leftover_bytes.len()); + + info!("Parsing messages container size = {:?} [{:?}]", bytes_read, &self.read_buf[0..bytes_read]); + + match MessagesContainer::from_bytes((&self.read_buf[0..bytes_read], 0)) { + Ok((_, container)) => return Ok(container), + Err(err) => return Err(DiagDeviceError::ParseMessagesContainerError(err)), } - Ok(container) } async fn write_request(&mut self, req: &Request) -> DiagResult<()> { @@ -215,15 +216,44 @@ impl DiagDevice { } } +// also found in: https://android.googlesource.com/kernel/msm.git/+/android-7.1.0_r0.3/drivers/char/diag/diagchar.h#399 +// +// the code on +// https://github.com/P1sec/QCSuper/blob/master/docs/The%20Diag%20protocol.md#the-diag-protocol-over-devdiag +// is misleading, mode_param is only 8 bits. sending the larger [u32; 3] payload will cause the +// IOCTL to be rejected by TPLINK M7350 HW rev 5 +// +// TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada +#[repr(C)] +struct diag_logging_mode_param_t { + req_mode: u32, + peripheral_mask: u32, + mode_param: u8 +} + // Triggers the diag device's debug logging mode -fn enable_frame_readwrite(fd: i32, mode: i32) -> DiagResult<()> { +fn enable_frame_readwrite(fd: i32, mode: u32) -> DiagResult<()> { unsafe { if libc::ioctl(fd, DIAG_IOCTL_SWITCH_LOGGING, mode, 0, 0, 0) < 0 { + let mut params = if cfg!(feature = "tplink") { + diag_logging_mode_param_t { + req_mode: mode, + peripheral_mask: 0, + mode_param: 1, + } + } else { + diag_logging_mode_param_t { + req_mode: mode, + peripheral_mask: u32::MAX, + mode_param: 0, + } + }; + let ret = libc::ioctl( fd, DIAG_IOCTL_SWITCH_LOGGING, - &mut [mode, -1, 0] as *mut _, // diag_logging_mode_param_t - std::mem::size_of::<[i32; 3]>(), 0, 0, 0, 0 + &mut params as *mut _, + std::mem::size_of::(), 0, 0, 0, 0 ); if ret < 0 { let msg = format!("DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}", ret);