Compare commits

...

14 Commits

Author SHA1 Message Date
Cooper Quintin f2b722ad5f version update 2025-06-04 10:12:08 -07:00
Cooper Quintin 5e2058e7ac update make to use firmware profile 2025-06-04 10:12:08 -07:00
cooperq 60daf4b716 update win docs 2025-06-04 10:11:12 -07:00
Cooper Quintin 4df317b028 dig deeper in the json tree. Fixes #360 2025-06-04 10:10:18 -07:00
Sashanoraa d7fb8b9c85 Move most serial commands to usb shared USB claim with adb 2025-06-04 09:11:06 -07:00
Markus Unterwaditzer d399532494 Add documentation for key input 2025-06-03 14:29:04 -07:00
Cooper Quintin 45df91a364 Update using-rayhunter.md 2025-06-03 14:23:50 -07:00
Matej Kovacic 672ed8c6c6 Update using-rayhunter.md 2025-06-03 14:23:50 -07:00
Matej Kovacic 5c7c7cd766 Add files via upload 2025-06-03 14:21:19 -07:00
Markus Unterwaditzer f41a8d38fe move analysis into diag reader thread as well 2025-06-03 13:58:47 -07:00
Markus Unterwaditzer f9c8c4671e Add basic key input 2025-06-03 13:58:47 -07:00
Markus Unterwaditzer 723b20541e Move business logic out of axum handlers 2025-06-03 13:58:47 -07:00
cooperq 272a4aeabf update docs 2025-06-03 11:49:20 -07:00
cooperq 6ae70556ba fix windows powershell installer and rust installer root process 2025-06-03 11:49:20 -07:00
21 changed files with 306 additions and 144 deletions
Generated
+6 -6
View File
@@ -5,7 +5,7 @@ version = 4
[[package]] [[package]]
name = "adb_client" name = "adb_client"
version = "2.1.11" version = "2.1.11"
source = "git+https://github.com/cooperq/adb_client.git?rev=88b3a3a24fe91d16101e44cebd84bd0ecc74ecdf#88b3a3a24fe91d16101e44cebd84bd0ecc74ecdf" source = "git+https://github.com/gaykitty/adb_client.git?rev=e732fc178a0eb237138e4091059ff5ffa241385a#e732fc178a0eb237138e4091059ff5ffa241385a"
dependencies = [ dependencies = [
"async-io", "async-io",
"base64", "base64",
@@ -1433,7 +1433,7 @@ dependencies = [
[[package]] [[package]]
name = "installer" name = "installer"
version = "0.3.2" version = "0.3.3"
dependencies = [ dependencies = [
"adb_client", "adb_client",
"anyhow", "anyhow",
@@ -2307,7 +2307,7 @@ dependencies = [
[[package]] [[package]]
name = "rayhunter" name = "rayhunter"
version = "0.3.2" version = "0.3.3"
dependencies = [ dependencies = [
"bytes", "bytes",
"chrono", "chrono",
@@ -2326,7 +2326,7 @@ dependencies = [
[[package]] [[package]]
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.3.2" version = "0.3.3"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -2453,7 +2453,7 @@ checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
[[package]] [[package]]
name = "rootshell" name = "rootshell"
version = "0.3.2" version = "0.3.3"
dependencies = [ dependencies = [
"nix", "nix",
] ]
@@ -2826,7 +2826,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "telcom-parser" name = "telcom-parser"
version = "0.3.2" version = "0.3.3"
dependencies = [ dependencies = [
"asn1-codecs", "asn1-codecs",
"asn1-compiler", "asn1-compiler",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter-daemon" name = "rayhunter-daemon"
version = "0.3.2" version = "0.3.3"
edition = "2021" edition = "2021"
[features] [features]
+2
View File
@@ -11,6 +11,7 @@ pub struct Config {
pub ui_level: u8, pub ui_level: u8,
pub enable_dummy_analyzer: bool, pub enable_dummy_analyzer: bool,
pub colorblind_mode: bool, pub colorblind_mode: bool,
pub key_input_mode: u8,
} }
impl Default for Config { impl Default for Config {
@@ -22,6 +23,7 @@ impl Default for Config {
ui_level: 1, ui_level: 1,
enable_dummy_analyzer: false, enable_dummy_analyzer: false,
colorblind_mode: false, colorblind_mode: false,
key_input_mode: 1,
} }
} }
} }
+9 -4
View File
@@ -4,6 +4,7 @@ mod diag;
mod display; mod display;
mod dummy_analyzer; mod dummy_analyzer;
mod error; mod error;
mod key_input;
mod pcap; mod pcap;
mod qmdl_store; mod qmdl_store;
mod server; mod server;
@@ -175,7 +176,7 @@ async fn main() -> Result<(), RayhunterError> {
let store = init_qmdl_store(&config).await?; let store = init_qmdl_store(&config).await?;
let analysis_status = AnalysisStatus::new(&store); let analysis_status = AnalysisStatus::new(&store);
let qmdl_store_lock = Arc::new(RwLock::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 (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5); let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
let mut maybe_ui_shutdown_tx = None; let mut maybe_ui_shutdown_tx = None;
@@ -193,13 +194,17 @@ async fn main() -> Result<(), RayhunterError> {
run_diag_read_thread( run_diag_read_thread(
&task_tracker, &task_tracker,
dev, dev,
rx, diag_rx,
ui_update_tx.clone(), ui_update_tx.clone(),
qmdl_store_lock.clone(), qmdl_store_lock.clone(),
analysis_tx.clone(),
config.enable_dummy_analyzer, config.enable_dummy_analyzer,
); );
info!("Starting UI"); info!("Starting UI");
display::update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx); 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::<()>(); let (server_shutdown_tx, server_shutdown_rx) = oneshot::channel::<()>();
info!("create shutdown thread"); info!("create shutdown thread");
@@ -213,7 +218,7 @@ async fn main() -> Result<(), RayhunterError> {
); );
run_ctrl_c_thread( run_ctrl_c_thread(
&task_tracker, &task_tracker,
tx.clone(), diag_tx.clone(),
server_shutdown_tx, server_shutdown_tx,
maybe_ui_shutdown_tx, maybe_ui_shutdown_tx,
qmdl_store_lock.clone(), qmdl_store_lock.clone(),
@@ -221,7 +226,7 @@ async fn main() -> Result<(), RayhunterError> {
); );
let state = Arc::new(ServerState { let state = Arc::new(ServerState {
qmdl_store_lock: qmdl_store_lock.clone(), qmdl_store_lock: qmdl_store_lock.clone(),
diag_device_ctrl_sender: tx, diag_device_ctrl_sender: diag_tx,
ui_update_sender: ui_update_tx, ui_update_sender: ui_update_tx,
debug_mode: config.debug_mode, debug_mode: config.debug_mode,
analysis_status_lock, analysis_status_lock,
+44 -63
View File
@@ -7,7 +7,7 @@ use axum::http::header::CONTENT_TYPE;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use log::{debug, error, info}; use log::{debug, error, info, warn};
use rayhunter::diag::DataType; use rayhunter::diag::DataType;
use rayhunter::diag_device::DiagDevice; use rayhunter::diag_device::DiagDevice;
use rayhunter::qmdl::QmdlWriter; use rayhunter::qmdl::QmdlWriter;
@@ -24,7 +24,7 @@ use crate::server::ServerState;
pub enum DiagDeviceCtrlMessage { pub enum DiagDeviceCtrlMessage {
StopRecording, StopRecording,
StartRecording((QmdlWriter<File>, File)), StartRecording,
Exit, Exit,
} }
@@ -34,6 +34,7 @@ pub fn run_diag_read_thread(
mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>, mut qmdl_file_rx: Receiver<DiagDeviceCtrlMessage>,
ui_update_sender: Sender<display::DisplayState>, ui_update_sender: Sender<display::DisplayState>,
qmdl_store_lock: Arc<RwLock<RecordingStore>>, qmdl_store_lock: Arc<RwLock<RecordingStore>>,
analysis_sender: Sender<AnalysisCtrlMessage>,
enable_dummy_analyzer: bool, enable_dummy_analyzer: bool,
) { ) {
task_tracker.spawn(async move { task_tracker.spawn(async move {
@@ -46,20 +47,56 @@ pub fn run_diag_read_thread(
tokio::select! { tokio::select! {
msg = qmdl_file_rx.recv() => { msg = qmdl_file_rx.recv() => {
match msg { match msg {
Some(DiagDeviceCtrlMessage::StartRecording((new_writer, new_analysis_file))) => { Some(DiagDeviceCtrlMessage::StartRecording) => {
maybe_qmdl_writer = Some(new_writer); 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 { if let Some(analysis_writer) = maybe_analysis_writer {
analysis_writer.close().await.expect("failed to close 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 maybe_analysis_writer = Some(AnalysisWriter::new(new_analysis_file, enable_dummy_analyzer).await
.expect("failed to write to analysis file")); .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) => { 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; maybe_qmdl_writer = None;
if let Some(analysis_writer) = maybe_analysis_writer { if let Some(analysis_writer) = maybe_analysis_writer {
analysis_writer.close().await.expect("failed to close analysis writer"); analysis_writer.close().await.expect("failed to close analysis writer");
} }
maybe_analysis_writer = None; 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 // None means all the Senders have been dropped, so it's
// time to go // time to go
@@ -125,37 +162,15 @@ pub async fn start_recording(
if state.debug_mode { if state.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); 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 state
.diag_device_ctrl_sender .diag_device_ctrl_sender
.send(DiagDeviceCtrlMessage::StartRecording(( .send(DiagDeviceCtrlMessage::StartRecording)
qmdl_writer,
analysis_file,
)))
.await .await
.map_err(|e| { .map_err(|e| {
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
format!("couldn't send stop recording message: {}", e), format!("couldn't send start 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),
) )
})?; })?;
@@ -168,30 +183,6 @@ pub async fn stop_recording(
if state.debug_mode { if state.debug_mode {
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string())); 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 state
.diag_device_ctrl_sender .diag_device_ctrl_sender
.send(DiagDeviceCtrlMessage::StopRecording) .send(DiagDeviceCtrlMessage::StopRecording)
@@ -202,16 +193,6 @@ pub async fn stop_recording(
format!("couldn't send stop recording message: {}", e), 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())) Ok((StatusCode::ACCEPTED, "ok".to_string()))
} }
+100
View File
@@ -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));
}
}
+3 -3
View File
@@ -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 => { const events: Event[] = analysis_json.events.map((event_json: any): Event | null => {
if (event_json === null) { if (event_json === null) {
return null; return null;
} else if (event_json.event_type === "Informational") { } else if (event_json.event_type.type === "Informational") {
num_informational_logs += 1; num_informational_logs += 1;
return { return {
type: EventType.Informational, type: EventType.Informational,
@@ -82,8 +82,8 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Anal
num_warnings += 1; num_warnings += 1;
return { return {
type: EventType.Warning, type: EventType.Warning,
severity: event_json.severity === "High" ? Severity.High : severity: event_json.event_type.severity === "High" ? Severity.High :
event_json.severity === "Medium" ? Severity.Medium : Severity.Low, event_json.event_type.severity === "Medium" ? Severity.Medium : Severity.Low,
message: event_json.message, message: event_json.message,
}; };
} }
@@ -22,7 +22,7 @@
Storage Storage
</th> </th>
<td class={table_cell_classes}> <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> </td>
</tr> </tr>
<tr class="border-b"> <tr class="border-b">
+5 -1
View File
@@ -14,5 +14,9 @@ colorblind_mode = false
# #
# TP-Link with one-bit display: # TP-Link with one-bit display:
# 0 = invisible mode # 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 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

+16 -6
View File
@@ -11,12 +11,22 @@ Windows support in Rayhunter's installer is a work-in-progress. Depending on the
## Orbic ## 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
![Zadig](./zadig2.png)
1. Select 'RNDIS (Interface 0)'
![Zadig](./zadig.png)
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 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` . 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. 1. Open a powershell terminal by pressing Win+R and typing `powershell` and hitting enter.
5. Run the following powershell command `Set-ExecutionPolicy remotesigned` 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 by double clicking on `install.ps1`. A powershell window will launch. 5. Run the install script: `.\installer.exe orbic` and hit enter.
The device will restart multiple times over the next few minutes. - 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!` - 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. 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.
+16 -5
View File
@@ -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. 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.
![Rayhunter_0 3 2](./Rayhunter_0.3.2.png)
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. 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: 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) 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). 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. 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. 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 **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. * 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). * 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`. * 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.
* **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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+5 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "installer" name = "installer"
version = "0.3.2" version = "0.3.3"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -21,13 +21,13 @@ tokio-retry2 = "0.5.7"
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
[target.'cfg(target_os = "linux")'.dependencies.adb_client] [target.'cfg(target_os = "linux")'.dependencies.adb_client]
git = "https://github.com/cooperq/adb_client.git" git = "https://github.com/gaykitty/adb_client.git"
rev = "88b3a3a24fe91d16101e44cebd84bd0ecc74ecdf" rev = "e732fc178a0eb237138e4091059ff5ffa241385a"
default-features = false default-features = false
features = ["trans-nusb"] features = ["trans-nusb"]
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client] [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.adb_client]
git = "https://github.com/cooperq/adb_client.git" git = "https://github.com/gaykitty/adb_client.git"
rev = "88b3a3a24fe91d16101e44cebd84bd0ecc74ecdf" rev = "e732fc178a0eb237138e4091059ff5ffa241385a"
default-features = false default-features = false
features = ["trans-libusb"] features = ["trans-libusb"]
+17 -16
View File
@@ -2,20 +2,14 @@ $global:adb = ".\platform-tools-latest-windows\platform-tools\adb.exe"
$global:serial = ".\installer-windows-x86_64\installer.exe" $global:serial = ".\installer-windows-x86_64\installer.exe"
function _adb_push { function _adb_push {
& $global:adb -d push @args | Out-Null & $global:adb -d push @args *> $null
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
write-host "push exited with exit code $($exitCode)"
}
return $exitCode return $exitCode
} }
function _adb_shell { function _adb_shell {
& $global:adb -d shell @args | Out-Null & $global:adb -d shell @args *> $null
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
write-host "shell exited with exit code $($exitCode)"
}
return $exitCode return $exitCode
} }
@@ -36,7 +30,7 @@ function _wait_for_atfwd_daemon {
function force_debug_mode { function force_debug_mode {
write-host "Using adb at $($global:adb)" write-host "Using adb at $($global:adb)"
write-host "Forcing a switch into debug mode to enable 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 write-host "adb enabled, waiting for reboot..." -nonewline
_wait_for_adb_shell _wait_for_adb_shell
write-host " it's alive!" write-host " it's alive!"
@@ -58,7 +52,8 @@ function _serial {
} }
function setup_rootshell { function setup_rootshell {
_adb_push "rootshell" "/tmp" write-host "setting up rootshell"
_adb_push "rootshell" "/tmp" | Out-null
write-host "cp..." write-host "cp..."
_serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host _serial "AT+SYSCMD=cp /tmp/rootshell /bin/rootshell" | Out-Host
start-sleep -seconds 1 start-sleep -seconds 1
@@ -68,19 +63,20 @@ function setup_rootshell {
write-host "chmod..." write-host "chmod..."
_serial "AT+SYSCMD=chmod 4755 /bin/rootshell" | Out-Host _serial "AT+SYSCMD=chmod 4755 /bin/rootshell" | Out-Host
start-sleep -seconds 1 start-sleep -seconds 1
_adb_shell '/bin/rootshell -c id' _adb_shell '/bin/rootshell -c id' | Out-null
write-host "we have root!" write-host "we have root!"
} }
function setup_rayhunter { function setup_rayhunter {
write-host "installing rayhunter..."
_serial "AT+SYSCMD=mkdir -p /data/rayhunter" | Out-Host _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 _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 _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 _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=mv /tmp/misc-daemon /etc/init.d/misc-daemon" | Out-Host
_serial "AT+SYSCMD=chmod 755 /data/rayhunter/rayhunter-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 write-host "checking for rayhunter server..." -nonewline
$seconds = 0 $seconds = 0
do { do {
$resp = invoke-webrequest -uri $URL try {
$resp = invoke-webrequest -uri $URL
} catch {
# Fail silently
$resp = $null
}
if ($resp.statuscode -eq 200) { if ($resp.statuscode -eq 200) {
write-host "success!" write-host "success!"
write-host "you can access rayhunter at $($URL)" write-host "you can access rayhunter at $($URL)"
+76 -28
View File
@@ -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. Please close any program that might be using your USB devices.
If you have adb installed you may need to kill the adb daemon"#; 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. 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. Please close any program that might be using your Orbic.
If you have adb installed you may need to kill the adb daemon"#; If you have adb installed you may need to kill the adb daemon"#;
const VENDOR_ID: u16 = 0x05c6; const VENDOR_ID: u16 = 0x05c6;
const PRODUCT_ID: u16 = 0xf601; 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 { macro_rules! echo {
($($arg:tt)*) => { ($($arg:tt)*) => {
print!($($arg)*); print!($($arg)*);
@@ -41,12 +49,11 @@ macro_rules! echo {
pub async fn install() -> Result<()> { pub async fn install() -> Result<()> {
let mut adb_device = force_debug_mode().await?; let mut adb_device = force_debug_mode().await?;
let serial_interface = open_orbic()?.ok_or_else(|| anyhow!(ORBIC_NOT_FOUND))?;
echo!("Installing rootshell... "); echo!("Installing rootshell... ");
setup_rootshell(&serial_interface, &mut adb_device).await?; setup_rootshell(&mut adb_device).await?;
println!("done"); println!("done");
echo!("Installing rayhunter... "); 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"); println!("done");
echo!("Testing rayhunter... "); echo!("Testing rayhunter... ");
test_rayhunter(&mut adb_device).await?; test_rayhunter(&mut adb_device).await?;
@@ -66,6 +73,7 @@ async fn force_debug_mode() -> Result<ADBUSBDevice> {
enable_command_mode()?; enable_command_mode()?;
echo!("ADB enabled, waiting for reboot... "); echo!("ADB enabled, waiting for reboot... ");
let mut adb_device = get_adb().await?; let mut adb_device = get_adb().await?;
adb_setup_serial(&mut adb_device).await?;
println!("it's alive!"); println!("it's alive!");
echo!("Waiting for atfwd_daemon to startup... "); echo!("Waiting for atfwd_daemon to startup... ");
adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?; adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?;
@@ -74,22 +82,20 @@ async fn force_debug_mode() -> Result<ADBUSBDevice> {
} }
async fn setup_rootshell( async fn setup_rootshell(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice, adb_device: &mut ADBUSBDevice,
) -> Result<()> { ) -> Result<()> {
let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL")); let rootshell_bin = include_bytes!(env!("FILE_ROOTSHELL"));
install_file( install_file(
serial_interface,
adb_device, adb_device,
"/bin/rootshell", "/bin/rootshell",
rootshell_bin, rootshell_bin,
) )
.await?; .await?;
tokio::time::sleep(Duration::from_secs(1)).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; 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"])?; let output = adb_command(adb_device, &["/bin/rootshell", "-c", "id"])?;
if !output.contains("uid=0") { if !output.contains("uid=0") {
bail!("rootshell is not giving us root."); bail!("rootshell is not giving us root.");
@@ -98,45 +104,40 @@ async fn setup_rootshell(
} }
async fn setup_rayhunter( async fn setup_rayhunter(
serial_interface: &Interface,
mut adb_device: ADBUSBDevice, mut adb_device: ADBUSBDevice,
) -> Result<ADBUSBDevice> { ) -> Result<ADBUSBDevice> {
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_ORBIC")); 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( install_file(
serial_interface,
&mut adb_device, &mut adb_device,
"/data/rayhunter/rayhunter-daemon", "/data/rayhunter/rayhunter-daemon",
rayhunter_daemon_bin, rayhunter_daemon_bin,
) )
.await?; .await?;
install_file( install_file(
serial_interface,
&mut adb_device, &mut adb_device,
"/data/rayhunter/config.toml", "/data/rayhunter/config.toml",
CONFIG_TOML.as_bytes(), CONFIG_TOML.as_bytes(),
) )
.await?; .await?;
install_file( install_file(
serial_interface,
&mut adb_device, &mut adb_device,
"/etc/init.d/rayhunter_daemon", "/etc/init.d/rayhunter_daemon",
RAYHUNTER_DAEMON_INIT.as_bytes(), RAYHUNTER_DAEMON_INIT.as_bytes(),
) )
.await?; .await?;
install_file( install_file(
serial_interface,
&mut adb_device, &mut adb_device,
"/etc/init.d/misc-daemon", "/etc/init.d/misc-daemon",
include_bytes!("../../dist/scripts/misc-daemon"), include_bytes!("../../dist/scripts/misc-daemon"),
) )
.await?; .await?;
at_syscmd(serial_interface, "chmod 755 /etc/init.d/rayhunter_daemon").await?; adb_at_syscmd(&mut adb_device, "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/misc-daemon").await?;
println!("done"); println!("done");
echo!("Waiting for reboot... "); 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) // first wait for shutdown (it can take ~10s)
tokio::time::timeout(Duration::from_secs(30), async { tokio::time::timeout(Duration::from_secs(30), async {
while let Ok(dev) = adb_echo_test(adb_device).await { while let Ok(dev) = adb_echo_test(adb_device).await {
@@ -169,7 +170,6 @@ async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> {
} }
async fn install_file( async fn install_file(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice, adb_device: &mut ADBUSBDevice,
dest: &str, dest: &str,
payload: &[u8], payload: &[u8],
@@ -177,7 +177,7 @@ async fn install_file(
const MAX_FAILURES: u32 = 5; const MAX_FAILURES: u32 = 5;
let mut failures = 0; let mut failures = 0;
loop { loop {
match install_file_impl(serial_interface, adb_device, dest, payload).await { match install_file_impl(adb_device, dest, payload).await {
Ok(()) => return Ok(()), Ok(()) => return Ok(()),
Err(e) => { Err(e) => {
if failures > MAX_FAILURES { if failures > MAX_FAILURES {
@@ -192,7 +192,6 @@ async fn install_file(
} }
async fn install_file_impl( async fn install_file_impl(
serial_interface: &Interface,
adb_device: &mut ADBUSBDevice, adb_device: &mut ADBUSBDevice,
dest: &str, dest: &str,
mut payload: &[u8], mut payload: &[u8],
@@ -209,7 +208,7 @@ async fn install_file_impl(
let file_hash_bytes = hasher.finalize(); let file_hash_bytes = hasher.finalize();
let file_hash = format!("{file_hash_bytes:x}"); let file_hash = format!("{file_hash_bytes:x}");
adb_device.push(&mut payload, &push_tmp_path)?; 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 let file_info = adb_device
.stat(dest) .stat(dest)
.context("Failed to stat transfered file")?; .context("Failed to stat transfered file")?;
@@ -251,7 +250,7 @@ async fn get_adb() -> Result<ADBUSBDevice> {
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => { Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => {
bail!(ORBIC_BUSY); bail!(ORBIC_BUSY);
} }
#[cfg(target_os = "macos")] #[cfg(any(target_os = "macos", target_os="windows"))]
Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => { Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::PermissionDenied => {
bail!(ORBIC_BUSY_MAC); bail!(ORBIC_BUSY_MAC);
} }
@@ -328,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<()> { async fn adb_setup_serial(adb_device: &mut ADBUSBDevice) -> Result<()> {
send_serial_cmd(interface, &format!("AT+SYSCMD={command}")).await 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 /// Sends an AT command to the usb device over the serial port
/// ///
/// First establish a USB handle and context by calling `open_orbic(<T>) /// First establish a USB handle and context by calling `open_orbic(<T>)
@@ -407,7 +455,7 @@ pub fn enable_command_mode() -> Result<()> {
index: 0, index: 0,
}; };
let interface = device let interface = device
.detach_and_claim_interface(1) .detach_and_claim_interface(RNDIS_INTERFACE)
.context("detach_and_claim_interface(1) failed")?; .context("detach_and_claim_interface(1) failed")?;
if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) { if let Err(e) = interface.control_out_blocking(enable_command_mode, &[], timeout) {
// If the device reboots while the command is still executing we // If the device reboots while the command is still executing we
@@ -428,7 +476,7 @@ pub fn open_orbic() -> Result<Option<Interface>> {
// Device after initial mode switch // Device after initial mode switch
if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? { if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? {
let interface = device 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")?; .context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface)); return Ok(Some(interface));
} }
@@ -436,7 +484,7 @@ pub fn open_orbic() -> Result<Option<Interface>> {
// Device with rndis enabled as well // Device with rndis enabled as well
if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? { if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? {
let interface = device 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")?; .context("detach_and_claim_interface(1) failed")?;
return Ok(Some(interface)); return Ok(Some(interface));
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rayhunter" name = "rayhunter"
version = "0.3.2" version = "0.3.3"
edition = "2021" edition = "2021"
description = "Realtime cellular data decoding and analysis for IMSI catcher detection" description = "Realtime cellular data decoding and analysis for IMSI catcher detection"
+2 -2
View File
@@ -2,8 +2,8 @@
pushd bin/web pushd bin/web
npm run build npm run build
popd 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 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..." echo "rebooting the device..."
adb shell '/bin/rootshell -c "reboot"' adb shell '/bin/rootshell -c "reboot"'
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rootshell" name = "rootshell"
version = "0.3.2" version = "0.3.3"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "telcom-parser" name = "telcom-parser"
version = "0.3.2" version = "0.3.3"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html