mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-09 22:33:32 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 369a1ba3af | |||
| d6d10ca3a4 | |||
| 728fbe0f4d |
@@ -7,4 +7,3 @@
|
||||
dist/config.toml.in eol=lf
|
||||
dist/scripts/misc-daemon eol=lf
|
||||
dist/scripts/rayhunter_daemon eol=lf
|
||||
scripts/*.sh eol=lf
|
||||
|
||||
Generated
-1
@@ -2759,7 +2759,6 @@ dependencies = [
|
||||
"installer",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-opener",
|
||||
|
||||
@@ -6,4 +6,3 @@ title = "Rayhunter - An IMSI Catcher Catcher"
|
||||
|
||||
[output.html]
|
||||
edit-url-template = "https://github.com/efforg/rayhunter/edit/main/{path}"
|
||||
additional-css = ["doc/custom.css"]
|
||||
|
||||
@@ -20,8 +20,6 @@ pub struct Config {
|
||||
pub ntfy_url: Option<String>,
|
||||
pub enabled_notifications: Vec<NotificationType>,
|
||||
pub analyzers: AnalyzerConfig,
|
||||
pub min_space_to_start_recording_mb: u64,
|
||||
pub min_space_to_continue_recording_mb: u64,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -37,8 +35,6 @@ impl Default for Config {
|
||||
analyzers: AnalyzerConfig::default(),
|
||||
ntfy_url: None,
|
||||
enabled_notifications: vec![NotificationType::Warning, NotificationType::LowBattery],
|
||||
min_space_to_start_recording_mb: 1,
|
||||
min_space_to_continue_recording_mb: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-181
@@ -27,15 +27,10 @@ use crate::display;
|
||||
use crate::notifications::{Notification, NotificationType};
|
||||
use crate::qmdl_store::{RecordingStore, RecordingStoreError};
|
||||
use crate::server::ServerState;
|
||||
use crate::stats::DiskStats;
|
||||
|
||||
const DISK_CHECK_BYTES_INTERVAL: usize = 256 * 1024;
|
||||
|
||||
pub enum DiagDeviceCtrlMessage {
|
||||
StopRecording,
|
||||
StartRecording {
|
||||
response_tx: Option<oneshot::Sender<Result<(), String>>>,
|
||||
},
|
||||
StartRecording,
|
||||
DeleteEntry {
|
||||
name: String,
|
||||
response_tx: oneshot::Sender<Result<(), RecordingStoreError>>,
|
||||
@@ -51,12 +46,8 @@ pub struct DiagTask {
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
notification_channel: tokio::sync::mpsc::Sender<Notification>,
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
state: DiagState,
|
||||
max_type_seen: EventType,
|
||||
bytes_since_space_check: usize,
|
||||
low_space_warned: bool,
|
||||
}
|
||||
|
||||
enum DiagState {
|
||||
@@ -67,99 +58,36 @@ enum DiagState {
|
||||
Stopped,
|
||||
}
|
||||
|
||||
enum DiskSpaceCheck {
|
||||
Ok(u64),
|
||||
Warning(u64),
|
||||
Critical(u64),
|
||||
Failed,
|
||||
}
|
||||
|
||||
fn check_disk_space(path: &std::path::Path, warning_mb: u64, critical_mb: u64) -> DiskSpaceCheck {
|
||||
match DiskStats::new(path.to_str().unwrap()) {
|
||||
Ok(stats) => {
|
||||
let available_mb = stats.available_bytes.unwrap_or(0) / 1024 / 1024;
|
||||
if available_mb < critical_mb {
|
||||
DiskSpaceCheck::Critical(available_mb)
|
||||
} else if available_mb < warning_mb {
|
||||
DiskSpaceCheck::Warning(available_mb)
|
||||
} else {
|
||||
DiskSpaceCheck::Ok(available_mb)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to check disk space: {e}");
|
||||
DiskSpaceCheck::Failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagTask {
|
||||
fn new(
|
||||
ui_update_sender: Sender<display::DisplayState>,
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
notification_channel: tokio::sync::mpsc::Sender<Notification>,
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
ui_update_sender,
|
||||
analysis_sender,
|
||||
analyzer_config,
|
||||
notification_channel,
|
||||
min_space_to_start_mb,
|
||||
min_space_to_continue_mb,
|
||||
state: DiagState::Stopped,
|
||||
max_type_seen: EventType::Informational,
|
||||
bytes_since_space_check: 0,
|
||||
low_space_warned: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start recording, returning an error if disk space is too low.
|
||||
async fn start(&mut self, qmdl_store: &mut RecordingStore) -> Result<(), String> {
|
||||
/// Start recording
|
||||
async fn start(&mut self, qmdl_store: &mut RecordingStore) {
|
||||
self.max_type_seen = EventType::Informational;
|
||||
self.bytes_since_space_check = 0;
|
||||
self.low_space_warned = false;
|
||||
|
||||
match check_disk_space(
|
||||
&qmdl_store.path,
|
||||
self.min_space_to_start_mb,
|
||||
self.min_space_to_continue_mb,
|
||||
) {
|
||||
DiskSpaceCheck::Critical(mb) | DiskSpaceCheck::Warning(mb) => {
|
||||
let msg = format!(
|
||||
"Insufficient disk space: {}MB available, {}MB required",
|
||||
mb, self.min_space_to_start_mb
|
||||
);
|
||||
error!("{msg}");
|
||||
return Err(msg);
|
||||
}
|
||||
DiskSpaceCheck::Ok(mb) => {
|
||||
info!("Starting recording with {}MB disk space available", mb);
|
||||
}
|
||||
DiskSpaceCheck::Failed => {}
|
||||
}
|
||||
|
||||
let (qmdl_file, analysis_file) = match qmdl_store.new_entry().await {
|
||||
Ok(files) => files,
|
||||
Err(e) => {
|
||||
let msg = format!("failed creating QMDL file entry: {e}");
|
||||
error!("{msg}");
|
||||
return Err(msg);
|
||||
}
|
||||
};
|
||||
let (qmdl_file, analysis_file) = qmdl_store
|
||||
.new_entry()
|
||||
.await
|
||||
.expect("failed creating QMDL file entry");
|
||||
self.stop_current_recording().await;
|
||||
let qmdl_writer = QmdlWriter::new(qmdl_file);
|
||||
let analysis_writer = match AnalysisWriter::new(analysis_file, &self.analyzer_config).await
|
||||
{
|
||||
Ok(writer) => Box::new(writer),
|
||||
Err(e) => {
|
||||
let msg = format!("failed to create analysis writer: {e}");
|
||||
error!("{msg}");
|
||||
return Err(msg);
|
||||
}
|
||||
};
|
||||
let analysis_writer = AnalysisWriter::new(analysis_file, &self.analyzer_config)
|
||||
.await
|
||||
.map(Box::new)
|
||||
.expect("failed to write to analysis file");
|
||||
self.state = DiagState::Recording {
|
||||
qmdl_writer,
|
||||
analysis_writer,
|
||||
@@ -171,17 +99,11 @@ impl DiagTask {
|
||||
{
|
||||
warn!("couldn't send ui update message: {e}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop recording, optionally annotating the entry with a reason.
|
||||
async fn stop(&mut self, qmdl_store: &mut RecordingStore, reason: Option<String>) {
|
||||
/// Stop recording
|
||||
async fn stop(&mut self, qmdl_store: &mut RecordingStore) {
|
||||
self.stop_current_recording().await;
|
||||
if let Some(reason) = reason
|
||||
&& let Err(e) = qmdl_store.set_current_stop_reason(reason).await
|
||||
{
|
||||
warn!("couldn't set stop reason: {e}");
|
||||
}
|
||||
if let Some((_, entry)) = qmdl_store.get_current_entry()
|
||||
&& let Err(e) = self
|
||||
.analysis_sender
|
||||
@@ -210,7 +132,7 @@ impl DiagTask {
|
||||
name: &str,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
if qmdl_store.is_current_entry(name) {
|
||||
self.stop(qmdl_store, None).await;
|
||||
self.stop(qmdl_store).await;
|
||||
}
|
||||
let res = qmdl_store.delete_entry(name).await;
|
||||
if let Err(e) = res.as_ref() {
|
||||
@@ -223,7 +145,7 @@ impl DiagTask {
|
||||
&mut self,
|
||||
qmdl_store: &mut RecordingStore,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
self.stop(qmdl_store, None).await;
|
||||
self.stop(qmdl_store).await;
|
||||
let res = qmdl_store.delete_all_entries().await;
|
||||
if let Err(e) = res.as_ref() {
|
||||
error!("Error deleting QMDL entries {e}");
|
||||
@@ -261,56 +183,10 @@ impl DiagTask {
|
||||
analysis_writer,
|
||||
} = &mut self.state
|
||||
{
|
||||
if self.bytes_since_space_check >= DISK_CHECK_BYTES_INTERVAL {
|
||||
self.bytes_since_space_check = 0;
|
||||
match check_disk_space(
|
||||
&qmdl_store.path,
|
||||
self.min_space_to_start_mb,
|
||||
self.min_space_to_continue_mb,
|
||||
) {
|
||||
DiskSpaceCheck::Critical(mb) => {
|
||||
let reason = format!(
|
||||
"Disk space critically low ({}MB free), recording stopped automatically",
|
||||
mb
|
||||
);
|
||||
error!("{reason}");
|
||||
|
||||
self.notification_channel
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
reason.clone(),
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
}
|
||||
DiskSpaceCheck::Warning(mb) => {
|
||||
if !self.low_space_warned {
|
||||
self.low_space_warned = true;
|
||||
warn!("Disk space low: {}MB remaining", mb);
|
||||
self.notification_channel
|
||||
.send(Notification::new(
|
||||
NotificationType::Warning,
|
||||
format!("Disk space low: {}MB free", mb),
|
||||
Some(Duration::from_secs(30)),
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = qmdl_writer.write_container(&container).await {
|
||||
let reason = format!("failed to write to QMDL (disk full?): {e}");
|
||||
error!("{reason}");
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
}
|
||||
qmdl_writer
|
||||
.write_container(&container)
|
||||
.await
|
||||
.expect("failed to write to QMDL writer");
|
||||
debug!(
|
||||
"total QMDL bytes written: {}, updating manifest...",
|
||||
qmdl_writer.total_written
|
||||
@@ -318,25 +194,15 @@ impl DiagTask {
|
||||
let index = qmdl_store
|
||||
.current_entry
|
||||
.expect("DiagDevice had qmdl_writer, but QmdlStore didn't have current entry???");
|
||||
if let Err(e) = qmdl_store
|
||||
qmdl_store
|
||||
.update_entry_qmdl_size(index, qmdl_writer.total_written)
|
||||
.await
|
||||
{
|
||||
let reason = format!("failed to update manifest (disk full?): {e}");
|
||||
error!("{reason}");
|
||||
self.stop(qmdl_store, Some(reason)).await;
|
||||
return;
|
||||
}
|
||||
.expect("failed to update qmdl file size");
|
||||
debug!("done!");
|
||||
let container_bytes: usize = container.messages.iter().map(|m| m.data.len()).sum();
|
||||
self.bytes_since_space_check += container_bytes;
|
||||
let max_type = match analysis_writer.analyze(container).await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
warn!("failed to analyze container: {e}");
|
||||
EventType::Informational
|
||||
}
|
||||
};
|
||||
let max_type = analysis_writer
|
||||
.analyze(container)
|
||||
.await
|
||||
.expect("failed to analyze container");
|
||||
|
||||
if max_type > EventType::Informational {
|
||||
info!("a heuristic triggered on this run!");
|
||||
@@ -378,30 +244,25 @@ pub fn run_diag_read_thread(
|
||||
analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
analyzer_config: AnalyzerConfig,
|
||||
notification_channel: tokio::sync::mpsc::Sender<Notification>,
|
||||
min_space_to_start_mb: u64,
|
||||
min_space_to_continue_mb: u64,
|
||||
) {
|
||||
task_tracker.spawn(async move {
|
||||
let mut diag_stream = pin!(dev.as_stream().into_stream());
|
||||
let mut diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config, notification_channel, min_space_to_start_mb, min_space_to_continue_mb);
|
||||
let mut diag_task = DiagTask::new(ui_update_sender, analysis_sender, analyzer_config, notification_channel);
|
||||
qmdl_file_tx
|
||||
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })
|
||||
.send(DiagDeviceCtrlMessage::StartRecording)
|
||||
.await
|
||||
.unwrap();
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = qmdl_file_rx.recv() => {
|
||||
match msg {
|
||||
Some(DiagDeviceCtrlMessage::StartRecording { response_tx }) => {
|
||||
Some(DiagDeviceCtrlMessage::StartRecording) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
let result = diag_task.start(qmdl_store.deref_mut()).await;
|
||||
if let Some(tx) = response_tx {
|
||||
tx.send(result).ok();
|
||||
}
|
||||
diag_task.start(qmdl_store.deref_mut()).await;
|
||||
},
|
||||
Some(DiagDeviceCtrlMessage::StopRecording) => {
|
||||
let mut qmdl_store = qmdl_store_lock.write().await;
|
||||
diag_task.stop(qmdl_store.deref_mut(), None).await;
|
||||
diag_task.stop(qmdl_store.deref_mut()).await;
|
||||
},
|
||||
// None means all the Senders have been dropped, so it's
|
||||
// time to go
|
||||
@@ -451,12 +312,9 @@ pub async fn start_recording(
|
||||
return Err((StatusCode::FORBIDDEN, "server is in debug mode".to_string()));
|
||||
}
|
||||
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
state
|
||||
.diag_device_ctrl_sender
|
||||
.send(DiagDeviceCtrlMessage::StartRecording {
|
||||
response_tx: Some(response_tx),
|
||||
})
|
||||
.send(DiagDeviceCtrlMessage::StartRecording)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
@@ -465,14 +323,7 @@ pub async fn start_recording(
|
||||
)
|
||||
})?;
|
||||
|
||||
match response_rx.await {
|
||||
Ok(Ok(())) => Ok((StatusCode::ACCEPTED, "ok".to_string())),
|
||||
Ok(Err(reason)) => Err((StatusCode::INSUFFICIENT_STORAGE, reason)),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("failed to receive start recording response: {e}"),
|
||||
)),
|
||||
}
|
||||
Ok((StatusCode::ACCEPTED, "ok".to_string()))
|
||||
}
|
||||
|
||||
/// Stop recording API for web thread
|
||||
|
||||
@@ -81,9 +81,8 @@ pub fn run_key_input_thread(
|
||||
{
|
||||
error!("Failed to send StopRecording: {e}");
|
||||
}
|
||||
if let Err(e) = diag_tx
|
||||
.send(DiagDeviceCtrlMessage::StartRecording { response_tx: None })
|
||||
.await
|
||||
if let Err(e) =
|
||||
diag_tx.send(DiagDeviceCtrlMessage::StartRecording).await
|
||||
{
|
||||
error!("Failed to send StartRecording: {e}");
|
||||
}
|
||||
|
||||
@@ -234,8 +234,6 @@ async fn run_with_config(
|
||||
analysis_tx.clone(),
|
||||
config.analyzers.clone(),
|
||||
notification_service.new_handler(),
|
||||
config.min_space_to_start_recording_mb,
|
||||
config.min_space_to_continue_recording_mb,
|
||||
);
|
||||
info!("Starting UI");
|
||||
|
||||
|
||||
@@ -54,8 +54,6 @@ pub struct ManifestEntry {
|
||||
pub rayhunter_version: Option<String>,
|
||||
pub system_os: Option<String>,
|
||||
pub arch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub stop_reason: Option<String>,
|
||||
}
|
||||
|
||||
impl ManifestEntry {
|
||||
@@ -70,7 +68,6 @@ impl ManifestEntry {
|
||||
rayhunter_version: Some(metadata.rayhunter_version),
|
||||
system_os: Some(metadata.system_os),
|
||||
arch: Some(metadata.arch),
|
||||
stop_reason: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +197,6 @@ impl RecordingStore {
|
||||
rayhunter_version: None,
|
||||
system_os: None,
|
||||
arch: None,
|
||||
stop_reason: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -346,17 +342,6 @@ impl RecordingStore {
|
||||
Some((entry_index, &self.manifest.entries[entry_index]))
|
||||
}
|
||||
|
||||
pub async fn set_current_stop_reason(
|
||||
&mut self,
|
||||
reason: String,
|
||||
) -> Result<(), RecordingStoreError> {
|
||||
if let Some(idx) = self.current_entry {
|
||||
self.manifest.entries[idx].stop_reason = Some(reason);
|
||||
self.write_manifest().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_current_entry(&self, name: &str) -> bool {
|
||||
match self.current_entry {
|
||||
Some(idx) => match self.manifest.entries.get(idx) {
|
||||
|
||||
+68
-32
@@ -1,4 +1,3 @@
|
||||
use std::ffi::CString;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::battery::get_battery_status;
|
||||
@@ -10,10 +9,53 @@ use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use log::error;
|
||||
use rayhunter::gsmtap_parser::get_cached_cell_info;
|
||||
use rayhunter::{Device, util::RuntimeMetadata};
|
||||
use serde::Serialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// LTE cell/signal information from DIAG measurements.
|
||||
/// All fields are optional since they may not be available on all devices
|
||||
/// or may not have been received yet.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CellSignalInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rsrp_dbm: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rsrq_db: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rssi_dbm: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pci: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub earfcn: Option<u32>,
|
||||
}
|
||||
|
||||
/// Get cell/signal information for devices that support DIAG.
|
||||
/// Returns None for devices without DIAG support or if no measurements available.
|
||||
pub fn get_cell_info(device: &Device) -> Option<CellSignalInfo> {
|
||||
match device {
|
||||
// Devices with DIAG support
|
||||
Device::Orbic | Device::Tplink | Device::Tmobile | Device::Wingtech => {
|
||||
let info = get_cached_cell_info();
|
||||
// Only return if we have at least some data
|
||||
if info.rsrp_dbm.is_some() || info.pci.is_some() {
|
||||
Some(CellSignalInfo {
|
||||
rsrp_dbm: info.rsrp_dbm,
|
||||
rsrq_db: info.rsrq_db,
|
||||
rssi_dbm: info.rssi_dbm,
|
||||
pci: info.pci,
|
||||
earfcn: info.earfcn,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
// Devices without DIAG support
|
||||
Device::Pinephone | Device::Uz801 => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SystemStats {
|
||||
pub disk_stats: DiskStats,
|
||||
@@ -21,12 +63,14 @@ pub struct SystemStats {
|
||||
pub runtime_metadata: RuntimeMetadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub battery_status: Option<BatteryState>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cell_info: Option<CellSignalInfo>,
|
||||
}
|
||||
|
||||
impl SystemStats {
|
||||
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
disk_stats: DiskStats::new(qmdl_path)?,
|
||||
disk_stats: DiskStats::new(qmdl_path, device).await?,
|
||||
memory_stats: MemoryStats::new(device).await?,
|
||||
runtime_metadata: RuntimeMetadata::new(),
|
||||
battery_status: match get_battery_status(device).await {
|
||||
@@ -37,6 +81,7 @@ impl SystemStats {
|
||||
None
|
||||
}
|
||||
},
|
||||
cell_info: get_cell_info(device),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -49,42 +94,33 @@ pub struct DiskStats {
|
||||
available_size: String,
|
||||
used_percent: String,
|
||||
mounted_on: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub available_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
impl DiskStats {
|
||||
#[allow(clippy::unnecessary_cast)] // c_ulong is u32 on ARM, u64 on macOS
|
||||
pub fn new(qmdl_path: &str) -> Result<Self, String> {
|
||||
let c_path =
|
||||
CString::new(qmdl_path).map_err(|e| format!("invalid path {qmdl_path}: {e}"))?;
|
||||
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
|
||||
if unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) } != 0 {
|
||||
return Err(format!(
|
||||
"statvfs({qmdl_path}) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
|
||||
let block_size = stat.f_frsize as u64;
|
||||
let total_kb = (stat.f_blocks as u64 * block_size / 1024) as usize;
|
||||
let free_kb = (stat.f_bfree as u64 * block_size / 1024) as usize;
|
||||
let available_kb = (stat.f_bavail as u64 * block_size / 1024) as usize;
|
||||
let used_kb = total_kb.saturating_sub(free_kb);
|
||||
let used_percent = if stat.f_blocks > 0 {
|
||||
format!("{}%", (stat.f_blocks - stat.f_bfree) * 100 / stat.f_blocks)
|
||||
// runs "df -h <qmdl_path>" to get storage statistics for the partition containing
|
||||
// the QMDL file.
|
||||
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
|
||||
// Uz801 needs to be told to use the busybox df specifically
|
||||
let mut df_cmd: Command;
|
||||
if matches!(device, Device::Uz801) {
|
||||
df_cmd = Command::new("busybox");
|
||||
df_cmd.arg("df");
|
||||
} else {
|
||||
"0%".to_string()
|
||||
};
|
||||
df_cmd = Command::new("df");
|
||||
}
|
||||
df_cmd.arg("-h");
|
||||
df_cmd.arg(qmdl_path);
|
||||
let stdout = get_cmd_output(df_cmd).await?;
|
||||
|
||||
// Handle standard df -h format
|
||||
let mut parts = stdout.split_whitespace().skip(7);
|
||||
Ok(Self {
|
||||
partition: qmdl_path.to_string(),
|
||||
total_size: humanize_kb(total_kb),
|
||||
used_size: humanize_kb(used_kb),
|
||||
available_size: humanize_kb(available_kb),
|
||||
used_percent,
|
||||
mounted_on: qmdl_path.to_string(),
|
||||
available_bytes: Some(stat.f_bavail as u64 * block_size),
|
||||
partition: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
total_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
available_size: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
used_percent: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
mounted_on: parts.next().ok_or("error parsing df output")?.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,26 +22,10 @@
|
||||
<p>Error getting analysis report: {entry.analysis_report}</p>
|
||||
{:else}
|
||||
{@const metadata: ReportMetadata = entry.analysis_report.metadata}
|
||||
{@const numWarnings: number = entry.get_num_warnings() || 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if !!numWarnings || !current}
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
{#if !!numWarnings}
|
||||
<div
|
||||
class="text-red-700 border-red-500 border rounded-lg text-blue-600 px-2 py-1 mr-12"
|
||||
>
|
||||
Your Rayhunter device raised {`${numWarnings}`} warning{`${
|
||||
numWarnings > 1 ? 's' : ''
|
||||
}`}!
|
||||
<a
|
||||
href="https://efforg.github.io/rayhunter/faq.html#red"
|
||||
class="text-blue-600 underline">Read the FAQ</a
|
||||
> to learn what you can do about it
|
||||
</div>
|
||||
{/if}
|
||||
{#if !current}
|
||||
<ReAnalyzeButton {entry} {manager} />
|
||||
{/if}
|
||||
{#if !current}
|
||||
<div class="flex flex-row justify-end items-center">
|
||||
<ReAnalyzeButton {entry} {manager} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if entry.analysis_report.rows.length > 0}
|
||||
|
||||
@@ -241,48 +241,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-6 space-y-3">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Storage Management</h3>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="min_space_to_start_recording_mb"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Minimum Space to Start Recording (MB)
|
||||
</label>
|
||||
<input
|
||||
id="min_space_to_start_recording_mb"
|
||||
type="number"
|
||||
min="1"
|
||||
bind:value={config.min_space_to_start_recording_mb}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Recording will not start if less than this amount of disk space is free
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="min_space_to_continue_recording_mb"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Minimum Space to Continue Recording (MB)
|
||||
</label>
|
||||
<input
|
||||
id="min_space_to_continue_recording_mb"
|
||||
type="number"
|
||||
min="1"
|
||||
bind:value={config.min_space_to_continue_recording_mb}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-rayhunter-blue"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Recording will stop automatically if disk space drops below this level
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4 mt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">
|
||||
Analyzer Heuristic Settings
|
||||
|
||||
@@ -81,11 +81,6 @@
|
||||
'N/A'}</span
|
||||
>
|
||||
</div>
|
||||
{#if entry.stop_reason}
|
||||
<div class="bg-yellow-50 border border-yellow-300 rounded p-2 text-yellow-800 text-sm">
|
||||
{entry.stop_reason}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row justify-between lg:justify-end gap-1 mt-2 overflow-x-auto">
|
||||
<DownloadLink url={entry.get_pcap_url()} text="pcap" full_button />
|
||||
<DownloadLink url={entry.get_qmdl_url()} text="qmdl" full_button />
|
||||
|
||||
@@ -33,6 +33,23 @@
|
||||
}
|
||||
return text;
|
||||
});
|
||||
|
||||
// Cell signal quality assessment based on RSRP
|
||||
// Excellent: >= -80 dBm, Good: -80 to -90, Fair: -90 to -100, Poor: < -100
|
||||
let signal_quality = $derived.by(() => {
|
||||
const rsrp = stats.cell_info?.rsrp_dbm;
|
||||
if (rsrp === undefined) return { label: 'Unknown', color: 'text-gray-500' };
|
||||
if (rsrp >= -80) return { label: 'Excellent', color: 'text-green-600' };
|
||||
if (rsrp >= -90) return { label: 'Good', color: 'text-green-600' };
|
||||
if (rsrp >= -100) return { label: 'Fair', color: 'text-yellow-600' };
|
||||
return { label: 'Poor', color: 'text-red-600' };
|
||||
});
|
||||
|
||||
// Format signal value with unit
|
||||
function format_signal(value: number | undefined, unit: string): string {
|
||||
if (value === undefined) return '—';
|
||||
return `${value.toFixed(1)} ${unit}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -116,6 +133,46 @@
|
||||
</svg>
|
||||
</td>
|
||||
</tr>
|
||||
{#if stats.cell_info}
|
||||
<tr class="border-b">
|
||||
<th class={table_cell_classes}> Cell Signal </th>
|
||||
<td class={table_cell_classes}>
|
||||
<div class="flex flex-col gap-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium {signal_quality.color}">
|
||||
{signal_quality.label}
|
||||
</span>
|
||||
{#if stats.cell_info.rsrp_dbm !== undefined}
|
||||
<span class="text-gray-600">
|
||||
(RSRP: {format_signal(stats.cell_info.rsrp_dbm, 'dBm')})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-gray-600 text-xs">
|
||||
{#if stats.cell_info.pci !== undefined}
|
||||
<span>PCI: {stats.cell_info.pci}</span>
|
||||
{/if}
|
||||
{#if stats.cell_info.earfcn !== undefined}
|
||||
<span class="ml-2">EARFCN: {stats.cell_info.earfcn}</span>
|
||||
{/if}
|
||||
{#if stats.cell_info.rsrq_db !== undefined}
|
||||
<span class="ml-2"
|
||||
>RSRQ: {format_signal(stats.cell_info.rsrq_db, 'dB')}</span
|
||||
>
|
||||
{/if}
|
||||
{#if stats.cell_info.rssi_dbm !== undefined}
|
||||
<span class="ml-2"
|
||||
>RSSI: {format_signal(
|
||||
stats.cell_info.rssi_dbm,
|
||||
'dBm'
|
||||
)}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ interface JsonManifestEntry {
|
||||
start_time: string;
|
||||
last_message_time: string;
|
||||
qmdl_size_bytes: number;
|
||||
stop_reason: string | null;
|
||||
}
|
||||
|
||||
export class Manifest {
|
||||
@@ -58,7 +57,6 @@ export class ManifestEntry {
|
||||
public analysis_size_bytes = $state(0);
|
||||
public analysis_status: AnalysisStatus | undefined = $state(undefined);
|
||||
public analysis_report: AnalysisReport | string | undefined = $state(undefined);
|
||||
public stop_reason: string | undefined = $state(undefined);
|
||||
|
||||
constructor(json: JsonManifestEntry) {
|
||||
this.name = json.name;
|
||||
@@ -67,9 +65,6 @@ export class ManifestEntry {
|
||||
if (json.last_message_time) {
|
||||
this.last_message_time = new Date(json.last_message_time);
|
||||
}
|
||||
if (json.stop_reason) {
|
||||
this.stop_reason = json.stop_reason;
|
||||
}
|
||||
}
|
||||
|
||||
get_readable_qmdl_size(): string {
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface SystemStats {
|
||||
memory_stats: MemoryStats;
|
||||
runtime_metadata: RuntimeMetadata;
|
||||
battery_status?: BatteryStatus;
|
||||
cell_info?: CellSignalInfo;
|
||||
}
|
||||
|
||||
export interface RuntimeMetadata {
|
||||
@@ -18,7 +19,6 @@ export interface DiskStats {
|
||||
available_size: string;
|
||||
used_percent: string;
|
||||
mounted_on: string;
|
||||
available_bytes?: number;
|
||||
}
|
||||
|
||||
export interface MemoryStats {
|
||||
@@ -31,3 +31,11 @@ export interface BatteryStatus {
|
||||
level: number;
|
||||
is_plugged_in: boolean;
|
||||
}
|
||||
|
||||
export interface CellSignalInfo {
|
||||
rsrp_dbm?: number;
|
||||
rsrq_db?: number;
|
||||
rssi_dbm?: number;
|
||||
pci?: number;
|
||||
earfcn?: number;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ export interface Config {
|
||||
ntfy_url: string;
|
||||
enabled_notifications: enabled_notifications[];
|
||||
analyzers: AnalyzerConfig;
|
||||
min_space_to_start_recording_mb: number;
|
||||
min_space_to_continue_recording_mb: number;
|
||||
}
|
||||
|
||||
export async function req(method: string, url: string, json_body?: unknown): Promise<string> {
|
||||
|
||||
@@ -5,7 +5,7 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.API_TARGET || 'http://localhost:8080',
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
configure: (proxy, _options) => {
|
||||
|
||||
Vendored
-6
@@ -28,12 +28,6 @@ ntfy_url = ""
|
||||
# What notification types to enable. Does nothing if the above ntfy_url is not set.
|
||||
enabled_notifications = ["Warning", "LowBattery"]
|
||||
|
||||
# Disk Space Management
|
||||
# Minimum free space (MB) required to start recording
|
||||
min_space_to_start_recording_mb = 1
|
||||
# Minimum free space (MB) to continue recording (stops if below this)
|
||||
min_space_to_continue_recording_mb = 1
|
||||
|
||||
# Analyzer Configuration
|
||||
# Enable/disable specific IMSI catcher detection heuristics
|
||||
# See https://github.com/EFForg/rayhunter/blob/main/doc/heuristics.md for details
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.warning-box {
|
||||
padding: 0.75em 1em;
|
||||
border-left: 4px solid #e33;
|
||||
border-radius: 4px;
|
||||
background-color: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
@@ -1,78 +1,63 @@
|
||||
# Installing from source
|
||||
|
||||
Building Rayhunter from source, either for development or otherwise, involves a
|
||||
number of external dependencies. Unless you need to do this, we recommend you
|
||||
use our [compiled builds](https://github.com/EFForg/rayhunter/releases).
|
||||
Building Rayhunter from source, either for development or because the install script doesn't work on your system, involves a number of external dependencies. Unless you need to do this, we recommend you use our [compiled builds](https://github.com/EFForg/rayhunter/releases).
|
||||
|
||||
At a high level, we have:
|
||||
* Install [nodejs/npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), which is required to build Rayhunter's web UI
|
||||
* Make sure to build the site with `pushd daemon/web && npm install && npm run build && popd` before building Rayhunter. If you're working directly on the frontend, `npm run dev` will allow you to test a local frontend with hot-reloading (use `http://localhost:5173` instead of `http://localhost:8080`).
|
||||
* Install ADB on your computer using the instructions above, and make sure it's in your terminal's PATH
|
||||
* You can verify if ADB is in your PATH by running `which adb` in a terminal. If it prints the filepath to where ADB is installed, you're set! Otherwise, try following one of these guides:
|
||||
* [linux](https://askubuntu.com/questions/652936/adding-android-sdk-platform-tools-to-path-downloaded-from-umake)
|
||||
* [macOS](https://www.repeato.app/setting-up-adb-on-macos-a-step-by-step-guide/)
|
||||
* [Windows](https://medium.com/@yadav-ajay/a-step-by-step-guide-to-setting-up-adb-path-on-windows-0b833faebf18)
|
||||
* Install `curl` on your computer to run the install scripts. It is not needed to build binaries.
|
||||
|
||||
* A JS frontend written in SvelteKit (`./daemon/web/`)
|
||||
* A Rust binary `rayhunter-daemon` (`./daemon/`) that runs on the device, and bundles the frontend.
|
||||
* A Rust binary `installer` (`./installer`) that runs on the computer and bundles `rayhunter-daemon`.
|
||||
### Install Rust targets
|
||||
|
||||
It's recommended to work either on Mac/Linux, or WSL on Windows.
|
||||
|
||||
## Building frontend and backend
|
||||
|
||||
First, install dependencies:
|
||||
|
||||
- [Rust](https://www.rust-lang.org/tools/install)
|
||||
- [Node.js/npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
||||
- C compiler tools (`apt install build-essential` on Linux, `xcode-select --install` on Mac)
|
||||
|
||||
Then you can build everything with:
|
||||
[Install Rust the usual way](https://www.rust-lang.org/tools/install). Then,
|
||||
|
||||
- install the cross-compilation target for the device Rayhunter will run on:
|
||||
```sh
|
||||
./scripts/build-dev.sh
|
||||
./scripts/install-dev.sh orbic # replace 'orbic' with your device type
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
```
|
||||
|
||||
## Hot-reloading the frontend
|
||||
|
||||
If you are working on the frontend, you normally have to repeat all of the above steps everytime to see a change.
|
||||
|
||||
You can instead run the frontend separately on your PC while the Rust parts
|
||||
continue running on your target device:
|
||||
|
||||
- install the statically compiled target for your host machine to build the binary installer `serial`.
|
||||
```sh
|
||||
cd daemon/web
|
||||
|
||||
# Assumes rayhunter-daemon is listening on localhost:8080
|
||||
npm run dev
|
||||
|
||||
# Use a custom target IP:port where the backend runs
|
||||
API_TARGET=http://192.168.1.1:8080 npm run dev
|
||||
# check which toolchain you have installed by default with
|
||||
rustup show
|
||||
# now install the correct variant for your host platform, one of:
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
rustup target add armv7-unknown-linux-musleabi
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add x86_64-pc-windows-gnu
|
||||
```
|
||||
|
||||
The UI will listen on `localhost:5173` and instantly show any frontend changes
|
||||
you make. Backend changes require building everything from the top (daemon and installer).
|
||||
|
||||
## Installer utils, getting a shell
|
||||
|
||||
Check `./scripts/install-dev.sh util --help`
|
||||
for useful utilities for transferring files, opening shells. The exact tools
|
||||
available wildly depend on the device you're working on, and they are
|
||||
usually documented the relevant device's page under [Supported
|
||||
Devices](./supported-devices.md).
|
||||
|
||||
A lot of devices run a trimmed down version of Android and have ADB (Android
|
||||
Debug Bridge) support. The USB-based installers (`orbic-usb`, `pinephone`,
|
||||
`uz801`) use ADB to perform the installation.
|
||||
|
||||
You might want to install and use actual ADB to connect to the device, push
|
||||
files and generally poke around. The installer contains some tools to enable ADB:
|
||||
Now you can root your device and install Rayhunter by running:
|
||||
|
||||
```sh
|
||||
adb kill-server
|
||||
# Build the daemon binary for local development (rustcrypto TLS backend, fast compilation)
|
||||
# WARNING: The rustcrypto library, though not known to be insecure, is less well
|
||||
# tested than its counterpart and could potentially have severe issues in
|
||||
# its cryptographic implementation. We therefore recommend using ring-tls in
|
||||
# production builds (see below)
|
||||
cargo build-daemon-firmware-devel
|
||||
|
||||
# Enables ADB on either of these devices
|
||||
./scripts/install-dev.sh util tmobile-start-adb
|
||||
./scripts/install-dev.sh orbic-usb
|
||||
# To build it exactly like in CI (more mature ring TLS backend, slower compilation)
|
||||
# CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc cargo build-daemon-firmware
|
||||
|
||||
adb shell
|
||||
# Build rootshell
|
||||
cargo build-rootshell-firmware-devel
|
||||
|
||||
# Replace 'orbic' with your device type if different.
|
||||
# A list of possible values can be found with 'cargo run --bin installer help'.
|
||||
FIRMWARE_PROFILE=firmware-devel cargo run -p installer --bin installer orbic
|
||||
```
|
||||
|
||||
Note though that we can't assist with any issues setting ADB up, _especially
|
||||
not_ on Windows. There have been too many driver issues to make this the
|
||||
"golden path" for most users or contributors. There have been instances where
|
||||
people managed to brick their orbic devices using ADB on Windows.
|
||||
### If you're on Windows or can't run the install scripts
|
||||
|
||||
* Root your device on Windows using the instructions here: <https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash-kajeet.4334899/#post-87855183>
|
||||
* Build the web UI using `cd daemon/web && npm install && npm run build`
|
||||
* Push the scripts in `scripts/` to `/etc/init.d` on device and make a directory called `/data/rayhunter` using `adb shell` (and sshell for your root shell if you followed the steps above)
|
||||
* You also need to copy `config.toml.in` to `/data/rayhunter/config.toml`. Uncomment the `device` line and set the value to your device type if necessary.
|
||||
* Then run `./make.sh`, which will build the binary, push it over adb, and restart the device. Once it's restarted, Rayhunter should be running!
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# LTE ML1 Serving Cell Measurement (0xB193)
|
||||
|
||||
This document describes the Qualcomm DIAG log code 0xB193 (LTE ML1 Serving Cell Measurement Response), which provides detailed LTE signal strength measurements including RSRP, RSRQ, and RSSI.
|
||||
|
||||
## Overview
|
||||
|
||||
Log code 0xB193 (`LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE`) is emitted by the Qualcomm modem's Layer 1 (ML1) component and contains periodic measurements of the serving cell's signal characteristics. Rayhunter captures these measurements and includes the RSRP value in GSMTAP headers for PCAP output.
|
||||
|
||||
## Packet Structure
|
||||
|
||||
The 0xB193 log uses a subpacket architecture common to many Qualcomm DIAG logs:
|
||||
|
||||
```
|
||||
+------------------+
|
||||
| Main Header | 4 bytes
|
||||
+------------------+
|
||||
| Subpacket Header | 4 bytes
|
||||
+------------------+
|
||||
| Subpacket Data | Variable (version-dependent)
|
||||
+------------------+
|
||||
```
|
||||
|
||||
### Main Header (4 bytes)
|
||||
|
||||
| Offset | Size | Field | Description |
|
||||
|--------|------|-----------------|---------------------------------------|
|
||||
| 0 | 1 | main_version | Main packet version (observed: 1) |
|
||||
| 1 | 1 | num_subpackets | Number of subpackets (typically 1) |
|
||||
| 2 | 2 | reserved | Reserved/padding |
|
||||
|
||||
### Subpacket Header (4 bytes)
|
||||
|
||||
| Offset | Size | Field | Description |
|
||||
|--------|------|-------------------|-------------------------------------|
|
||||
| 0 | 1 | subpacket_id | Subpacket identifier |
|
||||
| 1 | 1 | subpacket_version | Subpacket version (see below) |
|
||||
| 2 | 2 | subpacket_size | Size of subpacket including header |
|
||||
|
||||
### Known Subpacket Versions
|
||||
|
||||
Different modem firmware versions emit different subpacket versions. The field offsets within the subpacket data vary by version:
|
||||
|
||||
| Version | PCI Offset | EARFCN Offset | RSRP Offset | Notes |
|
||||
|---------|------------|---------------|-------------|--------------------------|
|
||||
| 4 | 0 | 2 | 12 | Early format (SCAT) |
|
||||
| 7 | 0 | 4 | 14 | Intermediate format |
|
||||
| 18-24 | 0 | 4 | 24 | Common on Orbic RC400L |
|
||||
| 35-40 | 0 | 4 | 28 | Newer modems |
|
||||
|
||||
The Orbic RC400L device used for development emits **subpacket version 18**.
|
||||
|
||||
## Signal Measurement Fields
|
||||
|
||||
### RSRP (Reference Signal Received Power)
|
||||
|
||||
RSRP is the primary signal strength indicator for LTE. The raw 12-bit value is converted to dBm:
|
||||
|
||||
```
|
||||
RSRP (dBm) = -180.0 + (raw_value & 0xFFF) * 0.0625
|
||||
```
|
||||
|
||||
Typical range: -140 dBm (very weak) to -44 dBm (very strong)
|
||||
|
||||
### PCI (Physical Cell ID)
|
||||
|
||||
The Physical Cell ID identifies the serving cell. Stored as a 16-bit little-endian value at the PCI offset.
|
||||
|
||||
Range: 0-503
|
||||
|
||||
### EARFCN (E-UTRA Absolute Radio Frequency Channel Number)
|
||||
|
||||
The EARFCN identifies the carrier frequency. Stored as a 32-bit little-endian value at the EARFCN offset.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. **Caching Strategy**: Since 0xB193 messages arrive independently from RRC OTA messages, rayhunter caches the most recent RSRP value and applies it to subsequent GSMTAP headers.
|
||||
|
||||
2. **Signal Conversion**: The `signal_dbm` field in GSMTAP headers is an `i8`, so the RSRP value is clamped to the range -128 to 0 dBm.
|
||||
|
||||
3. **Version Detection**: The subpacket version determines field offsets. Unknown versions fall back to the v7 layout.
|
||||
|
||||
## References
|
||||
|
||||
### SCAT (Signaling Collection and Analysis Tool)
|
||||
|
||||
The [SCAT project](https://github.com/fgsect/scat) by the Firmware Security (fgsect) research group at TU Berlin provides Qualcomm DIAG log parsers.
|
||||
|
||||
Relevant file: `parsers/qualcomm/diagltelogparser.py`
|
||||
|
||||
```python
|
||||
# SCAT v4/v5 parser structure (simplified)
|
||||
# pci = struct.unpack('<H', payload[0:2])
|
||||
# earfcn = struct.unpack('<H', payload[2:4]) # or <L for 32-bit
|
||||
# rsrp_raw = struct.unpack('<L', payload[offset:offset+4])
|
||||
```
|
||||
|
||||
### Mobile Insight
|
||||
|
||||
The [Mobile Insight project](https://github.com/mobile-insight/mobileinsight-core) from UCLA WiNG Lab provides comprehensive Qualcomm DIAG parsing with extensive version support.
|
||||
|
||||
Relevant file: `mobile_insight/analyzer/msg_logger.py` and related LTE analyzers
|
||||
|
||||
Mobile Insight documents subpacket versions 4, 7, 18, 19, 22, 24, 35, 36, and 40, with version-specific field layouts.
|
||||
|
||||
### QCSuper
|
||||
|
||||
The [QCSuper project](https://github.com/P1sec/QCSuper) by P1 Security provides another implementation of Qualcomm DIAG protocol handling.
|
||||
|
||||
### 3GPP Specifications
|
||||
|
||||
- **3GPP TS 36.214**: Physical layer measurements (defines RSRP, RSRQ, RSSI)
|
||||
- **3GPP TS 36.133**: Requirements for support of radio resource management
|
||||
|
||||
## Example Output
|
||||
|
||||
When rayhunter captures a 0xB193 log, debug output shows:
|
||||
|
||||
```
|
||||
ML1 0xB193 v18: RSRP=-94.8dBm, PCI=446, EARFCN=975
|
||||
```
|
||||
|
||||
The corresponding GSMTAP packets in Wireshark will display `Signal dBm: -95` (rounded to i8).
|
||||
@@ -5,16 +5,6 @@ Supported in Rayhunter since version 0.6.0.
|
||||
The Moxee Hotspot is a device very similar to the Orbic RC400L. It seems to be
|
||||
primarily for the US market.
|
||||
|
||||
<div class="warning-box">
|
||||
|
||||
**WARNING: These devices are known to become completely bricked by installing Rayhunter.**
|
||||
|
||||
Do not buy this device nor try to install _nor upgrade_ Rayhunter on it.
|
||||
|
||||
We're still trying to figure out what's wrong in [this discussion](https://github.com/EFForg/rayhunter/issues/865).
|
||||
|
||||
</div>
|
||||
|
||||
- [KonnectONE product page](https://www.konnectone.com/specs-hotspot)
|
||||
- [Moxee product page](https://www.moxee.com/hotspot)
|
||||
|
||||
|
||||
@@ -21,5 +21,4 @@ tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1.0.100"
|
||||
shlex = "1"
|
||||
installer = { path = "../../installer" }
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use anyhow::Context;
|
||||
use tauri::Emitter;
|
||||
|
||||
async fn run_installer(app_handle: tauri::AppHandle, args: String) -> anyhow::Result<()> {
|
||||
let args_vec = shlex::split(&args).context("Failed to parse arguments: unclosed quote")?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
installer::run_with_callback(
|
||||
args_vec.iter().map(|s| s.as_str()),
|
||||
// TODO: we should split using something similar to shlex in python
|
||||
args.split_whitespace(),
|
||||
Some(Box::new(move |output| {
|
||||
app_handle
|
||||
.emit("installer-output", output)
|
||||
|
||||
@@ -81,9 +81,6 @@
|
||||
<input
|
||||
class="mr-1 px-5 py-2 rounded-lg shadow-md"
|
||||
placeholder="Enter CLI installer args..."
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
bind:value={installerArgs}
|
||||
/>
|
||||
<button
|
||||
|
||||
+1
-8
@@ -4,14 +4,7 @@ use std::process::exit;
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-env-changed=NO_FIRMWARE_BIN");
|
||||
println!("cargo::rerun-if-env-changed=FIRMWARE_PROFILE");
|
||||
let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| {
|
||||
// Default to firmware-devel for debug builds, firmware for release builds
|
||||
if std::env::var("PROFILE").as_deref() == Ok("release") {
|
||||
"firmware".to_string()
|
||||
} else {
|
||||
"firmware-devel".to_string()
|
||||
}
|
||||
});
|
||||
let profile = std::env::var("FIRMWARE_PROFILE").unwrap_or_else(|_| "firmware".to_string());
|
||||
let include_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../target/armv7-unknown-linux-musleabihf")
|
||||
.join(&profile);
|
||||
|
||||
@@ -97,10 +97,7 @@ async fn login_and_exploit(admin_ip: &str, username: &str, password: &str) -> Re
|
||||
.context("Failed to parse login response")?;
|
||||
|
||||
if login_result.retcode != 0 {
|
||||
match login_result.retcode {
|
||||
201 => bail!("Login failed: incorrect password"),
|
||||
code => bail!("Login failed with retcode: {}", code),
|
||||
}
|
||||
bail!("Login failed with retcode: {}", login_result.retcode);
|
||||
}
|
||||
|
||||
// Step 4: Exploit using authenticated session
|
||||
|
||||
+915
@@ -0,0 +1,915 @@
|
||||
//! Diag protocol serialization/deserialization
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use crc::{Algorithm, Crc};
|
||||
use deku::prelude::*;
|
||||
|
||||
use crate::hdlc::{self, hdlc_decapsulate};
|
||||
use log::warn;
|
||||
use thiserror::Error;
|
||||
|
||||
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
||||
pub const MESSAGE_ESCAPE_CHAR: u8 = 0x7d;
|
||||
|
||||
pub const ESCAPED_MESSAGE_TERMINATOR: u8 = 0x5e;
|
||||
pub const ESCAPED_MESSAGE_ESCAPE_CHAR: u8 = 0x5d;
|
||||
|
||||
#[derive(Debug, Clone, DekuWrite)]
|
||||
pub struct RequestContainer {
|
||||
pub data_type: DataType,
|
||||
#[deku(skip)]
|
||||
pub use_mdm: bool,
|
||||
#[deku(skip, cond = "!*use_mdm")]
|
||||
pub mdm_field: i32,
|
||||
pub hdlc_encapsulated_request: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||
#[deku(id_type = "u32")]
|
||||
pub enum Request {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(LogConfigRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||
#[deku(id_type = "u32", endian = "little")]
|
||||
pub enum LogConfigRequest {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges,
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask {
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
log_mask: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u32", endian = "little")]
|
||||
pub enum DataType {
|
||||
#[deku(id = "32")]
|
||||
UserSpace,
|
||||
#[deku(id_pat = "_")]
|
||||
Other(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Error)]
|
||||
pub enum DiagParsingError {
|
||||
#[error("Failed to parse Message: {0}, data: {1:?}")]
|
||||
MessageParsingError(deku::DekuError, Vec<u8>),
|
||||
#[error("HDLC decapsulation of message failed: {0}, data: {1:?}")]
|
||||
HdlcDecapsulationError(hdlc::HdlcError, Vec<u8>),
|
||||
}
|
||||
|
||||
// this is sorta based on the params qcsuper uses, plus what seems to be used in
|
||||
// https://github.com/fgsect/scat/blob/f1538b397721df3ab8ba12acd26716abcf21f78b/util.py#L47
|
||||
pub const CRC_CCITT_ALG: Algorithm<u16> = Algorithm {
|
||||
poly: 0x1021,
|
||||
init: 0xffff,
|
||||
refin: true,
|
||||
refout: true,
|
||||
width: 16,
|
||||
xorout: 0xffff,
|
||||
check: 0x2189,
|
||||
residue: 0x0000,
|
||||
};
|
||||
|
||||
pub const CRC_CCITT: Crc<u16> = Crc::<u16>::new(&CRC_CCITT_ALG);
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct MessagesContainer {
|
||||
pub data_type: DataType,
|
||||
pub num_messages: u32,
|
||||
#[deku(count = "num_messages")]
|
||||
pub messages: Vec<HdlcEncapsulatedMessage>,
|
||||
}
|
||||
|
||||
impl MessagesContainer {
|
||||
pub fn into_messages(self) -> Vec<Result<Message, DiagParsingError>> {
|
||||
let mut result = Vec::new();
|
||||
for msg in self.messages {
|
||||
for sub_msg in msg.data.split_inclusive(|&b| b == MESSAGE_TERMINATOR) {
|
||||
match hdlc_decapsulate(sub_msg, &CRC_CCITT) {
|
||||
Ok(data) => match Message::from_bytes((&data, 0)) {
|
||||
Ok(((leftover_bytes, _), res)) => {
|
||||
if !leftover_bytes.is_empty() {
|
||||
warn!(
|
||||
"warning: {} leftover bytes when parsing Message",
|
||||
leftover_bytes.len()
|
||||
);
|
||||
}
|
||||
result.push(Ok(res));
|
||||
}
|
||||
Err(e) => result.push(Err(DiagParsingError::MessageParsingError(e, data))),
|
||||
},
|
||||
Err(err) => result.push(Err(DiagParsingError::HdlcDecapsulationError(
|
||||
err,
|
||||
sub_msg.to_vec(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct HdlcEncapsulatedMessage {
|
||||
pub len: u32,
|
||||
#[deku(count = "len")]
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u8")]
|
||||
pub enum Message {
|
||||
#[deku(id = "16")]
|
||||
Log {
|
||||
pending_msgs: u8,
|
||||
outer_length: u16,
|
||||
inner_length: u16,
|
||||
log_type: u16,
|
||||
timestamp: Timestamp,
|
||||
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(timestamp)))
|
||||
#[deku(ctx = "*log_type, inner_length.saturating_sub(12)")]
|
||||
body: LogBody,
|
||||
},
|
||||
|
||||
// kinda unpleasant deku hackery here. deku expects an enum's variant to be
|
||||
// right before its data, but in this case, a status value comes between the
|
||||
// variants and the data. so we need to use deku's context (ctx) feature to
|
||||
// pass those opcodes down to their respective parsers.
|
||||
#[deku(id_pat = "_")]
|
||||
Response {
|
||||
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
|
||||
opcode2: u8,
|
||||
opcode3: u8,
|
||||
opcode4: u8,
|
||||
subopcode: u32,
|
||||
status: u32,
|
||||
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
|
||||
payload: ResponsePayload,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "log_type: u16, hdr_len: u16", id = "log_type")]
|
||||
pub enum LogBody {
|
||||
#[deku(id = "0x412f")]
|
||||
WcdmaSignallingMessage {
|
||||
channel_type: u8,
|
||||
radio_bearer: u8,
|
||||
length: u16,
|
||||
#[deku(count = "length")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x512f")]
|
||||
GsmRrSignallingMessage {
|
||||
channel_type: u8,
|
||||
message_type: u8,
|
||||
length: u8,
|
||||
#[deku(count = "length")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x5226")]
|
||||
GprsMacSignallingMessage {
|
||||
channel_type: u8,
|
||||
message_type: u8,
|
||||
length: u8,
|
||||
#[deku(count = "length")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0xb0c0")]
|
||||
LteRrcOtaMessage {
|
||||
ext_header_version: u8,
|
||||
#[deku(ctx = "*ext_header_version")]
|
||||
packet: LteRrcOtaPacket,
|
||||
},
|
||||
// the four NAS command opcodes refer to:
|
||||
// * 0xb0e2: plain ESM NAS message (incoming)
|
||||
// * 0xb0e3: plain ESM NAS message (outgoing)
|
||||
// * 0xb0ec: plain EMM NAS message (incoming)
|
||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
||||
Nas4GMessage {
|
||||
#[deku(skip, default = "log_type")]
|
||||
log_type: u16,
|
||||
#[deku(ctx = "*log_type")]
|
||||
direction: Nas4GMessageDirection,
|
||||
ext_header_version: u8,
|
||||
rrc_rel: u8,
|
||||
rrc_version_minor: u8,
|
||||
rrc_version_major: u8,
|
||||
// message length = hdr_len - (sizeof(ext_header_version) + sizeof(rrc_rel) + sizeof(rrc_version_minor) + sizeof(rrc_version_major))
|
||||
#[deku(count = "hdr_len.saturating_sub(4)")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x11eb")]
|
||||
IpTraffic {
|
||||
// is this right?? based on https://github.com/P1sec/QCSuper/blob/81dbaeee15ec7747e899daa8e3495e27cdcc1264/src/modules/pcap_dump.py#L378
|
||||
#[deku(count = "hdr_len.saturating_sub(8)")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x713a")]
|
||||
UmtsNasOtaMessage {
|
||||
is_uplink: u8,
|
||||
length: u32,
|
||||
#[deku(count = "length")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0xb821")]
|
||||
NrRrcOtaMessage {
|
||||
#[deku(count = "hdr_len")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
/// LTE ML1 Serving Cell Measurement Response (0xB193)
|
||||
/// Contains RSRP, RSRQ, and RSSI measurements for the serving cell.
|
||||
/// This is used to populate signal strength in GSMTAP headers.
|
||||
#[deku(id = "0xb193")]
|
||||
LteMl1ServingCellMeas { meas: LteMl1ServingCellMeasData },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "log_type: u16", id = "log_type")]
|
||||
pub enum Nas4GMessageDirection {
|
||||
// * 0xb0e2: plain ESM NAS message (incoming)
|
||||
// * 0xb0e3: plain ESM NAS message (outgoing)
|
||||
// * 0xb0ec: plain EMM NAS message (incoming)
|
||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||
#[deku(id_pat = "0xb0e2 | 0xb0ec")]
|
||||
Downlink,
|
||||
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
|
||||
Uplink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
|
||||
pub enum LteRrcOtaPacket {
|
||||
#[deku(id_pat = "0..=4")]
|
||||
V0 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u16,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "5..=7")]
|
||||
V5 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u16,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "8..=24")]
|
||||
V8 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u32,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "25..")]
|
||||
V25 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
nr_rrc_rel_maj: u8,
|
||||
nr_rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u32,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LteRrcOtaPacket {
|
||||
fn get_sfn_subfn(&self) -> u16 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V5 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V8 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V25 { sfn_subfn, .. } => *sfn_subfn,
|
||||
}
|
||||
}
|
||||
pub fn get_sfn(&self) -> u32 {
|
||||
self.get_sfn_subfn() as u32 >> 4
|
||||
}
|
||||
|
||||
pub fn get_subfn(&self) -> u8 {
|
||||
(self.get_sfn_subfn() & 0xf) as u8
|
||||
}
|
||||
|
||||
pub fn get_pdu_num(&self) -> u8 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V5 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V8 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V25 { pdu_num, .. } => *pdu_num,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { earfcn, .. } => *earfcn as u32,
|
||||
LteRrcOtaPacket::V5 { earfcn, .. } => *earfcn as u32,
|
||||
LteRrcOtaPacket::V8 { earfcn, .. } => *earfcn,
|
||||
LteRrcOtaPacket::V25 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_payload(self) -> Vec<u8> {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V5 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V8 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V25 { packet, .. } => packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// LTE ML1 Serving Cell Measurement (0xB193) packet structure.
|
||||
/// Uses subpacket architecture per Mobile Insight / Qualcomm DIAG format.
|
||||
///
|
||||
/// Packet layout:
|
||||
/// - Main Header: version (1) + num_subpackets (1) + reserved (2) = 4 bytes
|
||||
/// - SubPacket Header: id (1) + version (1) + size (2) = 4 bytes
|
||||
/// - SubPacket Data: varies by subpacket version (v4, v7, v18, v19, v22, v24, v35, v36, v40)
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(endian = "little")]
|
||||
pub struct LteMl1ServingCellMeasData {
|
||||
pub main_version: u8,
|
||||
pub num_subpackets: u8,
|
||||
pub reserved: u16,
|
||||
// SubPacket header
|
||||
pub subpacket_id: u8,
|
||||
pub subpacket_version: u8,
|
||||
pub subpacket_size: u16,
|
||||
// SubPacket data - we read enough to get RSRP/RSRQ/RSSI
|
||||
// The actual layout depends on subpacket_version, but EARFCN and PCI are always first
|
||||
#[deku(count = "subpacket_size.saturating_sub(4).min(128)")]
|
||||
pub subpacket_data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl LteMl1ServingCellMeasData {
|
||||
/// Helper to read a u16 from subpacket data at given offset
|
||||
fn read_u16(&self, offset: usize) -> Option<u16> {
|
||||
if offset + 2 <= self.subpacket_data.len() {
|
||||
Some(u16::from_le_bytes([
|
||||
self.subpacket_data[offset],
|
||||
self.subpacket_data[offset + 1],
|
||||
]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to read a u32 from subpacket data at given offset
|
||||
fn read_u32(&self, offset: usize) -> Option<u32> {
|
||||
if offset + 4 <= self.subpacket_data.len() {
|
||||
Some(u32::from_le_bytes([
|
||||
self.subpacket_data[offset],
|
||||
self.subpacket_data[offset + 1],
|
||||
self.subpacket_data[offset + 2],
|
||||
self.subpacket_data[offset + 3],
|
||||
]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the RSRP field offset based on subpacket version
|
||||
/// Returns (earfcn_offset, earfcn_size, rsrp_offset)
|
||||
fn get_offsets(&self) -> (usize, usize, usize) {
|
||||
match self.subpacket_version {
|
||||
// v4: EARFCN(2) + PCI(2) + SFN(2) + skip(6) = offset 12 for RSRP
|
||||
4 => (0, 2, 12),
|
||||
// v7: EARFCN(4) + PCI(2) + SFN(2) + skip(6) = offset 14 for RSRP
|
||||
7 => (0, 4, 14),
|
||||
// v18+: more complex, estimate based on structure
|
||||
// EARFCN(4) + PCI(2) + ... + skip = ~24-34 for RSRP
|
||||
18..=24 => (0, 4, 24),
|
||||
// v35+: 4-antenna support, larger structure
|
||||
35..=40 => (0, 4, 28),
|
||||
// Unknown version, try v7 offsets
|
||||
_ => (0, 4, 14),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Physical Cell ID from measurement
|
||||
pub fn get_pci(&self) -> Option<u16> {
|
||||
let (earfcn_offset, earfcn_size, _) = self.get_offsets();
|
||||
let pci_offset = earfcn_offset + earfcn_size;
|
||||
self.read_u16(pci_offset).map(|v| v & 0x1FF)
|
||||
}
|
||||
|
||||
/// Get EARFCN from measurement
|
||||
pub fn get_earfcn(&self) -> Option<u32> {
|
||||
let (earfcn_offset, earfcn_size, _) = self.get_offsets();
|
||||
if earfcn_size == 2 {
|
||||
self.read_u16(earfcn_offset).map(|v| v as u32)
|
||||
} else {
|
||||
self.read_u32(earfcn_offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get RSRP (Reference Signal Received Power) in dBm.
|
||||
/// Formula: -180 + raw_value * 0.0625
|
||||
pub fn get_rsrp_dbm(&self) -> Option<f32> {
|
||||
let (_, _, rsrp_offset) = self.get_offsets();
|
||||
self.read_u32(rsrp_offset).map(|raw| {
|
||||
let rsrp_raw = raw & 0xFFF;
|
||||
-180.0 + (rsrp_raw as f32) * 0.0625
|
||||
})
|
||||
}
|
||||
|
||||
/// Get RSSI (Received Signal Strength Indicator) in dBm.
|
||||
/// Formula: -110 + raw_value * 0.0625
|
||||
/// RSSI is typically 12 bytes after RSRP (RSRP + avg_RSRP + RSRQ)
|
||||
pub fn get_rssi_dbm(&self) -> Option<f32> {
|
||||
let (_, _, rsrp_offset) = self.get_offsets();
|
||||
let rssi_offset = rsrp_offset + 12; // Skip RSRP(4) + avg_RSRP(4) + RSRQ(4)
|
||||
self.read_u32(rssi_offset).map(|raw| {
|
||||
let rssi_raw = (raw >> 10) & 0x7FF;
|
||||
-110.0 + (rssi_raw as f32) * 0.0625
|
||||
})
|
||||
}
|
||||
|
||||
/// Get RSRQ (Reference Signal Received Quality) in dB.
|
||||
/// Formula: -30 + raw_value * 0.0625
|
||||
pub fn get_rsrq_db(&self) -> Option<f32> {
|
||||
let (_, _, rsrp_offset) = self.get_offsets();
|
||||
let rsrq_offset = rsrp_offset + 8; // Skip RSRP(4) + avg_RSRP(4)
|
||||
self.read_u32(rsrq_offset).map(|raw| {
|
||||
let rsrq_raw = raw & 0x3FF;
|
||||
-30.0 + (rsrq_raw as f32) * 0.0625
|
||||
})
|
||||
}
|
||||
|
||||
/// Get signal strength as i8 for GSMTAP header (clamped to valid range).
|
||||
/// Uses RSRP as the primary signal indicator.
|
||||
pub fn get_signal_dbm_i8(&self) -> Option<i8> {
|
||||
self.get_rsrp_dbm()
|
||||
.map(|rsrp| rsrp.clamp(-128.0, 127.0) as i8)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(endian = "little")]
|
||||
pub struct Timestamp {
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
impl Timestamp {
|
||||
pub fn to_datetime(&self) -> DateTime<FixedOffset> {
|
||||
// Upper 48 bits: epoch at 1980-01-06 00:00:00, incremented by 1 for 1/800s
|
||||
// Lower 16 bits: time since last 1/800s tick in 1/32 chip units
|
||||
let ts_upper = self.ts >> 16;
|
||||
let ts_lower = self.ts & 0xffff;
|
||||
let epoch = chrono::DateTime::parse_from_rfc3339("1980-01-06T00:00:00-00:00").unwrap();
|
||||
let mut delta_seconds = ts_upper as f64 * 1.25;
|
||||
delta_seconds += ts_lower as f64 / 40960.0;
|
||||
let ts_delta = chrono::Duration::milliseconds(delta_seconds as i64);
|
||||
epoch + ts_delta
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "opcode: u32, subopcode: u32", id = "opcode")]
|
||||
pub enum ResponsePayload {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(#[deku(ctx = "subopcode")] LogConfigResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "subopcode: u32", id = "subopcode")]
|
||||
pub enum LogConfigResponse {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask,
|
||||
}
|
||||
|
||||
pub fn build_log_mask_request(
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
accepted_log_codes: &[u32],
|
||||
) -> Request {
|
||||
let mut current_byte: u8 = 0;
|
||||
let mut num_bits_written: u8 = 0;
|
||||
let mut log_mask: Vec<u8> = vec![];
|
||||
for i in 0..log_mask_bitsize {
|
||||
let log_code: u32 = (log_type << 12) | i;
|
||||
if accepted_log_codes.contains(&log_code) {
|
||||
current_byte |= 1 << num_bits_written;
|
||||
}
|
||||
num_bits_written += 1;
|
||||
|
||||
if num_bits_written == 8 || i == log_mask_bitsize - 1 {
|
||||
log_mask.push(current_byte);
|
||||
current_byte = 0;
|
||||
num_bits_written = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type,
|
||||
log_mask_bitsize,
|
||||
log_mask,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
// Just about all of these test cases from manually parsing diag packets w/ QCSuper
|
||||
|
||||
#[test]
|
||||
fn test_request_serialization() {
|
||||
let req = Request::LogConfig(LogConfigRequest::RetrieveIdRanges);
|
||||
assert_eq!(req.to_bytes().unwrap(), vec![115, 0, 0, 0, 1, 0, 0, 0]);
|
||||
|
||||
let req = Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type: 0,
|
||||
log_mask_bitsize: 0,
|
||||
log_mask: vec![],
|
||||
});
|
||||
assert_eq!(
|
||||
req.to_bytes().unwrap(),
|
||||
vec![115, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_log_mask_request() {
|
||||
let log_type = 11;
|
||||
let bitsize = 513;
|
||||
let req = build_log_mask_request(
|
||||
log_type,
|
||||
bitsize,
|
||||
&crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING,
|
||||
);
|
||||
// Expected mask includes:
|
||||
// - 0xB0C0 (LTE RRC): byte 24 = 0x01
|
||||
// - 0xB0E2, 0xB0E3, 0xB0EC, 0xB0ED (NAS): bytes 28-29 = 0x0C, 0x30
|
||||
// - 0xB193 (ML1 Serving Cell Meas): byte 50 = 0x08 (bit 3 for code 0x193 = 403)
|
||||
assert_eq!(
|
||||
req,
|
||||
Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type,
|
||||
log_mask_bitsize: bitsize,
|
||||
log_mask: vec![
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xc, 0x30, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_request_container() {
|
||||
let req = RequestContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
use_mdm: false,
|
||||
mdm_field: -1,
|
||||
hdlc_encapsulated_request: vec![1, 2, 3, 4],
|
||||
};
|
||||
assert_eq!(req.to_bytes().unwrap(), vec![32, 0, 0, 0, 1, 2, 3, 4,]);
|
||||
let req = RequestContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
use_mdm: true,
|
||||
mdm_field: -1,
|
||||
hdlc_encapsulated_request: vec![1, 2, 3, 4],
|
||||
};
|
||||
assert_eq!(
|
||||
req.to_bytes().unwrap(),
|
||||
vec![32, 0, 0, 0, 255, 255, 255, 255, 1, 2, 3, 4,]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logs() {
|
||||
let data = vec![
|
||||
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0, 160, 0,
|
||||
2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1, 238, 173, 213, 77, 208,
|
||||
];
|
||||
let msg = Message::from_bytes((&data, 0)).unwrap().1;
|
||||
assert_eq!(
|
||||
msg,
|
||||
Message::Log {
|
||||
pending_msgs: 0,
|
||||
outer_length: 38,
|
||||
inner_length: 38,
|
||||
log_type: 0xb0c0,
|
||||
timestamp: Timestamp {
|
||||
ts: 72659535985485082
|
||||
},
|
||||
body: LogBody::LteRrcOtaMessage {
|
||||
ext_header_version: 20,
|
||||
packet: LteRrcOtaPacket::V8 {
|
||||
rrc_rel_maj: 14,
|
||||
rrc_rel_min: 48,
|
||||
bearer_id: 0,
|
||||
phy_cell_id: 160,
|
||||
earfcn: 2050,
|
||||
sfn_subfn: 4057,
|
||||
pdu_num: 5,
|
||||
sib_mask: 0,
|
||||
len: 7,
|
||||
packet: vec![0x40, 0x1, 0xee, 0xad, 0xd5, 0x4d, 0xd0],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn make_container(data_type: DataType, message: HdlcEncapsulatedMessage) -> MessagesContainer {
|
||||
MessagesContainer {
|
||||
data_type,
|
||||
num_messages: 1,
|
||||
messages: vec![message],
|
||||
}
|
||||
}
|
||||
|
||||
// this log is based on one captured on a real device -- if it fails to
|
||||
// serialize or deserialize, that's probably a problem with this mock, not
|
||||
// the DiagReader implementation
|
||||
fn get_test_message(payload: &[u8]) -> (HdlcEncapsulatedMessage, Message) {
|
||||
let length_with_payload = 31 + payload.len() as u16;
|
||||
let message = Message::Log {
|
||||
pending_msgs: 0,
|
||||
outer_length: length_with_payload,
|
||||
inner_length: length_with_payload,
|
||||
log_type: 0xb0c0,
|
||||
timestamp: Timestamp {
|
||||
ts: 72659535985485082,
|
||||
},
|
||||
body: LogBody::LteRrcOtaMessage {
|
||||
ext_header_version: 20,
|
||||
packet: LteRrcOtaPacket::V8 {
|
||||
rrc_rel_maj: 14,
|
||||
rrc_rel_min: 48,
|
||||
bearer_id: 0,
|
||||
phy_cell_id: 160,
|
||||
earfcn: 2050,
|
||||
sfn_subfn: 4057,
|
||||
pdu_num: 5,
|
||||
sib_mask: 0,
|
||||
len: payload.len() as u16,
|
||||
packet: payload.to_vec(),
|
||||
},
|
||||
},
|
||||
};
|
||||
let serialized = message
|
||||
.to_bytes()
|
||||
.expect("failed to serialize test message");
|
||||
let encapsulated_data = hdlc::hdlc_encapsulate(&serialized, &CRC_CCITT);
|
||||
let encapsulated = HdlcEncapsulatedMessage {
|
||||
len: encapsulated_data.len() as u32,
|
||||
data: encapsulated_data,
|
||||
};
|
||||
(encapsulated, message)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_containers_with_multiple_messages() {
|
||||
let (encapsulated1, message1) = get_test_message(&[1]);
|
||||
let (encapsulated2, message2) = get_test_message(&[2]);
|
||||
let mut container = make_container(DataType::UserSpace, encapsulated1);
|
||||
container.messages.push(encapsulated2);
|
||||
container.num_messages += 1;
|
||||
assert_eq!(container.into_messages(), vec![Ok(message1), Ok(message2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_containers_with_concatenated_message() {
|
||||
let (mut encapsulated1, message1) = get_test_message(&[1]);
|
||||
let (encapsulated2, message2) = get_test_message(&[2]);
|
||||
encapsulated1.data.extend(encapsulated2.data);
|
||||
encapsulated1.len += encapsulated2.len;
|
||||
let container = make_container(DataType::UserSpace, encapsulated1);
|
||||
assert_eq!(container.into_messages(), vec![Ok(message1), Ok(message2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handles_parsing_errors() {
|
||||
let (encapsulated1, message1) = get_test_message(&[1]);
|
||||
let bad_message = hdlc::hdlc_encapsulate(&[0x01, 0x02, 0x03, 0x04], &CRC_CCITT);
|
||||
let encapsulated2 = HdlcEncapsulatedMessage {
|
||||
len: bad_message.len() as u32,
|
||||
data: bad_message,
|
||||
};
|
||||
let mut container = make_container(DataType::UserSpace, encapsulated1);
|
||||
container.messages.push(encapsulated2);
|
||||
container.num_messages += 1;
|
||||
let result = container.into_messages();
|
||||
assert_eq!(result[0], Ok(message1));
|
||||
assert!(matches!(
|
||||
result[1],
|
||||
Err(DiagParsingError::MessageParsingError(_, _))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handles_encapsulation_errors() {
|
||||
let (encapsulated1, message1) = get_test_message(&[1]);
|
||||
let bad_encapsulation = HdlcEncapsulatedMessage {
|
||||
len: 4,
|
||||
data: vec![0x01, 0x02, 0x03, 0x04],
|
||||
};
|
||||
let mut container = make_container(DataType::UserSpace, encapsulated1);
|
||||
container.messages.push(bad_encapsulation);
|
||||
container.num_messages += 1;
|
||||
let result = container.into_messages();
|
||||
assert_eq!(result[0], Ok(message1));
|
||||
assert!(matches!(
|
||||
result[1],
|
||||
Err(DiagParsingError::HdlcDecapsulationError(_, _))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_inner_length_underflow() {
|
||||
// Regression test: inner_length < 12 previously caused panic.
|
||||
// Fixed by using saturating_sub in Message::Log body length calculation.
|
||||
let fuzz_data = b"\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
let _ = Message::from_bytes((fuzz_data, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_nas_hdr_len_underflow() {
|
||||
// Regression test for two things:
|
||||
// - hdr_len < 4 previously caused panic in Nas4GMessage.
|
||||
// - Upgrading to deku 0.20 caused incorrect parsing behavior (double-read of discriminant)
|
||||
let nas_msg =
|
||||
b"\x10\x00\x14\x00\x02\x00\xe2\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00";
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((nas_msg, 0)).unwrap();
|
||||
|
||||
assert_eq!(rest.len(), 0);
|
||||
assert!(
|
||||
matches!(
|
||||
msg,
|
||||
Message::Log {
|
||||
log_type: 0xb0e2,
|
||||
body: LogBody::Nas4GMessage {
|
||||
direction: Nas4GMessageDirection::Downlink,
|
||||
..
|
||||
},
|
||||
..
|
||||
}
|
||||
),
|
||||
"Unexpected message: {:?}",
|
||||
msg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_ip_traffic_hdr_len_underflow() {
|
||||
// Regression test: hdr_len < 8 previously caused panic in IpTraffic.
|
||||
// Fixed by using saturating_sub for msg length calculation.
|
||||
let ip_msg = b"\x10\x00\x14\x00\x02\x00\xeb\x11\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00";
|
||||
let _ = Message::from_bytes((ip_msg, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_response_opcode_parsing() {
|
||||
// Regression test: Upgrading to deku 0.20 caused incorrect parsing of Response messages.
|
||||
// The issue was that deku 0.20 requires an `id` field for `id_pat = "_"` variants,
|
||||
// but in deku 0.18 the discriminant was NOT consumed from the stream.
|
||||
// This caused a 1-byte offset, making opcode and all subsequent fields misaligned.
|
||||
// Fixed by splitting the opcode into 4 separate u8 fields so the discriminant byte
|
||||
// becomes the first byte of the opcode, matching the old deku 0.18 behavior.
|
||||
let response_msg = b"\x73\x00\x00\x00\x03\x00\x00\x00\x0a\x00\xec\xb0\x8e\x51\x02\x6f\x2a\xc5\x0b\x01\x01\x09\x05\x00\x07\x45\x8e\x14\x7d";
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((response_msg, 0)).unwrap();
|
||||
|
||||
// Verify the opcode is correctly parsed as 115 (0x73 in first byte)
|
||||
// In little-endian: [0x73, 0x00, 0x00, 0x00] = 0x00000073 = 115
|
||||
assert!(
|
||||
matches!(
|
||||
msg,
|
||||
Message::Response {
|
||||
opcode1: 0x73,
|
||||
opcode2: 0x00,
|
||||
opcode3: 0x00,
|
||||
opcode4: 0x00,
|
||||
subopcode: 3,
|
||||
status: 2968256522, // [0x0a, 0x00, 0xec, 0xb0] in LE
|
||||
payload: ResponsePayload::LogConfig(LogConfigResponse::SetMask),
|
||||
}
|
||||
),
|
||||
"Unexpected message: {:?}",
|
||||
msg
|
||||
);
|
||||
|
||||
// Verify we consumed the expected number of bytes
|
||||
assert_eq!(rest.len(), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lte_ml1_serving_cell_meas_parsing() {
|
||||
// Test parsing of 0xB193 LTE ML1 Serving Cell Measurement log
|
||||
// with subpacket version 18 (common on Orbic RC400L)
|
||||
//
|
||||
// Structure:
|
||||
// - Log message header (type=16, log_type=0xB193)
|
||||
// - LteMl1ServingCellMeasData with v18 subpacket containing RSRP=-95dBm
|
||||
//
|
||||
// RSRP calculation: -180 + (raw & 0xFFF) * 0.0625
|
||||
// For -95 dBm: raw = (-95 + 180) / 0.0625 = 1360 = 0x550
|
||||
|
||||
let mut msg_bytes: Vec<u8> = vec![
|
||||
// Log message header
|
||||
0x10, // Message type: Log (16)
|
||||
0x00, // pending_msgs
|
||||
0x38, 0x00, // outer_length: 56
|
||||
0x34, 0x00, // inner_length: 52
|
||||
0x93, 0xB1, // log_type: 0xB193 (LTE ML1 Serving Cell Meas)
|
||||
// timestamp (8 bytes, arbitrary)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// LteMl1ServingCellMeasData
|
||||
0x01, // main_version
|
||||
0x01, // num_subpackets
|
||||
0x00, 0x00, // reserved
|
||||
0x00, // subpacket_id
|
||||
0x12, // subpacket_version: 18
|
||||
0x28, 0x00, // subpacket_size: 40 bytes (including header)
|
||||
];
|
||||
|
||||
// Subpacket data (36 bytes = 40 - 4 for header)
|
||||
// For v18: EARFCN at offset 0 (4 bytes), PCI at offset 4 (2 bytes), RSRP at offset 24
|
||||
let mut subpacket_data = vec![0u8; 36];
|
||||
// EARFCN = 975 at offset 0 (u32 LE)
|
||||
subpacket_data[0..4].copy_from_slice(&975u32.to_le_bytes());
|
||||
// PCI = 446 at offset 4 (u16 LE, only lower 9 bits used)
|
||||
subpacket_data[4..6].copy_from_slice(&446u16.to_le_bytes());
|
||||
// RSRP raw = 1360 (0x550) at offset 24 (u32 LE)
|
||||
// This gives RSRP = -180 + 1360 * 0.0625 = -95 dBm
|
||||
subpacket_data[24..28].copy_from_slice(&1360u32.to_le_bytes());
|
||||
|
||||
msg_bytes.extend(subpacket_data);
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((&msg_bytes, 0)).unwrap();
|
||||
|
||||
assert_eq!(rest.len(), 0, "Should consume all bytes");
|
||||
|
||||
if let Message::Log {
|
||||
log_type,
|
||||
body: LogBody::LteMl1ServingCellMeas { meas },
|
||||
..
|
||||
} = msg
|
||||
{
|
||||
assert_eq!(log_type, 0xB193);
|
||||
assert_eq!(meas.subpacket_version, 18);
|
||||
|
||||
// Verify RSRP extraction
|
||||
let rsrp = meas.get_rsrp_dbm().expect("Should extract RSRP");
|
||||
assert!(
|
||||
(rsrp - (-95.0)).abs() < 0.1,
|
||||
"RSRP should be -95 dBm, got {}",
|
||||
rsrp
|
||||
);
|
||||
|
||||
// Verify PCI extraction
|
||||
let pci = meas.get_pci().expect("Should extract PCI");
|
||||
assert_eq!(pci, 446);
|
||||
|
||||
// Verify EARFCN extraction
|
||||
let earfcn = meas.get_earfcn().expect("Should extract EARFCN");
|
||||
assert_eq!(earfcn, 975);
|
||||
|
||||
// Verify i8 conversion for GSMTAP header
|
||||
let signal_dbm = meas.get_signal_dbm_i8().expect("Should get signal_dbm");
|
||||
assert_eq!(signal_dbm, -95);
|
||||
} else {
|
||||
panic!("Expected LteMl1ServingCellMeas message, got {:?}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
//! Diag ML1 measurement log serialization/deserialization. These are pretty
|
||||
//! much entirely based on Shinjo Park's work in scat, since we couldn't find
|
||||
//! any other documentation for the logs' structure.
|
||||
|
||||
use deku::prelude::*;
|
||||
use deku::ctx::Order;
|
||||
|
||||
fn decode_rsrp(rsrp: u16) -> f32 {
|
||||
rsrp as f32 / 16.0 - 180.0
|
||||
}
|
||||
|
||||
fn decode_rssi(rssi: u16) -> f32 {
|
||||
rssi as f32 / 16.0 - 110.0
|
||||
}
|
||||
|
||||
fn decode_rsrq(rsrq: u16) -> f32 {
|
||||
rsrq as f32 / 16.0 - 30.0
|
||||
}
|
||||
|
||||
pub mod serving_cell {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(bit_order = "lsb")]
|
||||
pub struct MeasurementAndEvaluation {
|
||||
pub header: MeasurementAndEvaluationHeader,
|
||||
#[deku(bits = 12, pad_bits_after = "20")]
|
||||
meas_rsrp: u16,
|
||||
avg_rsrp: u32,
|
||||
#[deku(bits = 10, pad_bits_after = "22")]
|
||||
meas_rsrq: u16,
|
||||
#[deku(pad_bits_before = "10", bits = 11, pad_bits_after = "11")]
|
||||
meas_rssi: u16,
|
||||
rxlev: u32,
|
||||
s_search: u32,
|
||||
#[deku(cond = "header.get_rrc_rel() == 0x01")]
|
||||
r9_data: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "_: Order", id_type = "u8", bit_order = "lsb")]
|
||||
pub enum MeasurementAndEvaluationHeader {
|
||||
#[deku(id = "4")]
|
||||
V4 {
|
||||
rrc_rel: u8,
|
||||
_reserved: u16,
|
||||
earfcn: u16,
|
||||
#[deku(bits = 9)]
|
||||
pci: u16,
|
||||
#[deku(bits = 7)]
|
||||
serv_layer_priority: u8,
|
||||
},
|
||||
#[deku(id = "5")]
|
||||
V5 {
|
||||
rrc_rel: u8,
|
||||
_reserved: u16,
|
||||
earfcn: u32,
|
||||
#[deku(bits = 9)]
|
||||
pci: u16,
|
||||
#[deku(bits = 7, pad_bytes_after = "2")]
|
||||
serv_layer_priority: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl MeasurementAndEvaluationHeader {
|
||||
fn get_rrc_rel(&self) -> u8 {
|
||||
match self {
|
||||
MeasurementAndEvaluationHeader::V4 { rrc_rel, .. } => *rrc_rel,
|
||||
MeasurementAndEvaluationHeader::V5 { rrc_rel, .. } => *rrc_rel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MeasurementAndEvaluation {
|
||||
pub fn get_pci(&self) -> u16 {
|
||||
match &self.header {
|
||||
MeasurementAndEvaluationHeader::V4 { pci, .. } => *pci,
|
||||
MeasurementAndEvaluationHeader::V5 { pci, .. } => *pci,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match &self.header {
|
||||
MeasurementAndEvaluationHeader::V4 { earfcn, .. } => *earfcn as u32,
|
||||
MeasurementAndEvaluationHeader::V5 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_meas_rsrp(&self) -> f32 {
|
||||
decode_rsrp(self.meas_rsrp)
|
||||
}
|
||||
|
||||
pub fn get_meas_rssi(&self) -> f32 {
|
||||
decode_rssi(self.meas_rssi)
|
||||
}
|
||||
|
||||
pub fn get_meas_rsrq(&self) -> f32 {
|
||||
decode_rsrq(self.meas_rsrq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod neighbor_cells {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
|
||||
#[deku(id_type = "u8", bit_order = "lsb")]
|
||||
pub enum MeasurementsHeader {
|
||||
#[deku(id = "4")]
|
||||
V4 {
|
||||
rrc_rel: u8,
|
||||
_reserved1: u16,
|
||||
earfcn: u16,
|
||||
#[deku(bits = 6)]
|
||||
q_rxlevmin: u8,
|
||||
#[deku(bits = 10)]
|
||||
n_cells: u16,
|
||||
},
|
||||
#[deku(id = "5")]
|
||||
V5 {
|
||||
rrc_rel: u8,
|
||||
_reserved1: u16,
|
||||
earfcn: u32,
|
||||
#[deku(bits = 6)]
|
||||
q_rxlevmin: u8,
|
||||
#[deku(bits = 26)]
|
||||
n_cells: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl MeasurementsHeader {
|
||||
fn get_n_cells(&self) -> usize {
|
||||
match self {
|
||||
MeasurementsHeader::V4 { n_cells, .. } => *n_cells as usize,
|
||||
MeasurementsHeader::V5 { n_cells, .. } => *n_cells as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
|
||||
pub struct Measurements {
|
||||
pub header: MeasurementsHeader,
|
||||
#[deku(count = "header.get_n_cells()")]
|
||||
pub cells: Vec<MeasurementsCell>
|
||||
}
|
||||
|
||||
impl Measurements {
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match &self.header {
|
||||
MeasurementsHeader::V4 { earfcn, .. } => *earfcn as u32,
|
||||
MeasurementsHeader::V5 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, DekuRead, DekuWrite, PartialEq)]
|
||||
#[deku(bit_order = "lsb")]
|
||||
pub struct MeasurementsCell {
|
||||
#[deku(bits = 9)]
|
||||
pub pci: u16,
|
||||
#[deku(bits = 11)]
|
||||
meas_rssi: u16,
|
||||
#[deku(bits = 12)]
|
||||
meas_rsrp: u16,
|
||||
#[deku(pad_bits_before = "12", bits = 12, pad_bits_after = "8")]
|
||||
avg_rsrp: u16,
|
||||
#[deku(pad_bits_before = "12", bits = 10, pad_bits_after = "10")]
|
||||
meas_rsrq: u16,
|
||||
#[deku(bits = 10, pad_bits_after = "10")]
|
||||
avg_rsrq: u16,
|
||||
#[deku(bits = 6, pad_bits_after = "6")]
|
||||
s_rxlev: u16,
|
||||
n_freq_offset: u16,
|
||||
val5: u16,
|
||||
ant0_offset: u32,
|
||||
ant1_offset: u32,
|
||||
unk1: u32,
|
||||
}
|
||||
|
||||
impl MeasurementsCell {
|
||||
pub fn get_meas_rsrp(&self) -> f32 {
|
||||
decode_rsrp(self.meas_rsrp)
|
||||
}
|
||||
|
||||
pub fn get_meas_rssi(&self) -> f32 {
|
||||
decode_rssi(self.meas_rssi)
|
||||
}
|
||||
|
||||
pub fn get_meas_rsrq(&self) -> f32 {
|
||||
decode_rsrq(self.meas_rsrq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::diag::diaglog::LogBody;
|
||||
use crate::log_codes::{LOG_LTE_ML1_NEIGHBOR_MEAS, LOG_LTE_ML1_SERVING_CELL_MEAS_RESP_AND_EVAL};
|
||||
use std::io::{Cursor, Seek};
|
||||
|
||||
fn unhexlify(hexlified_bytes: &str) -> (usize, Reader<Cursor<Vec<u8>>>) {
|
||||
let byte_len = hexlified_bytes.len() / 2;
|
||||
let bytes = (0..hexlified_bytes.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&hexlified_bytes[i..i+2], 16).unwrap())
|
||||
.collect();
|
||||
(byte_len, Reader::new(Cursor::new(bytes)))
|
||||
}
|
||||
|
||||
fn parse_ncell_measurements(hexlified_bytes: &str) -> (u8, neighbor_cells::Measurements) {
|
||||
let (total_size, mut reader) = unhexlify(hexlified_bytes);
|
||||
match LogBody::from_reader_with_ctx(&mut reader, (LOG_LTE_ML1_NEIGHBOR_MEAS as u16, 0)) {
|
||||
Ok(LogBody::LteMl1NeighborCellsMeasurements { data }) => {
|
||||
if !reader.end() {
|
||||
let leftover_bits = reader.rest();
|
||||
let leftover_bytes = total_size - reader.stream_position().unwrap() as usize;
|
||||
panic!("failed to read entire buffer ({} bytes, {} bits left)", leftover_bytes, leftover_bits.len());
|
||||
}
|
||||
let pkt_version = match data.header {
|
||||
neighbor_cells::MeasurementsHeader::V4 { .. } => 4,
|
||||
neighbor_cells::MeasurementsHeader::V5 { .. } => 5,
|
||||
};
|
||||
(pkt_version, data)
|
||||
},
|
||||
Ok(x) => panic!("expected MeasurementAndEvaluation, but parsed {:?}", x),
|
||||
Err(x) => panic!("failed to parse MeasurementAndEvaluation {:?}", x),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_meas_eval(hexlified_bytes: &str) -> (u8, serving_cell::MeasurementAndEvaluation) {
|
||||
let (total_size, mut reader) = unhexlify(hexlified_bytes);
|
||||
match LogBody::from_reader_with_ctx(&mut reader, (LOG_LTE_ML1_SERVING_CELL_MEAS_RESP_AND_EVAL as u16, 0)) {
|
||||
Ok(LogBody::LteMl1ServingCellMeasurementAndEvaluation { data }) => {
|
||||
if !reader.end() {
|
||||
let leftover_bits = reader.rest();
|
||||
let leftover_bytes = total_size - reader.stream_position().unwrap() as usize;
|
||||
panic!("failed to read entire buffer ({} bytes, {} bits left)", leftover_bytes, leftover_bits.len());
|
||||
}
|
||||
let pkt_version = match data.header {
|
||||
serving_cell::MeasurementAndEvaluationHeader::V4 { .. } => 4,
|
||||
serving_cell::MeasurementAndEvaluationHeader::V5 { .. } => 5,
|
||||
};
|
||||
(pkt_version, data)
|
||||
},
|
||||
Ok(x) => panic!("expected MeasurementAndEvaluation, but parsed {:?}", x),
|
||||
Err(x) => panic!("failed to parse MeasurementAndEvaluation {:?}", x),
|
||||
}
|
||||
}
|
||||
|
||||
fn scell_meas_and_eval_case(
|
||||
hexlified_bytes: &str,
|
||||
pkt_version: u8,
|
||||
pci: u16,
|
||||
earfcn: u32,
|
||||
rsrp: f32,
|
||||
rsrq: f32,
|
||||
rssi: f32
|
||||
) {
|
||||
let (parsed_pkt_version, data) = parse_meas_eval(hexlified_bytes);
|
||||
assert_eq!(parsed_pkt_version, pkt_version);
|
||||
assert_eq!(data.get_pci(), pci, "incorrect pci");
|
||||
assert_eq!(data.get_earfcn(), earfcn, "incorrect earfcn");
|
||||
assert_eq!(data.get_meas_rsrp(), rsrp, "incorrect rsrp");
|
||||
assert_eq!(data.get_meas_rsrq(), rsrq, "incorrect rsrq");
|
||||
assert_eq!(data.get_meas_rssi(), rssi, "incorrect rssi");
|
||||
}
|
||||
|
||||
// Adapted from scat's TestDiagLteLogParser::test_parse_lte_ml1_scell_meas,
|
||||
// but edited to print full-precision floats
|
||||
#[test]
|
||||
fn test_scell_meas() {
|
||||
scell_meas_and_eval_case(
|
||||
"040100009C18D60AECC44E00E2244E00FFFCE30FFED80A0047AD56021D310100A2624100",
|
||||
4,
|
||||
214,
|
||||
6300,
|
||||
-101.25,
|
||||
-14.0625,
|
||||
-66.625
|
||||
);
|
||||
scell_meas_and_eval_case(
|
||||
"05010000160d0000d40e00004bb444005444450039e514133149070048adfe019f310100a23f0000",
|
||||
5,
|
||||
212,
|
||||
3350,
|
||||
-111.3125,
|
||||
-10.4375,
|
||||
-80.875,
|
||||
);
|
||||
scell_meas_and_eval_case(
|
||||
"05010000f424000a4d43434d4e434d41524b45527c307c3236327c317c34323330333233347c7c4d43434d4e434d41524b45520a0a434f504d41524b45527c434f504552524f5232363230317c434f504d41524b45520a006306000057755500577555001d75d4111d290b0048ad7e02dd370100a27f4100",
|
||||
5,
|
||||
333,
|
||||
167781620,
|
||||
-127.125,
|
||||
-22.25,
|
||||
2.75,
|
||||
);
|
||||
scell_meas_and_eval_case(
|
||||
"0501000000190000a90d0000d9944d00d9944d006081d5d55d2568bc48ad3e027f314fe0891900e0",
|
||||
5,
|
||||
425,
|
||||
6400,
|
||||
-102.4375,
|
||||
-8.0,
|
||||
-77.4375,
|
||||
);
|
||||
}
|
||||
|
||||
fn ncell_meas_case(
|
||||
hexlified_bytes: &str,
|
||||
pkt_version: u8,
|
||||
earfcn: u32,
|
||||
cells: Vec<(u16, f32, f32, f32)>,
|
||||
) {
|
||||
let (parsed_pkt_version, data) = parse_ncell_measurements(hexlified_bytes);
|
||||
assert_eq!(parsed_pkt_version, pkt_version, "incorrect pkt_version");
|
||||
assert_eq!(data.cells.len(), cells.len(), "incorrect number of cells");
|
||||
assert_eq!(data.get_earfcn(), earfcn, "incorrect earfcn");
|
||||
for (parsed, (pci, rsrp, rssi, rsrq)) in data.cells.iter().zip(cells) {
|
||||
assert_eq!(parsed.pci, pci, "incorrect pci");
|
||||
assert_eq!(parsed.get_meas_rsrp(), rsrp, "incorrect rsrp");
|
||||
assert_eq!(parsed.get_meas_rssi(), rssi, "incorrect rssi");
|
||||
assert_eq!(parsed.get_meas_rsrq(), rsrq, "incorrect rsrq");
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from scat's TestDiagLteLogParser::test_parse_lte_ml1_ncell_meas,
|
||||
// but edited to print full-precision floats
|
||||
#[test]
|
||||
fn test_ncell_meas() {
|
||||
ncell_meas_case(
|
||||
"040100009C1847008348E44DDEA44C00CAB4CC32B6D8420300000000FF773301FF77330122020100",
|
||||
4,
|
||||
6300,
|
||||
vec![
|
||||
(131, -102.125, -75.75, -17.3125),
|
||||
]
|
||||
);
|
||||
ncell_meas_case(
|
||||
"05010000160d0000480000006cea413bb4433b00b4f3cc33cf3c130200000000ffefc00fffefc00f45081600",
|
||||
5,
|
||||
3350,
|
||||
vec![
|
||||
(108, -120.75, -94.6875, -17.0625),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
//! Diag LogBody serialization/deserialization
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use deku::prelude::*;
|
||||
|
||||
pub mod measurement;
|
||||
pub mod rrc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "log_type: u16, hdr_len: u16", id = "log_type")]
|
||||
pub enum LogBody {
|
||||
#[deku(id = "0x412f")]
|
||||
WcdmaSignallingMessage {
|
||||
channel_type: u8,
|
||||
radio_bearer: u8,
|
||||
length: u16,
|
||||
#[deku(count = "length")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x512f")]
|
||||
GsmRrSignallingMessage {
|
||||
channel_type: u8,
|
||||
message_type: u8,
|
||||
length: u8,
|
||||
#[deku(count = "length")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x5226")]
|
||||
GprsMacSignallingMessage {
|
||||
channel_type: u8,
|
||||
message_type: u8,
|
||||
length: u8,
|
||||
#[deku(count = "length")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0xb0c0")]
|
||||
LteRrcOtaMessage {
|
||||
ext_header_version: u8,
|
||||
#[deku(ctx = "*ext_header_version")]
|
||||
packet: rrc::LteRrcOtaPacket,
|
||||
},
|
||||
// the four NAS command opcodes refer to:
|
||||
// * 0xb0e2: plain ESM NAS message (incoming)
|
||||
// * 0xb0e3: plain ESM NAS message (outgoing)
|
||||
// * 0xb0ec: plain EMM NAS message (incoming)
|
||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||
#[deku(id_pat = "0xb0e2 | 0xb0e3 | 0xb0ec | 0xb0ed")]
|
||||
Nas4GMessage {
|
||||
#[deku(skip, default = "log_type")]
|
||||
log_type: u16,
|
||||
#[deku(ctx = "*log_type")]
|
||||
direction: Nas4GMessageDirection,
|
||||
ext_header_version: u8,
|
||||
rrc_rel: u8,
|
||||
rrc_version_minor: u8,
|
||||
rrc_version_major: u8,
|
||||
// message length = hdr_len - (sizeof(ext_header_version) + sizeof(rrc_rel) + sizeof(rrc_version_minor) + sizeof(rrc_version_major))
|
||||
#[deku(count = "hdr_len.saturating_sub(4)")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x11eb")]
|
||||
IpTraffic {
|
||||
// is this right?? based on https://github.com/P1sec/QCSuper/blob/81dbaeee15ec7747e899daa8e3495e27cdcc1264/src/modules/pcap_dump.py#L378
|
||||
#[deku(count = "hdr_len.saturating_sub(8)")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0x713a")]
|
||||
UmtsNasOtaMessage {
|
||||
is_uplink: u8,
|
||||
length: u32,
|
||||
#[deku(count = "length")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0xb821")]
|
||||
NrRrcOtaMessage {
|
||||
#[deku(count = "hdr_len")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0xb17f")]
|
||||
LteMl1ServingCellMeasurementAndEvaluation {
|
||||
data: measurement::serving_cell::MeasurementAndEvaluation,
|
||||
},
|
||||
#[deku(id = "0xb180")]
|
||||
LteMl1NeighborCellsMeasurements {
|
||||
data: measurement::neighbor_cells::Measurements,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "log_type: u16", id = "log_type")]
|
||||
pub enum Nas4GMessageDirection {
|
||||
// * 0xb0e2: plain ESM NAS message (incoming)
|
||||
// * 0xb0e3: plain ESM NAS message (outgoing)
|
||||
// * 0xb0ec: plain EMM NAS message (incoming)
|
||||
// * 0xb0ed: plain EMM NAS message (outgoing)
|
||||
#[deku(id_pat = "0xb0e2 | 0xb0ec")]
|
||||
Downlink,
|
||||
#[deku(id_pat = "0xb0e3 | 0xb0ed")]
|
||||
Uplink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(endian = "little")]
|
||||
pub struct Timestamp {
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
impl Timestamp {
|
||||
pub fn to_datetime(&self) -> DateTime<FixedOffset> {
|
||||
// Upper 48 bits: epoch at 1980-01-06 00:00:00, incremented by 1 for 1/800s
|
||||
// Lower 16 bits: time since last 1/800s tick in 1/32 chip units
|
||||
let ts_upper = self.ts >> 16;
|
||||
let ts_lower = self.ts & 0xffff;
|
||||
let epoch = chrono::DateTime::parse_from_rfc3339("1980-01-06T00:00:00-00:00").unwrap();
|
||||
let mut delta_seconds = ts_upper as f64 * 1.25;
|
||||
delta_seconds += ts_lower as f64 / 40960.0;
|
||||
let ts_delta = chrono::Duration::milliseconds(delta_seconds as i64);
|
||||
epoch + ts_delta
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::diag::Message;
|
||||
|
||||
#[test]
|
||||
fn test_logs() {
|
||||
let data = vec![
|
||||
16, 0, 38, 0, 38, 0, 192, 176, 26, 165, 245, 135, 118, 35, 2, 1, 20, 14, 48, 0, 160, 0,
|
||||
2, 8, 0, 0, 217, 15, 5, 0, 0, 0, 0, 7, 0, 64, 1, 238, 173, 213, 77, 208,
|
||||
];
|
||||
let msg = Message::from_bytes((&data, 0)).unwrap().1;
|
||||
assert_eq!(
|
||||
msg,
|
||||
Message::Log {
|
||||
pending_msgs: 0,
|
||||
outer_length: 38,
|
||||
inner_length: 38,
|
||||
log_type: 0xb0c0,
|
||||
timestamp: Timestamp {
|
||||
ts: 72659535985485082
|
||||
},
|
||||
body: LogBody::LteRrcOtaMessage {
|
||||
ext_header_version: 20,
|
||||
packet: rrc::LteRrcOtaPacket::V8 {
|
||||
rrc_rel_maj: 14,
|
||||
rrc_rel_min: 48,
|
||||
bearer_id: 0,
|
||||
phy_cell_id: 160,
|
||||
earfcn: 2050,
|
||||
sfn_subfn: 4057,
|
||||
pdu_num: 5,
|
||||
sib_mask: 0,
|
||||
len: 7,
|
||||
packet: vec![0x40, 0x1, 0xee, 0xad, 0xd5, 0x4d, 0xd0],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_inner_length_underflow() {
|
||||
// Regression test: inner_length < 12 previously caused panic.
|
||||
// Fixed by using saturating_sub in Message::Log body length calculation.
|
||||
let fuzz_data = b"\x10\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
let _ = Message::from_bytes((fuzz_data, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_nas_hdr_len_underflow() {
|
||||
// Regression test for two things:
|
||||
// - hdr_len < 4 previously caused panic in Nas4GMessage.
|
||||
// - Upgrading to deku 0.20 caused incorrect parsing behavior (double-read of discriminant)
|
||||
let nas_msg =
|
||||
b"\x10\x00\x14\x00\x02\x00\xe2\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00";
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((nas_msg, 0)).unwrap();
|
||||
|
||||
assert_eq!(rest.len(), 0);
|
||||
assert!(
|
||||
matches!(
|
||||
msg,
|
||||
Message::Log {
|
||||
log_type: 0xb0e2,
|
||||
body: LogBody::Nas4GMessage {
|
||||
direction: Nas4GMessageDirection::Downlink,
|
||||
..
|
||||
},
|
||||
..
|
||||
}
|
||||
),
|
||||
"Unexpected message: {:?}",
|
||||
msg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_ip_traffic_hdr_len_underflow() {
|
||||
// Regression test: hdr_len < 8 previously caused panic in IpTraffic.
|
||||
// Fixed by using saturating_sub for msg length calculation.
|
||||
let ip_msg = b"\x10\x00\x14\x00\x02\x00\xeb\x11\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00";
|
||||
let _ = Message::from_bytes((ip_msg, 0));
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
//! Diag LTE RRC serialization/deserialization
|
||||
|
||||
use deku::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "ext_header_version: u8", id = "ext_header_version")]
|
||||
pub enum LteRrcOtaPacket {
|
||||
#[deku(id_pat = "0..=4")]
|
||||
V0 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u16,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "5..=7")]
|
||||
V5 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u16,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "8..=24")]
|
||||
V8 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u32,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
#[deku(id_pat = "25..")]
|
||||
V25 {
|
||||
rrc_rel_maj: u8,
|
||||
rrc_rel_min: u8,
|
||||
nr_rrc_rel_maj: u8,
|
||||
nr_rrc_rel_min: u8,
|
||||
bearer_id: u8,
|
||||
phy_cell_id: u16,
|
||||
earfcn: u32,
|
||||
sfn_subfn: u16,
|
||||
pdu_num: u8,
|
||||
sib_mask: u32,
|
||||
len: u16,
|
||||
#[deku(count = "len")]
|
||||
packet: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LteRrcOtaPacket {
|
||||
fn get_sfn_subfn(&self) -> u16 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V5 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V8 { sfn_subfn, .. } => *sfn_subfn,
|
||||
LteRrcOtaPacket::V25 { sfn_subfn, .. } => *sfn_subfn,
|
||||
}
|
||||
}
|
||||
pub fn get_sfn(&self) -> u32 {
|
||||
self.get_sfn_subfn() as u32 >> 4
|
||||
}
|
||||
|
||||
pub fn get_subfn(&self) -> u8 {
|
||||
(self.get_sfn_subfn() & 0xf) as u8
|
||||
}
|
||||
|
||||
pub fn get_pdu_num(&self) -> u8 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V5 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V8 { pdu_num, .. } => *pdu_num,
|
||||
LteRrcOtaPacket::V25 { pdu_num, .. } => *pdu_num,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { earfcn, .. } => *earfcn as u32,
|
||||
LteRrcOtaPacket::V5 { earfcn, .. } => *earfcn as u32,
|
||||
LteRrcOtaPacket::V8 { earfcn, .. } => *earfcn,
|
||||
LteRrcOtaPacket::V25 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_payload(self) -> Vec<u8> {
|
||||
match self {
|
||||
LteRrcOtaPacket::V0 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V5 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V8 { packet, .. } => packet,
|
||||
LteRrcOtaPacket::V25 { packet, .. } => packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
//! Diag protocol serialization/deserialization
|
||||
|
||||
use crc::{Algorithm, Crc};
|
||||
use deku::prelude::*;
|
||||
|
||||
use crate::hdlc::{self, hdlc_decapsulate};
|
||||
use log::warn;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod diaglog;
|
||||
|
||||
use diaglog::{LogBody, Timestamp};
|
||||
|
||||
pub const MESSAGE_TERMINATOR: u8 = 0x7e;
|
||||
pub const MESSAGE_ESCAPE_CHAR: u8 = 0x7d;
|
||||
|
||||
pub const ESCAPED_MESSAGE_TERMINATOR: u8 = 0x5e;
|
||||
pub const ESCAPED_MESSAGE_ESCAPE_CHAR: u8 = 0x5d;
|
||||
|
||||
#[derive(Debug, Clone, DekuWrite)]
|
||||
pub struct RequestContainer {
|
||||
pub data_type: DataType,
|
||||
#[deku(skip)]
|
||||
pub use_mdm: bool,
|
||||
#[deku(skip, cond = "!*use_mdm")]
|
||||
pub mdm_field: i32,
|
||||
pub hdlc_encapsulated_request: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||
#[deku(id_type = "u32")]
|
||||
pub enum Request {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(LogConfigRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuWrite)]
|
||||
#[deku(id_type = "u32", endian = "little")]
|
||||
pub enum LogConfigRequest {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges,
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask {
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
log_mask: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u32", endian = "little")]
|
||||
pub enum DataType {
|
||||
#[deku(id = "32")]
|
||||
UserSpace,
|
||||
#[deku(id_pat = "_")]
|
||||
Other(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Error)]
|
||||
pub enum DiagParsingError {
|
||||
#[error("Failed to parse Message: {0}, data: {1:?}")]
|
||||
MessageParsingError(deku::DekuError, Vec<u8>),
|
||||
#[error("HDLC decapsulation of message failed: {0}, data: {1:?}")]
|
||||
HdlcDecapsulationError(hdlc::HdlcError, Vec<u8>),
|
||||
}
|
||||
|
||||
// this is sorta based on the params qcsuper uses, plus what seems to be used in
|
||||
// https://github.com/fgsect/scat/blob/f1538b397721df3ab8ba12acd26716abcf21f78b/util.py#L47
|
||||
pub const CRC_CCITT_ALG: Algorithm<u16> = Algorithm {
|
||||
poly: 0x1021,
|
||||
init: 0xffff,
|
||||
refin: true,
|
||||
refout: true,
|
||||
width: 16,
|
||||
xorout: 0xffff,
|
||||
check: 0x2189,
|
||||
residue: 0x0000,
|
||||
};
|
||||
|
||||
pub const CRC_CCITT: Crc<u16> = Crc::<u16>::new(&CRC_CCITT_ALG);
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct MessagesContainer {
|
||||
pub data_type: DataType,
|
||||
pub num_messages: u32,
|
||||
#[deku(count = "num_messages")]
|
||||
pub messages: Vec<HdlcEncapsulatedMessage>,
|
||||
}
|
||||
|
||||
impl MessagesContainer {
|
||||
pub fn into_messages(self) -> Vec<Result<Message, DiagParsingError>> {
|
||||
let mut result = Vec::new();
|
||||
for msg in self.messages {
|
||||
for sub_msg in msg.data.split_inclusive(|&b| b == MESSAGE_TERMINATOR) {
|
||||
match hdlc_decapsulate(sub_msg, &CRC_CCITT) {
|
||||
Ok(data) => match Message::from_bytes((&data, 0)) {
|
||||
Ok(((leftover_bytes, _), res)) => {
|
||||
if !leftover_bytes.is_empty() {
|
||||
warn!(
|
||||
"warning: {} leftover bytes when parsing Message",
|
||||
leftover_bytes.len()
|
||||
);
|
||||
}
|
||||
result.push(Ok(res));
|
||||
}
|
||||
Err(e) => result.push(Err(DiagParsingError::MessageParsingError(e, data))),
|
||||
},
|
||||
Err(err) => result.push(Err(DiagParsingError::HdlcDecapsulationError(
|
||||
err,
|
||||
sub_msg.to_vec(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
pub struct HdlcEncapsulatedMessage {
|
||||
pub len: u32,
|
||||
#[deku(count = "len")]
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(id_type = "u8")]
|
||||
pub enum Message {
|
||||
#[deku(id = "16")]
|
||||
Log {
|
||||
pending_msgs: u8,
|
||||
outer_length: u16,
|
||||
inner_length: u16,
|
||||
log_type: u16,
|
||||
timestamp: Timestamp,
|
||||
// pass the log type and log length (inner_length - (sizeof(log_type) + sizeof(timestamp)))
|
||||
#[deku(ctx = "*log_type, inner_length.saturating_sub(12)")]
|
||||
body: LogBody,
|
||||
},
|
||||
|
||||
// kinda unpleasant deku hackery here. deku expects an enum's variant to be
|
||||
// right before its data, but in this case, a status value comes between the
|
||||
// variants and the data. so we need to use deku's context (ctx) feature to
|
||||
// pass those opcodes down to their respective parsers.
|
||||
#[deku(id_pat = "_")]
|
||||
Response {
|
||||
opcode1: u8, // the "id" (from deku's POV) gets parsed into this field
|
||||
opcode2: u8,
|
||||
opcode3: u8,
|
||||
opcode4: u8,
|
||||
subopcode: u32,
|
||||
status: u32,
|
||||
#[deku(ctx = "u32::from_le_bytes([*opcode1, *opcode2, *opcode3, *opcode4]), *subopcode")]
|
||||
payload: ResponsePayload,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "opcode: u32, subopcode: u32", id = "opcode")]
|
||||
pub enum ResponsePayload {
|
||||
#[deku(id = "115")]
|
||||
LogConfig(#[deku(ctx = "subopcode")] LogConfigResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "subopcode: u32", id = "subopcode")]
|
||||
pub enum LogConfigResponse {
|
||||
#[deku(id = "1")]
|
||||
RetrieveIdRanges { log_mask_sizes: [u32; 16] },
|
||||
|
||||
#[deku(id = "3")]
|
||||
SetMask,
|
||||
}
|
||||
|
||||
pub fn build_log_mask_request(
|
||||
log_type: u32,
|
||||
log_mask_bitsize: u32,
|
||||
accepted_log_codes: &[u32],
|
||||
) -> Request {
|
||||
let mut current_byte: u8 = 0;
|
||||
let mut num_bits_written: u8 = 0;
|
||||
let mut log_mask: Vec<u8> = vec![];
|
||||
for i in 0..log_mask_bitsize {
|
||||
let log_code: u32 = (log_type << 12) | i;
|
||||
if accepted_log_codes.contains(&log_code) {
|
||||
current_byte |= 1 << num_bits_written;
|
||||
}
|
||||
num_bits_written += 1;
|
||||
|
||||
if num_bits_written == 8 || i == log_mask_bitsize - 1 {
|
||||
log_mask.push(current_byte);
|
||||
current_byte = 0;
|
||||
num_bits_written = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type,
|
||||
log_mask_bitsize,
|
||||
log_mask,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
// Just about all of these test cases from manually parsing diag packets w/ QCSuper
|
||||
|
||||
#[test]
|
||||
fn test_request_serialization() {
|
||||
let req = Request::LogConfig(LogConfigRequest::RetrieveIdRanges);
|
||||
assert_eq!(req.to_bytes().unwrap(), vec![115, 0, 0, 0, 1, 0, 0, 0]);
|
||||
|
||||
let req = Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type: 0,
|
||||
log_mask_bitsize: 0,
|
||||
log_mask: vec![],
|
||||
});
|
||||
assert_eq!(
|
||||
req.to_bytes().unwrap(),
|
||||
vec![115, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_log_mask_request() {
|
||||
let log_type = 11;
|
||||
let bitsize = 513;
|
||||
let req = build_log_mask_request(
|
||||
log_type,
|
||||
bitsize,
|
||||
&crate::diag_device::LOG_CODES_FOR_RAW_PACKET_LOGGING,
|
||||
);
|
||||
assert_eq!(
|
||||
req,
|
||||
Request::LogConfig(LogConfigRequest::SetMask {
|
||||
log_type,
|
||||
log_mask_bitsize: bitsize,
|
||||
log_mask: vec![
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0xc, 0x30, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_request_container() {
|
||||
let req = RequestContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
use_mdm: false,
|
||||
mdm_field: -1,
|
||||
hdlc_encapsulated_request: vec![1, 2, 3, 4],
|
||||
};
|
||||
assert_eq!(req.to_bytes().unwrap(), vec![32, 0, 0, 0, 1, 2, 3, 4,]);
|
||||
let req = RequestContainer {
|
||||
data_type: DataType::UserSpace,
|
||||
use_mdm: true,
|
||||
mdm_field: -1,
|
||||
hdlc_encapsulated_request: vec![1, 2, 3, 4],
|
||||
};
|
||||
assert_eq!(
|
||||
req.to_bytes().unwrap(),
|
||||
vec![32, 0, 0, 0, 255, 255, 255, 255, 1, 2, 3, 4,]
|
||||
);
|
||||
}
|
||||
|
||||
fn make_container(data_type: DataType, message: HdlcEncapsulatedMessage) -> MessagesContainer {
|
||||
MessagesContainer {
|
||||
data_type,
|
||||
num_messages: 1,
|
||||
messages: vec![message],
|
||||
}
|
||||
}
|
||||
|
||||
// this log is based on one captured on a real device -- if it fails to
|
||||
// serialize or deserialize, that's probably a problem with this mock, not
|
||||
// the DiagReader implementation
|
||||
fn get_test_message(payload: &[u8]) -> (HdlcEncapsulatedMessage, Message) {
|
||||
let length_with_payload = 31 + payload.len() as u16;
|
||||
let message = Message::Log {
|
||||
pending_msgs: 0,
|
||||
outer_length: length_with_payload,
|
||||
inner_length: length_with_payload,
|
||||
log_type: 0xb0c0,
|
||||
timestamp: Timestamp {
|
||||
ts: 72659535985485082,
|
||||
},
|
||||
body: LogBody::LteRrcOtaMessage {
|
||||
ext_header_version: 20,
|
||||
packet: diaglog::rrc::LteRrcOtaPacket::V8 {
|
||||
rrc_rel_maj: 14,
|
||||
rrc_rel_min: 48,
|
||||
bearer_id: 0,
|
||||
phy_cell_id: 160,
|
||||
earfcn: 2050,
|
||||
sfn_subfn: 4057,
|
||||
pdu_num: 5,
|
||||
sib_mask: 0,
|
||||
len: payload.len() as u16,
|
||||
packet: payload.to_vec(),
|
||||
},
|
||||
},
|
||||
};
|
||||
let serialized = message
|
||||
.to_bytes()
|
||||
.expect("failed to serialize test message");
|
||||
let encapsulated_data = hdlc::hdlc_encapsulate(&serialized, &CRC_CCITT);
|
||||
let encapsulated = HdlcEncapsulatedMessage {
|
||||
len: encapsulated_data.len() as u32,
|
||||
data: encapsulated_data,
|
||||
};
|
||||
(encapsulated, message)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_containers_with_multiple_messages() {
|
||||
let (encapsulated1, message1) = get_test_message(&[1]);
|
||||
let (encapsulated2, message2) = get_test_message(&[2]);
|
||||
let mut container = make_container(DataType::UserSpace, encapsulated1);
|
||||
container.messages.push(encapsulated2);
|
||||
container.num_messages += 1;
|
||||
assert_eq!(container.into_messages(), vec![Ok(message1), Ok(message2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_containers_with_concatenated_message() {
|
||||
let (mut encapsulated1, message1) = get_test_message(&[1]);
|
||||
let (encapsulated2, message2) = get_test_message(&[2]);
|
||||
encapsulated1.data.extend(encapsulated2.data);
|
||||
encapsulated1.len += encapsulated2.len;
|
||||
let container = make_container(DataType::UserSpace, encapsulated1);
|
||||
assert_eq!(container.into_messages(), vec![Ok(message1), Ok(message2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handles_parsing_errors() {
|
||||
let (encapsulated1, message1) = get_test_message(&[1]);
|
||||
let bad_message = hdlc::hdlc_encapsulate(&[0x01, 0x02, 0x03, 0x04], &CRC_CCITT);
|
||||
let encapsulated2 = HdlcEncapsulatedMessage {
|
||||
len: bad_message.len() as u32,
|
||||
data: bad_message,
|
||||
};
|
||||
let mut container = make_container(DataType::UserSpace, encapsulated1);
|
||||
container.messages.push(encapsulated2);
|
||||
container.num_messages += 1;
|
||||
let result = container.into_messages();
|
||||
assert_eq!(result[0], Ok(message1));
|
||||
assert!(matches!(
|
||||
result[1],
|
||||
Err(DiagParsingError::MessageParsingError(_, _))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handles_encapsulation_errors() {
|
||||
let (encapsulated1, message1) = get_test_message(&[1]);
|
||||
let bad_encapsulation = HdlcEncapsulatedMessage {
|
||||
len: 4,
|
||||
data: vec![0x01, 0x02, 0x03, 0x04],
|
||||
};
|
||||
let mut container = make_container(DataType::UserSpace, encapsulated1);
|
||||
container.messages.push(bad_encapsulation);
|
||||
container.num_messages += 1;
|
||||
let result = container.into_messages();
|
||||
assert_eq!(result[0], Ok(message1));
|
||||
assert!(matches!(
|
||||
result[1],
|
||||
Err(DiagParsingError::HdlcDecapsulationError(_, _))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_crash_response_opcode_parsing() {
|
||||
// Regression test: Upgrading to deku 0.20 caused incorrect parsing of Response messages.
|
||||
// The issue was that deku 0.20 requires an `id` field for `id_pat = "_"` variants,
|
||||
// but in deku 0.18 the discriminant was NOT consumed from the stream.
|
||||
// This caused a 1-byte offset, making opcode and all subsequent fields misaligned.
|
||||
// Fixed by splitting the opcode into 4 separate u8 fields so the discriminant byte
|
||||
// becomes the first byte of the opcode, matching the old deku 0.18 behavior.
|
||||
let response_msg = b"\x73\x00\x00\x00\x03\x00\x00\x00\x0a\x00\xec\xb0\x8e\x51\x02\x6f\x2a\xc5\x0b\x01\x01\x09\x05\x00\x07\x45\x8e\x14\x7d";
|
||||
|
||||
let ((rest, _), msg) = Message::from_bytes((response_msg, 0)).unwrap();
|
||||
|
||||
// Verify the opcode is correctly parsed as 115 (0x73 in first byte)
|
||||
// In little-endian: [0x73, 0x00, 0x00, 0x00] = 0x00000073 = 115
|
||||
assert!(
|
||||
matches!(
|
||||
msg,
|
||||
Message::Response {
|
||||
opcode1: 0x73,
|
||||
opcode2: 0x00,
|
||||
opcode3: 0x00,
|
||||
opcode4: 0x00,
|
||||
subopcode: 3,
|
||||
status: 2968256522, // [0x0a, 0x00, 0xec, 0xb0] in LE
|
||||
payload: ResponsePayload::LogConfig(LogConfigResponse::SetMask),
|
||||
}
|
||||
),
|
||||
"Unexpected message: {:?}",
|
||||
msg
|
||||
);
|
||||
|
||||
// Verify we consumed the expected number of bytes
|
||||
assert_eq!(rest.len(), 17);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ pub enum DiagDeviceError {
|
||||
ParseMessagesContainerError(deku::DekuError),
|
||||
}
|
||||
|
||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 14] = [
|
||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 12] = [
|
||||
// Layer 2:
|
||||
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
|
||||
// Layer 3:
|
||||
@@ -56,10 +56,8 @@ pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 14] = [
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
|
||||
// User IP traffic:
|
||||
log_codes::LOG_DATA_PROTOCOL_LOGGING_C, // 0x11eb
|
||||
// Statistics
|
||||
log_codes::LOG_LTE_ML1_SERVING_CELL_MEAS_RESP_AND_EVAL, // 0xb17f
|
||||
// Signal strength measurements:
|
||||
log_codes::LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE, // 0xb193
|
||||
log_codes::LOG_LTE_ML1_NEIGHBOR_MEAS, // 0xb180
|
||||
];
|
||||
|
||||
const BUFFER_LEN: usize = 1024 * 1024 * 10;
|
||||
|
||||
@@ -1,10 +1,70 @@
|
||||
use crate::diag::Message;
|
||||
use crate::diag::diaglog::{Timestamp, LogBody, Nas4GMessageDirection};
|
||||
use crate::gsmtap::{GsmtapHeader, GsmtapMessage, GsmtapType, LteNasSubtype, LteRrcSubtype};
|
||||
use crate::diag::*;
|
||||
use crate::gsmtap::*;
|
||||
|
||||
use log::debug;
|
||||
use log::{debug, error};
|
||||
use serde::Serialize;
|
||||
use std::sync::RwLock;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Cached LTE cell information from ML1 measurements.
|
||||
/// Contains signal strength and cell identity information.
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct CellInfo {
|
||||
/// Reference Signal Received Power in dBm (typical range: -140 to -44)
|
||||
pub rsrp_dbm: Option<f32>,
|
||||
/// Reference Signal Received Quality in dB (typical range: -20 to -3)
|
||||
pub rsrq_db: Option<f32>,
|
||||
/// Received Signal Strength Indicator in dBm
|
||||
pub rssi_dbm: Option<f32>,
|
||||
/// Physical Cell ID (0-503)
|
||||
pub pci: Option<u16>,
|
||||
/// E-UTRA Absolute Radio Frequency Channel Number
|
||||
pub earfcn: Option<u32>,
|
||||
}
|
||||
|
||||
/// Global cache for the most recent cell/signal measurement.
|
||||
/// This is populated by LteMl1ServingCellMeas messages and can be used
|
||||
/// to add signal strength to GSMTAP headers and display in the UI.
|
||||
///
|
||||
/// Uses RwLock for consistent multi-field updates. Reads >> writes so this is efficient.
|
||||
static CACHED_CELL_INFO: RwLock<CellInfo> = RwLock::new(CellInfo {
|
||||
rsrp_dbm: None,
|
||||
rsrq_db: None,
|
||||
rssi_dbm: None,
|
||||
pci: None,
|
||||
earfcn: None,
|
||||
});
|
||||
|
||||
/// Get the cached cell information.
|
||||
/// Returns a clone of the current cell info state.
|
||||
pub fn get_cached_cell_info() -> CellInfo {
|
||||
CACHED_CELL_INFO
|
||||
.read()
|
||||
.expect("cell info lock poisoned")
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Get the cached signal strength (RSRP) in dBm as i8 for GSMTAP header compatibility.
|
||||
/// Returns 0 if no measurement has been received yet.
|
||||
pub fn get_cached_signal_dbm() -> i8 {
|
||||
CACHED_CELL_INFO
|
||||
.read()
|
||||
.expect("cell info lock poisoned")
|
||||
.rsrp_dbm
|
||||
.map(|rsrp| rsrp.clamp(-128.0, 127.0) as i8)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Update the cached cell info from a measurement.
|
||||
fn update_cell_info_cache(meas: &LteMl1ServingCellMeasData) {
|
||||
let mut cache = CACHED_CELL_INFO.write().expect("cell info lock poisoned");
|
||||
cache.rsrp_dbm = meas.get_rsrp_dbm();
|
||||
cache.rsrq_db = meas.get_rsrq_db();
|
||||
cache.rssi_dbm = meas.get_rssi_dbm();
|
||||
cache.pci = meas.get_pci();
|
||||
cache.earfcn = meas.get_earfcn();
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GsmtapParserError {
|
||||
#[error("Invalid LteRrcOtaMessage ext header version {0}")]
|
||||
@@ -139,6 +199,8 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
|
||||
header.frame_number = packet.get_sfn();
|
||||
header.subslot = packet.get_subfn();
|
||||
// Apply cached signal strength from ML1 measurements
|
||||
header.signal_dbm = get_cached_signal_dbm();
|
||||
Ok(Some(GsmtapMessage {
|
||||
header,
|
||||
payload: packet.take_payload(),
|
||||
@@ -153,8 +215,24 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
payload: msg,
|
||||
}))
|
||||
}
|
||||
LogBody::LteMl1ServingCellMeas { meas, .. } => {
|
||||
// Update the cell info cache with measurement data
|
||||
update_cell_info_cache(&meas);
|
||||
debug!(
|
||||
"ML1 0xB193 v{}: RSRP={:?}dBm, RSRQ={:?}dB, RSSI={:?}dBm, PCI={:?}, EARFCN={:?}",
|
||||
meas.subpacket_version,
|
||||
meas.get_rsrp_dbm(),
|
||||
meas.get_rsrq_db(),
|
||||
meas.get_rssi_dbm(),
|
||||
meas.get_pci(),
|
||||
meas.get_earfcn()
|
||||
);
|
||||
// Measurement messages don't produce GSMTAP output themselves,
|
||||
// they just update the cell info cache for subsequent messages.
|
||||
Ok(None)
|
||||
}
|
||||
_ => {
|
||||
debug!("gsmtap_sink: ignoring unhandled log type: {value:?}");
|
||||
error!("gsmtap_sink: ignoring unhandled log type: {value:?}");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ pub const LOG_NR_RRC_OTA_MSG_LOG_C: u32 = 0xb821;
|
||||
// These are 4G-related log types.
|
||||
|
||||
pub const LOG_LTE_RRC_OTA_MSG_LOG_C: u32 = 0xb0c0;
|
||||
|
||||
// LTE ML1 (Modem Layer 1) measurement logs - contain signal strength data
|
||||
pub const LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE: u32 = 0xb193;
|
||||
pub const LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C: u32 = 0xb0e2;
|
||||
pub const LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C: u32 = 0xb0e3;
|
||||
pub const LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C: u32 = 0xb0ec;
|
||||
@@ -103,7 +106,3 @@ pub const WCDMA_SIGNALLING_MESSAGE: u32 = 0x412f;
|
||||
pub const LOG_DATA_PROTOCOL_LOGGING_C: u32 = 0x11eb;
|
||||
|
||||
pub const LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C: u32 = 0x713a;
|
||||
|
||||
pub const LOG_LTE_ML1_SERVING_CELL_MEAS_RESP_AND_EVAL: u32 = 0xb17f;
|
||||
pub const LOG_LTE_ML1_SERVING_CELL_MEAS_RESPONSE: u32 = 0xb193;
|
||||
pub const LOG_LTE_ML1_NEIGHBOR_MEAS: u32 = 0xb180;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
//! Parse QMDL files and create a pcap file.
|
||||
//! Creates a plausible IP header and [GSMtap](https://osmocom.org/projects/baseband/wiki/GSMTAP) header and then puts the rest of the data under that for wireshark to parse.
|
||||
use crate::diag::diaglog::Timestamp;
|
||||
use crate::diag::Timestamp;
|
||||
use crate::gsmtap::GsmtapMessage;
|
||||
|
||||
use chrono::prelude::*;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use deku::prelude::*;
|
||||
use rayhunter::{
|
||||
diag::Message,
|
||||
diag::diaglog::{LogBody, rrc::LteRrcOtaPacket, Timestamp},
|
||||
diag::{LogBody, LteRrcOtaPacket, Message, Timestamp},
|
||||
gsmtap_parser,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Build Rayhunter from source for development.
|
||||
# Prerequisites: Rust (rustup) and Node.js (npm).
|
||||
#
|
||||
# Usage: ./scripts/build-dev.sh [build|frontend|check]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
check_dependencies() {
|
||||
local missing=0
|
||||
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo "Error: cargo not found. Install Rust via https://www.rust-lang.org/tools/install"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "Error: npm not found. Install Node.js via https://docs.npmjs.com/downloading-and-installing-node-js-and-npm"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if [ "$missing" -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure the ARM cross-compilation target is installed
|
||||
if ! rustup target list --installed | grep -q "armv7-unknown-linux-musleabihf"; then
|
||||
echo "Installing ARM target (armv7-unknown-linux-musleabihf)..."
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
fi
|
||||
}
|
||||
|
||||
build_frontend() {
|
||||
echo "Building web frontend..."
|
||||
pushd daemon/web > /dev/null
|
||||
npm install
|
||||
npm run build
|
||||
popd > /dev/null
|
||||
}
|
||||
|
||||
build_daemon() {
|
||||
echo "Building daemon..."
|
||||
cargo build-daemon-firmware-devel
|
||||
|
||||
echo "Building rootshell..."
|
||||
cargo build-rootshell-firmware-devel
|
||||
}
|
||||
|
||||
COMMAND="${1:-build}"
|
||||
|
||||
case "$COMMAND" in
|
||||
build)
|
||||
check_dependencies
|
||||
build_frontend
|
||||
build_daemon
|
||||
echo ""
|
||||
echo "Build complete! To install to a device, run:"
|
||||
echo " ./scripts/install-dev.sh <device>"
|
||||
echo ""
|
||||
echo "Replace <device> with your device type (e.g. orbic, tplink)."
|
||||
;;
|
||||
frontend)
|
||||
build_frontend
|
||||
;;
|
||||
check)
|
||||
check_dependencies
|
||||
;;
|
||||
help|--help|-h)
|
||||
echo "Usage: $0 [command]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " build Build frontend, daemon, and rootshell (default)"
|
||||
echo " frontend Build only the web frontend"
|
||||
echo " check Check dependencies only"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $COMMAND"
|
||||
echo "Run '$0 help' for usage."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Install a development build of Rayhunter to a device.
|
||||
# Run ./scripts/build-dev.sh first.
|
||||
#
|
||||
# Usage: ./scripts/install-dev.sh <device> [options...]
|
||||
# Example: ./scripts/install-dev.sh orbic --admin-password mypass
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
cargo run -p installer --bin installer -- "$@"
|
||||
Reference in New Issue
Block a user