mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-31 18:23:35 -07:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2b722ad5f | |||
| 5e2058e7ac | |||
| 60daf4b716 | |||
| 4df317b028 | |||
| d7fb8b9c85 | |||
| d399532494 | |||
| 45df91a364 | |||
| 672ed8c6c6 | |||
| 5c7c7cd766 | |||
| f41a8d38fe | |||
| f9c8c4671e | |||
| 723b20541e | |||
| 272a4aeabf | |||
| 6ae70556ba | |||
| 2915dea9e9 | |||
| 6941bc57b6 | |||
| 5b9dd856a8 | |||
| 5007cb0b36 | |||
| 1b244122df | |||
| 3c4cb56ce6 | |||
| 58843413b5 |
@@ -74,7 +74,15 @@ jobs:
|
||||
npm install
|
||||
npm run build
|
||||
popd
|
||||
cargo build --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile=firmware --no-default-features --features ${{ matrix.device.name }}
|
||||
# Run with -p so that cargo will select the minimum feature set for this package.
|
||||
#
|
||||
# Otherwise, it will consider the union of all requested features
|
||||
# from all packages in the workspace. For example, if installer
|
||||
# requires tokio with "full" feature, it will be included no matter
|
||||
# what the feature selection in rayhunter-daemon is.
|
||||
#
|
||||
# https://github.com/rust-lang/cargo/issues/4463
|
||||
cargo build -p rayhunter-daemon --bin rayhunter-daemon --target armv7-unknown-linux-musleabihf --profile=firmware --no-default-features --features ${{ matrix.device.name }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rayhunter-daemon-${{ matrix.device.name }}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
/book
|
||||
.DS_Store
|
||||
|
||||
Generated
+7
-25
@@ -5,7 +5,7 @@ version = 4
|
||||
[[package]]
|
||||
name = "adb_client"
|
||||
version = "2.1.11"
|
||||
source = "git+https://github.com/gaykitty/adb_client.git?rev=1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81#1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
|
||||
source = "git+https://github.com/gaykitty/adb_client.git?rev=e732fc178a0eb237138e4091059ff5ffa241385a#e732fc178a0eb237138e4091059ff5ffa241385a"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"base64",
|
||||
@@ -944,7 +944,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
@@ -967,17 +966,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
@@ -1026,13 +1014,10 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -1448,7 +1433,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"adb_client",
|
||||
"anyhow",
|
||||
@@ -2322,15 +2307,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"deku",
|
||||
"env_logger 0.10.2",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
@@ -2343,14 +2326,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
"clap",
|
||||
"env_logger 0.10.2",
|
||||
"env_logger 0.11.8",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"image",
|
||||
"include_dir",
|
||||
@@ -2471,7 +2453,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
||||
|
||||
[[package]]
|
||||
name = "rootshell"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"nix",
|
||||
]
|
||||
@@ -2844,7 +2826,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "telcom-parser"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"asn1-codecs",
|
||||
"asn1-compiler",
|
||||
|
||||
+6
-7
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter-daemon"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
@@ -22,20 +22,19 @@ path = "src/check.rs"
|
||||
rayhunter = { path = "../lib" }
|
||||
toml = "0.8.8"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
axum = "0.8"
|
||||
futures-core = "0.3.30"
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["fs", "signal", "process", "rt-multi-thread"] }
|
||||
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
|
||||
thiserror = "1.0.52"
|
||||
libc = "0.2.150"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.10.1"
|
||||
env_logger = { version = "0.11", default-features = false }
|
||||
tokio-util = { version = "0.7.10", features = ["rt", "io"] }
|
||||
futures-macro = "0.3.30"
|
||||
include_dir = "0.7.3"
|
||||
mime_guess = "2.0.4"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
tokio-stream = "0.1.14"
|
||||
futures = "0.3.30"
|
||||
tokio-stream = { version = "0.1.14", default-features = false }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
clap = { version = "4.5.2", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct Config {
|
||||
pub ui_level: u8,
|
||||
pub enable_dummy_analyzer: bool,
|
||||
pub colorblind_mode: bool,
|
||||
pub key_input_mode: u8,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -22,6 +23,7 @@ impl Default for Config {
|
||||
ui_level: 1,
|
||||
enable_dummy_analyzer: false,
|
||||
colorblind_mode: false,
|
||||
key_input_mode: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -4,6 +4,7 @@ mod diag;
|
||||
mod display;
|
||||
mod dummy_analyzer;
|
||||
mod error;
|
||||
mod key_input;
|
||||
mod pcap;
|
||||
mod qmdl_store;
|
||||
mod server;
|
||||
@@ -175,7 +176,7 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
let store = init_qmdl_store(&config).await?;
|
||||
let analysis_status = AnalysisStatus::new(&store);
|
||||
let qmdl_store_lock = Arc::new(RwLock::new(store));
|
||||
let (tx, rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
||||
let (diag_tx, diag_rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
|
||||
let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
|
||||
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
|
||||
let mut maybe_ui_shutdown_tx = None;
|
||||
@@ -193,13 +194,17 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
run_diag_read_thread(
|
||||
&task_tracker,
|
||||
dev,
|
||||
rx,
|
||||
diag_rx,
|
||||
ui_update_tx.clone(),
|
||||
qmdl_store_lock.clone(),
|
||||
analysis_tx.clone(),
|
||||
config.enable_dummy_analyzer,
|
||||
);
|
||||
info!("Starting UI");
|
||||
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
|
||||
|
||||
info!("Starting Key Input service");
|
||||
key_input::run_key_input_thread(&task_tracker, &config, diag_tx.clone());
|
||||
}
|
||||
let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
|
||||
info!("create shutdown thread");
|
||||
@@ -213,7 +218,7 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
);
|
||||
run_ctrl_c_thread(
|
||||
&task_tracker,
|
||||
tx.clone(),
|
||||
diag_tx.clone(),
|
||||
server_shutdown_tx,
|
||||
maybe_ui_shutdown_tx,
|
||||
qmdl_store_lock.clone(),
|
||||
@@ -221,7 +226,7 @@ async fn main() -> Result<(), RayhunterError> {
|
||||
);
|
||||
let state = Arc::new(ServerState {
|
||||
qmdl_store_lock: qmdl_store_lock.clone(),
|
||||
diag_device_ctrl_sender: tx,
|
||||
diag_device_ctrl_sender: diag_tx,
|
||||
ui_update_sender: ui_update_tx,
|
||||
debug_mode: config.debug_mode,
|
||||
analysis_status_lock,
|
||||
|
||||
+44
-63
@@ -7,7 +7,7 @@ use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use log::{debug, error, info};
|
||||
use log::{debug, error, info, warn};
|
||||
use rayhunter::diag::DataType;
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use rayhunter::qmdl::QmdlWriter;
|
||||
@@ -24,7 +24,7 @@ use crate::server::ServerState;
|
||||
|
||||
pub enum DiagDeviceCtrlMessage {
|
||||
StopRecording,
|
||||
StartRecording((QmdlWriter<File>, File)),
|
||||
StartRecording,
|
||||
Exit,
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ pub fn run_diag_read_thread(
|
||||
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
|
||||
ui_update_sender: Sender<display::DisplayState>,
|
||||
qmdl_store_lock: Arc<RwLock<RecordingStore>>,
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
enable_dummy_analyzer: bool,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
@@ -46,20 +47,56 @@ pub fn run_diag_read_thread(
|
||||
tokio::select! {
|
||||
msg = qmdl_file_rx.recv() => {
|
||||
match msg {
|
||||
Some(DiagDeviceCtrlMessage::StartRecording((new_writer, new_analysis_file))) => {
|
||||
maybe_qmdl_writer = Some(new_writer);
|
||||
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let (qmdl_file, new_analysis_file) = match qmdl_store.new_entry().await {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("couldn't create new qmdl entry: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
maybe_qmdl_writer = Some(QmdlWriter::new(qmdl_file));
|
||||
|
||||
if let Some(analysis_writer) = maybe_analysis_writer {
|
||||
analysis_writer.close().await.expect("failed to close analysis writer");
|
||||
}
|
||||
|
||||
maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).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;
|
||||
match qmdl_store.get_current_entry() {
|
||||
Some((_, entry)) => {
|
||||
if let Err(e) = analysis_sender
|
||||
.send(AnalysisCtrlMessage::RecordingFinished(
|
||||
entry.name.to_string(),
|
||||
))
|
||||
.await {
|
||||
warn!("couldn't send analysis message: {}", e);
|
||||
}
|
||||
}
|
||||
None => todo!(),
|
||||
}
|
||||
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
|
||||
@@ -125,37 +162,15 @@ pub async fn start_recording(
|
||||
if state.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
let (qmdl_file, analysis_file) = qmdl_store.new_entry().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't create new qmdl entry: {}", e),
|
||||
)
|
||||
})?;
|
||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||
|
||||
state
|
||||
.diag_device_ctrl_sender
|
||||
.send(DiagDeviceCtrlMessage::StartRecording((
|
||||
qmdl_writer,
|
||||
analysis_file,
|
||||
)))
|
||||
.send(DiagDeviceCtrlMessage::StartRecording)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send stop recording message: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let display_state = 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),
|
||||
format!("couldn't send start recording message: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -168,30 +183,6 @@ pub async fn stop_recording(
|
||||
if state.debug_mode {
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
let mut qmdl_store = state.qmdl_store_lock.write().await;
|
||||
match qmdl_store.get_current_entry() {
|
||||
Some((_, entry)) => {
|
||||
state
|
||||
.analysis_sender
|
||||
.send(AnalysisCtrlMessage::RecordingFinished(
|
||||
entry.name.to_string(),
|
||||
))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't send AnalysisCtrlMessage: {}", e),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
None => todo!(),
|
||||
}
|
||||
qmdl_store.close_current_entry().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("couldn't close current qmdl entry: {}", e),
|
||||
)
|
||||
})?;
|
||||
state
|
||||
.diag_device_ctrl_sender
|
||||
.send(DiagDeviceCtrlMessage::StopRecording)
|
||||
@@ -202,16 +193,6 @@ pub async fn stop_recording(
|
||||
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()))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use log::error;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::config;
|
||||
use crate::diag::DiagDeviceCtrlMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Event {
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
}
|
||||
|
||||
const INPUT_EVENT_SIZE: usize = 32;
|
||||
|
||||
pub fn run_key_input_thread(
|
||||
task_tracker: &TaskTracker,
|
||||
config: &config::Config,
|
||||
diag_tx: Sender<DiagDeviceCtrlMessage>,
|
||||
) {
|
||||
if config.key_input_mode == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
task_tracker.spawn(async move {
|
||||
// Open the input device
|
||||
let mut file = match File::open("/dev/input/event0").await {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
error!("Failed to open /dev/input/event0: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut buffer = [0u8; INPUT_EVENT_SIZE];
|
||||
let mut last_keyup: Option<Instant> = None;
|
||||
|
||||
loop {
|
||||
if let Err(e) = file.read_exact(&mut buffer).await {
|
||||
error!("failed to read key input: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let event = parse_event(buffer);
|
||||
|
||||
match event {
|
||||
Event::KeyUp => {
|
||||
if last_keyup.is_some()
|
||||
&& last_keyup.unwrap().elapsed() < Duration::from_millis(500)
|
||||
{
|
||||
if let Err(e) = diag_tx.send(DiagDeviceCtrlMessage::StopRecording).await {
|
||||
error!("Failed to send StopRecording: {}", e);
|
||||
}
|
||||
if let Err(e) = diag_tx.send(DiagDeviceCtrlMessage::StartRecording).await {
|
||||
error!("Failed to send StartRecording: {}", e);
|
||||
}
|
||||
last_keyup = None;
|
||||
} else {
|
||||
last_keyup = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
Event::KeyDown => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_event(input: [u8; INPUT_EVENT_SIZE]) -> Event {
|
||||
if input[12] == 0 {
|
||||
Event::KeyUp
|
||||
} else {
|
||||
Event::KeyDown
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_event_keydown_m7350_v5() {
|
||||
let input = [
|
||||
0x57, 0x6c, 0x09, 0x00, 0x7c, 0xfb, 0x03, 0x00, 0x01, 0x00, 0x74, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
assert!(matches!(parse_event(input), Event::KeyDown));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_event_keyup_m7350_v5() {
|
||||
let input = [
|
||||
0x57, 0x6c, 0x09, 0x00, 0x1b, 0x15, 0x05, 0x00, 0x01, 0x00, 0x74, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
assert!(matches!(parse_event(input), Event::KeyUp));
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
|
||||
const events: Event[] = analysis_json.events.map((event_json: any): Event | null => {
|
||||
if (event_json === null) {
|
||||
return null;
|
||||
} else if (event_json.event_type === "Informational") {
|
||||
} else if (event_json.event_type.type === "Informational") {
|
||||
num_informational_logs += 1;
|
||||
return {
|
||||
type: EventType.Informational,
|
||||
@@ -82,8 +82,8 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
|
||||
num_warnings += 1;
|
||||
return {
|
||||
type: EventType.Warning,
|
||||
severity: event_json.severity === "High" ? Severity.High :
|
||||
event_json.severity === "Medium" ? Severity.Medium : Severity.Low,
|
||||
severity: event_json.event_type.severity === "High" ? Severity.High :
|
||||
event_json.event_type.severity === "Medium" ? Severity.Medium : Severity.Low,
|
||||
message: event_json.message,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
Storage
|
||||
</th>
|
||||
<td class={table_cell_classes}>
|
||||
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} / {stats.disk_stats.available_size})
|
||||
{stats.disk_stats.used_percent} used ({stats.disk_stats.used_size} used / {stats.disk_stats.available_size} available)
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b">
|
||||
|
||||
Vendored
+5
-1
@@ -14,5 +14,9 @@ colorblind_mode = false
|
||||
#
|
||||
# TP-Link with one-bit display:
|
||||
# 0 = invisible mode
|
||||
# 1..3 = show emoji for status. :) for running, :( for warnings, no mouth for paused.
|
||||
# 1..3 = show emoji for status. :) for running, ! for warnings, no mouth for paused.
|
||||
ui_level = 1
|
||||
|
||||
# 0 = rayhunter does not read button presses
|
||||
# 1 = double-tapping the power button starts/stops recordings
|
||||
key_input_mode = 1
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
@@ -11,12 +11,22 @@ Windows support in Rayhunter's installer is a work-in-progress. Depending on the
|
||||
|
||||
## Orbic
|
||||
|
||||
1. Install the [Zadig WinUSB driver](https://zadig.akeo.ie/).
|
||||
1. Connect the device to your computer using the provided USB cable.
|
||||
1. Install the [Zadig WinUSB driver installer](https://zadig.akeo.ie/).
|
||||
1. Open Zadig, click options->show all devices
|
||||
|
||||

|
||||
|
||||
1. Select 'RNDIS (Interface 0)'
|
||||
|
||||

|
||||
|
||||
1. Click 'install driver' and wait for it to finish.
|
||||
2. Download the latest `rayhunter-vX.X.X.zip` from the [Rayhunter releases page](https://github.com/EFForg/rayhunter/releases). The version you download will have numbers instead of X
|
||||
3. Unzip `rayhunter-vX.X.X` .
|
||||
4. Save the [`install.ps1` file here](https://github.com/EFForg/rayhunter/blob/powershell/installer/install.ps1) in top of the folder that was unzipped from release.zip.
|
||||
5. Run the following powershell command `Set-ExecutionPolicy remotesigned`
|
||||
5. Run the install script by double clicking on `install.ps1`. A powershell window will launch.
|
||||
The device will restart multiple times over the next few minutes.
|
||||
You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
|
||||
1. Open a powershell terminal by pressing Win+R and typing `powershell` and hitting enter.
|
||||
5. Type `cd ~\Downloads\rayhunter-v<x.x.x>\installer-windows-x86_64` (**Replace <x.x.x> with the rayhunter version you just unzipped**) and hit enter.
|
||||
5. Run the install script: `.\installer.exe orbic` and hit enter.
|
||||
- The device will restart multiple times over the next few minutes.
|
||||
- You will know it is done when you see terminal output that says `checking for rayhunter server...success!`
|
||||
6. Rayhunter should now be running! You can verify this by following the instructions below to [view the web UI](#usage-viewing-the-web-ui). You should also see a green line flash along the top of top the display on the device.
|
||||
|
||||
@@ -20,8 +20,8 @@ Make sure you've got one of Rayhunter's [supported devices](./supported-devices.
|
||||
First, enter the correct subfolder for your operating system:
|
||||
- for Ubuntu on x64 arhitecture: `cd installer-ubuntu-24`
|
||||
- for Ubuntu on ARM64 arhitecture: `cd installer-ubuntu-24-aarch64`
|
||||
- for MacOS on Intel architecture: `cd installer-macos-intel`
|
||||
- for MacOS on ARM achitecture: `cd installer-macos-arm`
|
||||
- for MacOS on Intel (old macbooks) architecture: `cd installer-macos-intel`
|
||||
- for MacOS on ARM (M1/M2 etc.) achitecture: `cd installer-macos-arm`
|
||||
- for Windows: `cd installer-windows-x86_64`
|
||||
|
||||
```bash
|
||||
|
||||
+12
-3
@@ -7,10 +7,10 @@ The TP-Link M7350 is supported by Rayhunter from 0.3.0 release. TP-Link M7350 su
|
||||
The TP-Link comes in many different *hardware versions*. Support for installation varies:
|
||||
|
||||
* `1.0`, `2.0`: **Not suported**, probably impossible to obtain anymore (even second-hand), however there is one report that installation is possible on `1.0` (but no reports if it is working or not)
|
||||
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: **Tested, no known issues.**
|
||||
* `3.0`, `3.2`, `5.0`, `5.2`, `7.0`, `8.0`: **Tested, no known issues since 0.3.0.**
|
||||
* `6.2`: **One user reported it is working**
|
||||
* `4.0`: **Not working yet** ([issue](https://github.com/EFForg/rayhunter/issues/332)), however [it could be installed manually](https://github.com/m0veax/rayhunter-tplink-m7350/))
|
||||
* `9.0`: **Not working yet** ([issue](https://github.com/EFForg/rayhunter/issues/325), however opening/rooting works, Rayhunter could be installed manually, but does not work because of [IOCTL error](#302))
|
||||
* `4.0`: **Manual firmware downgrade required** ([issue](https://github.com/EFForg/rayhunter/issues/332))
|
||||
* `9.0`: **Working since 0.3.2.**
|
||||
|
||||
TP-Link versions newer than `3.0` have cyan packaging and a color display. Version `3.0` has a one-bit display and white packaging.
|
||||
|
||||
@@ -62,6 +62,15 @@ You can change the `port` (default is `8080`) where Rayhunter is listening for i
|
||||
By default the device will go to sleep after N minutes of no devices being connected. In that mode it will also turn off connections to cell phone towers.
|
||||
In order for Rayhunter to record continuously, you have to turn off this sleep mode in TP-Link's admin panel (go to **Advanced** - **Power Saving**) or keep e.g. your phone connectd on the TP-Link's WiFi.
|
||||
|
||||
## Port triggers
|
||||
|
||||
On hardware revisions starting with v4.0, the installer will modify settings to
|
||||
add two port triggers. You can look at `Settings > NAT Settings > Port
|
||||
Triggers` in TP-Link's admin UI to see them.
|
||||
|
||||
1. One port trigger "rayhunter-root" to launch the telnet shell. This is only needed for installation, and can be removed after upgrade. You can reinstall it using `./installer util tplink-start-telnet`.
|
||||
2. One port trigger "rayhunter-daemon" to auto-start rayhunter on boot. If you remove this, rayhunter will have to be started manually from shell.
|
||||
|
||||
## Other links
|
||||
|
||||
For more information on the device and instructions on how to install Rayhunter without an installer (i.e. manually), please see [rayhunter-tplink-m7350](https://github.com/m0veax/rayhunter-tplink-m7350/)
|
||||
|
||||
+6
-1
@@ -16,4 +16,9 @@ Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabl
|
||||
|
||||
## TPLink
|
||||
|
||||
TODO
|
||||
1. Run `./installer util tplink-start-telnet`
|
||||
2. Telnet into the device `telnet 192.168.0.1`
|
||||
3. `rm /data/rayhunter /etc/init.d/rayhunter_daemon`
|
||||
4. `update-rc.d rayhunter_daemon remove`
|
||||
5. (hardware revision v4.0+ only) In `Settings > NAT Settings > Port Triggers` in TP-Link's admin UI, remove any leftover port triggers.
|
||||
|
||||
|
||||
+16
-5
@@ -2,19 +2,30 @@
|
||||
|
||||
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](#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.
|
||||
|
||||
## The web UI
|
||||
|
||||
You can access this UI in one of two ways:
|
||||
|
||||
* **Connect over wifi:** Connect your phone/laptop to your device's wifi
|
||||
* **Connect over WiFi:** Connect your phone/laptop to your device's WiFi
|
||||
network and visit [http://192.168.1.1:8080](http://192.168.1.1:8080) (orbic)
|
||||
or [http://192.168.0.1:8080](http://192.168.0.1:8080) (tplink).
|
||||
|
||||
Click past your browser warning you about the connection not being secure, Rayhunter doesn't have HTTPS yet.
|
||||
|
||||
On the Orbic, you can find the wifi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
|
||||
* **Connect over USB (orbic):** Connect your device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit [http://localhost:8080](http://localhost:8080).
|
||||
On the **Orbic**, you can find the WiFi network password by going to the Orbic's menu > 2.4 GHz WIFI Info > Enter > find the 8-character password next to the lock 🔒 icon.
|
||||
On the **TP-Link**, you can find the WiFi network password by going to the TP-Link's menu > Advanced > Wireless > Basic Settings.
|
||||
|
||||
* **Connect over USB (Orbic):** Connect your device to your laptop via USB. Run `adb forward tcp:8080 tcp:8080`, then visit [http://localhost:8080](http://localhost:8080).
|
||||
* For this you will need to install the Android Debug Bridge (ADB) on your computer, you can copy the version that was downloaded inside the `releases/platform-tools/` folder to somewhere else in your path or you can install it manually.
|
||||
* You can find instructions for doing so on your platform [here](https://www.xda-developers.com/install-adb-windows-macos-linux/#how-to-set-up-adb-on-your-computer), (don't worry about instructions for installing it on a phone/device yet).
|
||||
* On macOS, the easiest way to install ADB is with Homebrew: First [install Homebrew](https://brew.sh/), then run `brew install android-platform-tools`.
|
||||
* **Connect over USB (tplink):** Plug in the TP-Link and use USB tethering to establish a network connection. ADB support can be enabled on the device, but the installer won't do it for you.
|
||||
* On MacOS, the easiest way to install ADB is with Homebrew: First [install Homebrew](https://brew.sh/), then run `brew install android-platform-tools`.
|
||||
|
||||
* **Connect over USB (TP-Link):** Plug in the TP-Link and use USB tethering to establish a network connection. ADB support can be enabled on the device, but the installer won't do it for you.
|
||||
|
||||
## 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.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "installer"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -22,12 +22,12 @@ tokio-stream = "0.1.17"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies.adb_client]
|
||||
git = "https://github.com/gaykitty/adb_client.git"
|
||||
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
|
||||
rev = "e732fc178a0eb237138e4091059ff5ffa241385a"
|
||||
default-features = false
|
||||
features = ["trans-nusb"]
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client]
|
||||
git = "https://github.com/gaykitty/adb_client.git"
|
||||
rev = "1fb0f4f5cbcc95bbbb98db4ee2f1e53a1005aa81"
|
||||
rev = "e732fc178a0eb237138e4091059ff5ffa241385a"
|
||||
default-features = false
|
||||
features = ["trans-libusb"]
|
||||
|
||||
+17
-16
@@ -2,20 +2,14 @@ $global:adb = ".\platform-tools-latest-windows\platform-tools\adb.exe"
|
||||
$global:serial = ".\installer-windows-x86_64\installer.exe"
|
||||
|
||||
function _adb_push {
|
||||
& $global:adb -d push @args | Out-Null
|
||||
& $global:adb -d push @args *> $null
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
write-host "push exited with exit code $($exitCode)"
|
||||
}
|
||||
return $exitCode
|
||||
}
|
||||
|
||||
function _adb_shell {
|
||||
& $global:adb -d shell @args | Out-Null
|
||||
& $global:adb -d shell @args *> $null
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
write-host "shell exited with exit code $($exitCode)"
|
||||
}
|
||||
return $exitCode
|
||||
}
|
||||
|
||||
@@ -36,7 +30,7 @@ function _wait_for_atfwd_daemon {
|
||||
function force_debug_mode {
|
||||
write-host "Using adb at $($global:adb)"
|
||||
write-host "Forcing a switch into debug mode to enable ADB"
|
||||
_serial "util serial --root" | Out-Host
|
||||
_serial "--root" | Out-Host
|
||||
write-host "adb enabled, waiting for reboot..." -nonewline
|
||||
_wait_for_adb_shell
|
||||
write-host " it's alive!"
|
||||
@@ -58,7 +52,8 @@ function _serial {
|
||||
}
|
||||
|
||||
function setup_rootshell {
|
||||
_adb_push "rootshell" "/tmp"
|
||||
write-host "setting up rootshell"
|
||||
_adb_push "rootshell" "/tmp" | Out-null
|
||||
write-host "cp..."
|
||||
_serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host
|
||||
start-sleep -seconds 1
|
||||
@@ -68,19 +63,20 @@ function setup_rootshell {
|
||||
write-host "chmod..."
|
||||
_serial "AT+SYSCMD=chmod 4755 /bin/rootshell" | Out-Host
|
||||
start-sleep -seconds 1
|
||||
_adb_shell '/bin/rootshell -c id'
|
||||
_adb_shell '/bin/rootshell -c id' | Out-null
|
||||
write-host "we have root!"
|
||||
}
|
||||
|
||||
function setup_rayhunter {
|
||||
write-host "installing rayhunter..."
|
||||
_serial "AT+SYSCMD=mkdir -p /data/rayhunter" | Out-Host
|
||||
_adb_push "config.toml.example" "/tmp/config.toml"
|
||||
_adb_push "config.toml.example" "/tmp/config.toml" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/config.toml /data/rayhunter" | Out-Host
|
||||
_adb_push "rayhunter-daemon-orbic/rayhunter-daemon" "/tmp/rayhunter-daemon"
|
||||
_adb_push "rayhunter-daemon-orbic/rayhunter-daemon" "/tmp/rayhunter-daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/rayhunter-daemon /data/rayhunter" | Out-Host
|
||||
_adb_push "scripts/rayhunter_daemon" "/tmp/rayhunter_daemon"
|
||||
_adb_push "scripts/rayhunter_daemon" "/tmp/rayhunter_daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/rayhunter_daemon /etc/init.d/rayhunter_daemon" | Out-Host
|
||||
_adb_push "scripts/misc-daemon" "/tmp/misc-daemon"
|
||||
_adb_push "scripts/misc-daemon" "/tmp/misc-daemon" | Out-Null
|
||||
_serial "AT+SYSCMD=mv /tmp/misc-daemon /etc/init.d/misc-daemon" | Out-Host
|
||||
|
||||
_serial "AT+SYSCMD=chmod 755 /data/rayhunter/rayhunter-daemon" | Out-Host
|
||||
@@ -108,7 +104,12 @@ function test_rayhunter {
|
||||
write-host "checking for rayhunter server..." -nonewline
|
||||
$seconds = 0
|
||||
do {
|
||||
$resp = invoke-webrequest -uri $URL
|
||||
try {
|
||||
$resp = invoke-webrequest -uri $URL
|
||||
} catch {
|
||||
# Fail silently
|
||||
$resp = $null
|
||||
}
|
||||
if ($resp.statuscode -eq 200) {
|
||||
write-host "success!"
|
||||
write-host "you can access rayhunter at $($URL)"
|
||||
|
||||
+19
-1
@@ -1,5 +1,6 @@
|
||||
use anyhow::{Context, Error, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use env_logger::Env;
|
||||
|
||||
mod orbic;
|
||||
mod tplink;
|
||||
@@ -34,6 +35,17 @@ struct InstallTpLink {
|
||||
/// IP address for TP-Link admin interface, if custom.
|
||||
#[arg(long, default_value = "192.168.0.1")]
|
||||
admin_ip: String,
|
||||
|
||||
/// For advanced users: Specify the path of the SD card to be mounted explicitly.
|
||||
///
|
||||
/// The default (empty string) is to use whichever sdcard path the device would use natively to
|
||||
/// mount storage on. On most TP-Link this is /media/card, but on hardware versions 9+ this is
|
||||
/// /media/sdcard
|
||||
///
|
||||
/// Only override this when the installer does not work on your hardware version, as otherwise
|
||||
/// your custom path may conflict with the builtin storage functionality.
|
||||
#[arg(long, default_value = "")]
|
||||
sdcard_path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -49,6 +61,8 @@ struct Util {
|
||||
enum UtilSubCommand {
|
||||
/// Send a serial command to the Orbic.
|
||||
Serial(Serial),
|
||||
/// Start an ADB shell
|
||||
Shell(Shell),
|
||||
/// Root the tplink and launch telnetd.
|
||||
TplinkStartTelnet(TplinkStartTelnet),
|
||||
}
|
||||
@@ -67,8 +81,11 @@ struct Serial {
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Shell {}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("off")).init();
|
||||
let Args { command } = Args::parse();
|
||||
|
||||
match command {
|
||||
@@ -93,6 +110,7 @@ async fn run() -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
}
|
||||
UtilSubCommand::Shell(_) => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
|
||||
UtilSubCommand::TplinkStartTelnet(options) => {
|
||||
tplink::start_telnet(&options.admin_ip).await?;
|
||||
}
|
||||
|
||||
+83
-36
@@ -22,16 +22,24 @@ const ORBIC_BUSY: &str = r#"The Orbic is plugged in but is being used by another
|
||||
Please close any program that might be using your USB devices.
|
||||
If you have adb installed you may need to kill the adb daemon"#;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
const ORBIC_BUSY_MAC: &str = r#"Permission denied.
|
||||
|
||||
On macOS this might be caused by another program using the Orbic.
|
||||
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"#;
|
||||
|
||||
const VENDOR_ID: u16 = 0x05c6;
|
||||
const PRODUCT_ID: u16 = 0xf601;
|
||||
|
||||
const INTERFACE: u8 = 1;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const RNDIS_INTERFACE: u8 = 0;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const RNDIS_INTERFACE: u8 = 1;
|
||||
|
||||
macro_rules! echo {
|
||||
($($arg:tt)*) => {
|
||||
print!($($arg)*);
|
||||
@@ -41,12 +49,11 @@ macro_rules! echo {
|
||||
|
||||
pub async fn install() -> Result<()> {
|
||||
let mut adb_device = force_debug_mode().await?;
|
||||
let serial_interface = open_orbic()?.ok_or_else(|| anyhow!(ORBIC_NOT_FOUND))?;
|
||||
echo!("Installing rootshell... ");
|
||||
setup_rootshell(&serial_interface, &mut adb_device).await?;
|
||||
setup_rootshell(&mut adb_device).await?;
|
||||
println!("done");
|
||||
echo!("Installing rayhunter... ");
|
||||
let mut adb_device = setup_rayhunter(&serial_interface, adb_device).await?;
|
||||
let mut adb_device = setup_rayhunter(adb_device).await?;
|
||||
println!("done");
|
||||
echo!("Testing rayhunter... ");
|
||||
test_rayhunter(&mut adb_device).await?;
|
||||
@@ -54,11 +61,19 @@ pub async fn install() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shell() -> Result<()> {
|
||||
println!("opening shell");
|
||||
let mut adb_device = get_adb().await?;
|
||||
adb_device.shell(&mut std::io::stdin(), Box::new(std::io::stdout()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn force_debug_mode() -> Result<ADBUSBDevice> {
|
||||
println!("Forcing a switch into the debug mode to enable ADB");
|
||||
enable_command_mode()?;
|
||||
echo!("ADB enabled, waiting for reboot... ");
|
||||
let mut adb_device = get_adb().await?;
|
||||
adb_setup_serial(&mut adb_device).await?;
|
||||
println!("it's alive!");
|
||||
echo!("Waiting for atfwd_daemon to startup... ");
|
||||
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
|
||||
@@ -67,22 +82,20 @@ async fn force_debug_mode() -> Result<ADBUSBDevice> {
|
||||
}
|
||||
|
||||
async fn setup_rootshell(
|
||||
serial_interface: &Interface,
|
||||
adb_device: &mut ADBUSBDevice,
|
||||
) -> Result<()> {
|
||||
let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
|
||||
|
||||
install_file(
|
||||
serial_interface,
|
||||
adb_device,
|
||||
"/bin/rootshell",
|
||||
rootshell_bin,
|
||||
)
|
||||
.await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
at_syscmd(serial_interface, "chown root /bin/rootshell").await?;
|
||||
adb_at_syscmd(adb_device, "chown root /bin/rootshell").await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
at_syscmd(serial_interface, "chmod 4755 /bin/rootshell").await?;
|
||||
adb_at_syscmd(adb_device, "chmod 4755 /bin/rootshell").await?;
|
||||
let output = adb_command(adb_device, &["/bin/rootshell", "-c", "id"])?;
|
||||
if !output.contains("uid=0") {
|
||||
bail!("rootshell is not giving us root.");
|
||||
@@ -91,45 +104,40 @@ async fn setup_rootshell(
|
||||
}
|
||||
|
||||
async fn setup_rayhunter(
|
||||
serial_interface: &Interface,
|
||||
mut adb_device: ADBUSBDevice,
|
||||
) -> Result<ADBUSBDevice> {
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_ORBIC"));
|
||||
|
||||
at_syscmd(serial_interface, "mkdir -p /data/rayhunter").await?;
|
||||
adb_at_syscmd(&mut adb_device, "mkdir -p /data/rayhunter").await?;
|
||||
install_file(
|
||||
serial_interface,
|
||||
&mut adb_device,
|
||||
"/data/rayhunter/rayhunter-daemon",
|
||||
rayhunter_daemon_bin,
|
||||
)
|
||||
.await?;
|
||||
install_file(
|
||||
serial_interface,
|
||||
&mut adb_device,
|
||||
"/data/rayhunter/config.toml",
|
||||
CONFIG_TOML.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
install_file(
|
||||
serial_interface,
|
||||
&mut adb_device,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
RAYHUNTER_DAEMON_INIT.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
install_file(
|
||||
serial_interface,
|
||||
&mut adb_device,
|
||||
"/etc/init.d/misc-daemon",
|
||||
include_bytes!("../../dist/scripts/misc-daemon"),
|
||||
)
|
||||
.await?;
|
||||
at_syscmd(serial_interface, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
|
||||
at_syscmd(serial_interface, "chmod 755 /etc/init.d/misc-daemon").await?;
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/rayhunter_daemon").await?;
|
||||
adb_at_syscmd(&mut adb_device, "chmod 755 /etc/init.d/misc-daemon").await?;
|
||||
println!("done");
|
||||
echo!("Waiting for reboot... ");
|
||||
at_syscmd(serial_interface, "shutdown -r -t 1 now").await?;
|
||||
adb_at_syscmd(&mut adb_device, "shutdown -r -t 1 now").await?;
|
||||
// first wait for shutdown (it can take ~10s)
|
||||
tokio::time::timeout(Duration::from_secs(30), async {
|
||||
while let Ok(dev) = adb_echo_test(adb_device).await {
|
||||
@@ -162,7 +170,6 @@ async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn install_file(
|
||||
serial_interface: &Interface,
|
||||
adb_device: &mut ADBUSBDevice,
|
||||
dest: &str,
|
||||
payload: &[u8],
|
||||
@@ -170,7 +177,7 @@ async fn install_file(
|
||||
const MAX_FAILURES: u32 = 5;
|
||||
let mut failures = 0;
|
||||
loop {
|
||||
match install_file_impl(serial_interface, adb_device, dest, payload).await {
|
||||
match install_file_impl(adb_device, dest, payload).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
if failures > MAX_FAILURES {
|
||||
@@ -185,7 +192,6 @@ async fn install_file(
|
||||
}
|
||||
|
||||
async fn install_file_impl(
|
||||
serial_interface: &Interface,
|
||||
adb_device: &mut ADBUSBDevice,
|
||||
dest: &str,
|
||||
mut payload: &[u8],
|
||||
@@ -202,7 +208,7 @@ async fn install_file_impl(
|
||||
let file_hash_bytes = hasher.finalize();
|
||||
let file_hash = format!("{file_hash_bytes:x}");
|
||||
adb_device.push(&mut payload, &push_tmp_path)?;
|
||||
at_syscmd(serial_interface, &format!("mv {push_tmp_path} {dest}")).await?;
|
||||
adb_at_syscmd(adb_device, &format!("mv {push_tmp_path} {dest}")).await?;
|
||||
let file_info = adb_device
|
||||
.stat(dest)
|
||||
.context("Failed to stat transfered file")?;
|
||||
@@ -244,7 +250,7 @@ async fn get_adb() -> Result<ADBUSBDevice> {
|
||||
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => {
|
||||
bail!(ORBIC_BUSY);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os="windows"))]
|
||||
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => {
|
||||
bail!(ORBIC_BUSY_MAC);
|
||||
}
|
||||
@@ -321,9 +327,58 @@ async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn at_syscmd(interface: &Interface, command: &str) -> Result<()> {
|
||||
send_serial_cmd(interface, &format!("AT+SYSCMD={command}")).await
|
||||
async fn adb_setup_serial(adb_device: &mut ADBUSBDevice) -> Result<()> {
|
||||
Ok(adb_device.get_transport_mut().claim_interface(INTERFACE)?)
|
||||
}
|
||||
|
||||
async fn adb_at_syscmd(adb_device: &mut ADBUSBDevice, command: &str) -> Result<()> {
|
||||
adb_serial_cmd(adb_device, &format!("AT+SYSCMD={command}")).await
|
||||
}
|
||||
|
||||
async fn adb_serial_cmd(adb_device: &mut ADBUSBDevice, command: &str) -> Result<()> {
|
||||
let mut data = String::new();
|
||||
data.push_str("\r\n");
|
||||
data.push_str(command);
|
||||
data.push_str("\r\n");
|
||||
|
||||
let timeout = Duration::from_secs(2);
|
||||
let mut response = [0; 256];
|
||||
|
||||
// Set up the serial port appropriately
|
||||
adb_device
|
||||
.get_transport_mut()
|
||||
.send_usb_class_control_msg(INTERFACE, 0x22, 3, 1, &[], timeout)
|
||||
.context("Failed to send control request")?;
|
||||
|
||||
// Send the command
|
||||
adb_device
|
||||
.get_transport_mut()
|
||||
.usb_bulk_write(INTERFACE, 0x2, data.as_bytes(), timeout)
|
||||
.context("Failed to write command")?;
|
||||
|
||||
// Consume the echoed command
|
||||
adb_device
|
||||
.get_transport_mut()
|
||||
.usb_bulk_read(INTERFACE, 0x82, &mut response, timeout)
|
||||
.context("Failed to read submitted command")?;
|
||||
|
||||
// Read the actual response
|
||||
adb_device
|
||||
.get_transport_mut()
|
||||
.usb_bulk_read(INTERFACE, 0x82, &mut response, timeout)
|
||||
.context("Failed to read response")?;
|
||||
|
||||
// For some reason, on macOS the response buffer gets filled with garbage data that's
|
||||
// rarely valid UTF-8. Luckily we only care about the first couple bytes, so just drop
|
||||
// the garbage with `from_utf8_lossy` and look for our expected success string.
|
||||
let responsestr = String::from_utf8_lossy(&response);
|
||||
if !responsestr.contains("\r\nOK\r\n") {
|
||||
bail!("Received unexpected response: {0}", responsestr);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends an AT command to the usb device over the serial port
|
||||
///
|
||||
/// First establish a USB handle and context by calling `open_orbic(<T>)
|
||||
@@ -400,7 +455,7 @@ pub fn enable_command_mode() -> Result<()> {
|
||||
index: 0,
|
||||
};
|
||||
let interface = device
|
||||
.detach_and_claim_interface(1)
|
||||
.detach_and_claim_interface(RNDIS_INTERFACE)
|
||||
.context("detach_and_claim_interface(1) failed")?;
|
||||
if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) {
|
||||
// If the device reboots while the command is still executing we
|
||||
@@ -421,7 +476,7 @@ pub fn open_orbic() -> Result<Option<Interface>> {
|
||||
// Device after initial mode switch
|
||||
if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? {
|
||||
let interface = device
|
||||
.detach_and_claim_interface(1) // will reattach drivers on release
|
||||
.detach_and_claim_interface(INTERFACE) // will reattach drivers on release
|
||||
.context("detach_and_claim_interface(1) failed")?;
|
||||
return Ok(Some(interface));
|
||||
}
|
||||
@@ -429,15 +484,7 @@ pub fn open_orbic() -> Result<Option<Interface>> {
|
||||
// Device with rndis enabled as well
|
||||
if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? {
|
||||
let interface = device
|
||||
.detach_and_claim_interface(1) // will reattach drivers on release
|
||||
.context("detach_and_claim_interface(1) failed")?;
|
||||
return Ok(Some(interface));
|
||||
}
|
||||
|
||||
// Another device with rndis enabled as well
|
||||
if let Some(device) = open_usb_device(VENDOR_ID, 0xf626)? {
|
||||
let interface = device
|
||||
.detach_and_claim_interface(1) // will reattach drivers on release
|
||||
.detach_and_claim_interface(INTERFACE) // will reattach drivers on release
|
||||
.context("detach_and_claim_interface(1) failed")?;
|
||||
return Ok(Some(interface));
|
||||
}
|
||||
|
||||
+69
-20
@@ -27,10 +27,11 @@ pub async fn main_tplink(
|
||||
InstallTpLink {
|
||||
skip_sdcard,
|
||||
admin_ip,
|
||||
sdcard_path,
|
||||
}: InstallTpLink,
|
||||
) -> Result<(), Error> {
|
||||
start_telnet(&admin_ip).await?;
|
||||
tplink_run_install(skip_sdcard, admin_ip).await
|
||||
let is_v3 = start_telnet(&admin_ip).await?;
|
||||
tplink_run_install(skip_sdcard, admin_ip, sdcard_path, is_v3).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -38,7 +39,7 @@ struct V3RootResponse {
|
||||
result: u64,
|
||||
}
|
||||
|
||||
pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
|
||||
pub async fn start_telnet(admin_ip: &str) -> Result<bool, Error> {
|
||||
let qcmap_web_cgi_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi");
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
@@ -51,7 +52,9 @@ pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() == 404 {
|
||||
let is_v3 = response.status() != 404;
|
||||
|
||||
if !is_v3 {
|
||||
println!("Got a 404 trying to run exploit for hardware revision v3, trying v5 exploit");
|
||||
tplink_launch_telnet_v5(admin_ip).await?;
|
||||
} else {
|
||||
@@ -82,20 +85,49 @@ pub async fn start_telnet(admin_ip: &str) -> Result<(), Error> {
|
||||
println!(
|
||||
"Succeeded in rooting the device! Now you can use 'telnet {admin_ip}' to get a root shell. Use './installer util tplink-start-telnet' to root again without installing rayhunter."
|
||||
);
|
||||
Ok(())
|
||||
Ok(is_v3)
|
||||
}
|
||||
|
||||
async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), Error> {
|
||||
async fn tplink_run_install(
|
||||
skip_sdcard: bool,
|
||||
admin_ip: String,
|
||||
mut sdcard_path: String,
|
||||
is_v3: bool,
|
||||
) -> Result<(), Error> {
|
||||
println!("Connecting via telnet to {admin_ip}");
|
||||
let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap();
|
||||
|
||||
if !skip_sdcard {
|
||||
println!("Mounting sdcard");
|
||||
if telnet_send_command(addr, "mount | grep -q /media/card", "exit code 0")
|
||||
.await
|
||||
.is_err()
|
||||
if sdcard_path.is_empty() {
|
||||
if telnet_send_command(addr, "ls /media/card", "exit code 0")
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
// TP-Link hardware less than v9.0
|
||||
sdcard_path = "/media/card".to_owned();
|
||||
} else if telnet_send_command(addr, "ls /media/sdcard", "exit code 0")
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
// TP-Link hardware v9.0
|
||||
sdcard_path = "/media/sdcard".to_owned();
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"unable to determine sdcard path. this is a bug. please file an issue with your hardware version."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Mounting sdcard on {sdcard_path}");
|
||||
if telnet_send_command(
|
||||
addr,
|
||||
&format!("mount | grep -q {sdcard_path}"),
|
||||
"exit code 0",
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
telnet_send_command(addr, "mount /dev/mmcblk0p1 /media/card", "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?;
|
||||
telnet_send_command(addr, &format!("mount /dev/mmcblk0p1 {sdcard_path}"), "exit code 0").await.context("Rayhunter needs a FAT-formatted SD card to function for more than a few minutes. Insert one and rerun this installer, or pass --skip-sdcard")?;
|
||||
} else {
|
||||
println!("sdcard already mounted");
|
||||
}
|
||||
@@ -105,28 +137,38 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
|
||||
// expects things to be at this location
|
||||
telnet_send_command(addr, "rm -rf /data/rayhunter", "exit code 0").await?;
|
||||
telnet_send_command(addr, "mkdir -p /data", "exit code 0").await?;
|
||||
telnet_send_command(addr, "ln -sf /media/card /data/rayhunter", "exit code 0").await?;
|
||||
telnet_send_command(
|
||||
addr,
|
||||
&format!("ln -sf {sdcard_path} /data/rayhunter"),
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/media/card/config.toml",
|
||||
&format!("{sdcard_path}/config.toml"),
|
||||
crate::CONFIG_TOML.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_TPLINK"));
|
||||
|
||||
telnet_send_file(addr, "/media/card/rayhunter-daemon", rayhunter_daemon_bin).await?;
|
||||
telnet_send_file(
|
||||
addr,
|
||||
&format!("{sdcard_path}/rayhunter-daemon"),
|
||||
rayhunter_daemon_bin,
|
||||
)
|
||||
.await?;
|
||||
telnet_send_file(
|
||||
addr,
|
||||
"/etc/init.d/rayhunter_daemon",
|
||||
get_rayhunter_daemon().as_bytes(),
|
||||
get_rayhunter_daemon(&sdcard_path).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
telnet_send_command(
|
||||
addr,
|
||||
"chmod ugo+x /media/card/rayhunter-daemon",
|
||||
&format!("chmod ugo+x {sdcard_path}/rayhunter-daemon"),
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
@@ -136,7 +178,13 @@ async fn tplink_run_install(skip_sdcard: bool, admin_ip: String) -> Result<(), E
|
||||
"exit code 0",
|
||||
)
|
||||
.await?;
|
||||
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
|
||||
|
||||
// if the device is not v3, the JS-based root exploit already added rayhunter_daemon as a
|
||||
// startup script. tplink v9 does not have update-rc.d, and it was reported that *sometimes* it
|
||||
// is unreliable on other hardware revisions too.
|
||||
if is_v3 {
|
||||
telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "exit code 0").await?;
|
||||
}
|
||||
|
||||
println!(
|
||||
"Done. Rebooting device. After it's started up again, check out the web interface at http://{admin_ip}:8080"
|
||||
@@ -278,6 +326,7 @@ async fn handler(state: State<AppState>, mut req: Request) -> Result<Response, S
|
||||
// inject some javascript into the admin UI to get us a telnet shell.
|
||||
data.extend(br#";window.rayhunterPoll = window.setInterval(() => {
|
||||
Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"});
|
||||
Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"});
|
||||
alert("Success! You can go back to the rayhunter installer.");
|
||||
window.clearInterval(window.rayhunterPoll);
|
||||
}, 1000);"#);
|
||||
@@ -324,7 +373,7 @@ async fn tplink_launch_telnet_v5(admin_ip: &str) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_rayhunter_daemon() -> String {
|
||||
fn get_rayhunter_daemon(sdcard_path: &str) -> String {
|
||||
// Even though TP-Link eventually auto-mounts the SD card, it sometimes does so too late. And
|
||||
// changing the order in which daemons are started up seems to not work reliably.
|
||||
//
|
||||
@@ -332,12 +381,12 @@ fn get_rayhunter_daemon() -> String {
|
||||
// specific to a particular hardware revision here.
|
||||
crate::RAYHUNTER_DAEMON_INIT.replace(
|
||||
"#RAYHUNTER-PRESTART",
|
||||
"mount /dev/mmcblk0p1 /media/card || true",
|
||||
&format!("mount /dev/mmcblk0p1 {sdcard_path} || true"),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_rayhunter_daemon() {
|
||||
let s = get_rayhunter_daemon();
|
||||
let s = get_rayhunter_daemon("/media/card");
|
||||
assert!(s.contains("mount /dev/mmcblk0p1 /media/card"));
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rayhunter"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
|
||||
|
||||
@@ -19,14 +19,12 @@ bytes = "1.5.0"
|
||||
chrono = "0.4.31"
|
||||
crc = "3.0.1"
|
||||
deku = { version = "0.18.0", features = ["logging"] }
|
||||
env_logger = "0.10.1"
|
||||
libc = "0.2.150"
|
||||
log = "0.4.20"
|
||||
nix = { version = "0.29.0", features = ["feature"] }
|
||||
pcap-file-tokio = "0.1.0"
|
||||
thiserror = "1.0.50"
|
||||
telcom-parser = { path = "../telcom-parser" }
|
||||
tokio = { version = "1.44.2", features = ["full"] }
|
||||
futures-core = "0.3.30"
|
||||
futures = "0.3.30"
|
||||
tokio = { version = "1.44.2", default-features = false }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
+28
-16
@@ -6,7 +6,7 @@ use crate::hdlc::hdlc_encapsulate;
|
||||
use crate::log_codes;
|
||||
|
||||
use deku::prelude::*;
|
||||
use futures_core::TryStream;
|
||||
use futures::TryStream;
|
||||
use log::{error, info};
|
||||
use std::io::ErrorKind;
|
||||
use std::os::fd::AsRawFd;
|
||||
@@ -251,6 +251,7 @@ impl DiagDevice {
|
||||
//
|
||||
// TPLINK M7350 v5 source code can be downloaded at https://www.tp-link.com/de/support/gpl-code/?app=omada
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct diag_logging_mode_param_t {
|
||||
req_mode: u32,
|
||||
peripheral_mask: u32,
|
||||
@@ -261,30 +262,41 @@ struct diag_logging_mode_param_t {
|
||||
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") {
|
||||
let try_params: &[diag_logging_mode_param_t] = &[
|
||||
// tplink M7350 HW revision 3-8 need this mode
|
||||
#[cfg(feature = "tplink")]
|
||||
diag_logging_mode_param_t {
|
||||
req_mode: mode,
|
||||
peripheral_mask: 0,
|
||||
mode_param: 1,
|
||||
}
|
||||
} else {
|
||||
},
|
||||
// tplink M7350 HW revision v9 requires the same parameters as orbic
|
||||
diag_logging_mode_param_t {
|
||||
req_mode: mode,
|
||||
peripheral_mask: u32::MAX,
|
||||
mode_param: 0,
|
||||
}
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
let mut ret = 0;
|
||||
|
||||
for params in try_params {
|
||||
let mut params = *params;
|
||||
ret = libc::ioctl(
|
||||
fd,
|
||||
DIAG_IOCTL_SWITCH_LOGGING,
|
||||
&mut params as *mut diag_logging_mode_param_t,
|
||||
std::mem::size_of::<diag_logging_mode_param_t>(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
if ret == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let ret = libc::ioctl(
|
||||
fd,
|
||||
DIAG_IOCTL_SWITCH_LOGGING,
|
||||
&mut params as *mut _,
|
||||
std::mem::size_of::<diag_logging_mode_param_t>(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
if ret < 0 {
|
||||
let msg = format!(
|
||||
"DIAG_IOCTL_SWITCH_LOGGING ioctl failed with error code {}",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
pushd bin/web
|
||||
npm run build
|
||||
popd
|
||||
cargo build --release --target="armv7-unknown-linux-musleabihf" #--features debug
|
||||
cargo build --profile firmware --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/release/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
||||
adb push target/armv7-unknown-linux-musleabihf/firmware/rayhunter-daemon /data/rayhunter/rayhunter-daemon
|
||||
echo "rebooting the device..."
|
||||
adb shell '/bin/rootshell -c "reboot"'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rootshell"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telcom-parser"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
Reference in New Issue
Block a user