mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-05 20:51:53 -07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2226071d2 | |||
| 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"
|
||||
|
||||
Generated
+1
@@ -2422,6 +2422,7 @@ name = "rayhunter-daemon"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
"axum",
|
||||
"chrono",
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ edition = "2024"
|
||||
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"
|
||||
|
||||
@@ -1,6 +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;
|
||||
@@ -11,8 +16,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)]
|
||||
@@ -21,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 {
|
||||
@@ -65,15 +82,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 +109,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 +145,110 @@ pub trait GenericFramebuffer: Send + 'static {
|
||||
buffer.push(color.rgb());
|
||||
}
|
||||
|
||||
self.write_buffer(&buffer);
|
||||
self.write_buffer(buffer).await
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +268,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 +304,202 @@ 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
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;
|
||||
|
||||
use super::generic_framebuffer::FramebufferDevice;
|
||||
|
||||
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,
|
||||
@@ -42,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,
|
||||
)
|
||||
|
||||
@@ -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,18 +1,16 @@
|
||||
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, 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 {
|
||||
@@ -24,52 +22,28 @@ struct fb_fillrect {
|
||||
rop: u32,
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
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).unwrap();
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,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,
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,38 +6,16 @@
|
||||
/// WT_HARDWARE_VERSION=89323_1_20
|
||||
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;
|
||||
|
||||
use super::generic_framebuffer::FramebufferDevice;
|
||||
|
||||
const FB_PATH: &str = "/dev/fb0";
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
struct Framebuffer;
|
||||
|
||||
impl GenericFramebuffer for Framebuffer {
|
||||
fn dimensions(&self) -> Dimensions {
|
||||
Dimensions {
|
||||
height: 128,
|
||||
width: 160,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -47,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
@@ -182,7 +182,7 @@ fn run_shutdown_thread(
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<(), RayhunterError> {
|
||||
env_logger::init();
|
||||
|
||||
|
||||
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).
|
||||
|
||||
+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.
|
||||
|
||||
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).
|
||||
|
||||
@@ -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"'
|
||||
|
||||
Reference in New Issue
Block a user