mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-29 21:52:06 -07:00
Collect signal strength and timing advances. LTE serving cell measurements (0xb17f) and RACH Timing Advance (0xb062)
This commit is contained in:
committed by
Will Greenberg
parent
68ba2ab625
commit
8f03ad8f4a
@@ -0,0 +1,101 @@
|
||||
use deku::prelude::*;
|
||||
|
||||
// Qualcomm ML1 (physical layer) serving cell measurement log (0xb17f).
|
||||
// Format from SCAT: https://github.com/fgsect/scat/blob/master/src/scat/parsers/qualcomm/diagltelogparser.py
|
||||
// V5 format string (after version byte): '<BHLH2xLLLLLL'
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(ctx = "version: u8", id = "version")]
|
||||
pub enum LteMl1ServingCellMeasPacket {
|
||||
#[deku(id = "4")]
|
||||
V4 {
|
||||
rrc_release: u16,
|
||||
reserved: u16,
|
||||
earfcn: u16,
|
||||
pci_serv_layer: u16,
|
||||
meas_rsrp: u32,
|
||||
avg_rsrp: u32,
|
||||
rsrq: u32,
|
||||
rssi: u32,
|
||||
rxlev: u32,
|
||||
search_threshold: u32,
|
||||
},
|
||||
// V5 expanded earfcn to u32; rrc_release shrunk to u8 with a reserved u16 before earfcn;
|
||||
// 2-byte padding follows pci_serv_layer (SCAT: 2x)
|
||||
#[deku(id_pat = "5..=255")]
|
||||
V5 {
|
||||
rrc_release: u8,
|
||||
reserved: u16,
|
||||
earfcn: u32,
|
||||
#[deku(pad_bytes_after = "2")]
|
||||
pci_serv_layer: u16,
|
||||
meas_rsrp: u32,
|
||||
avg_rsrp: u32,
|
||||
rsrq: u32,
|
||||
rssi: u32,
|
||||
rxlev: u32,
|
||||
search_threshold: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl LteMl1ServingCellMeasPacket {
|
||||
pub fn get_earfcn(&self) -> u32 {
|
||||
match self {
|
||||
Self::V4 { earfcn, .. } => *earfcn as u32,
|
||||
Self::V5 { earfcn, .. } => *earfcn,
|
||||
}
|
||||
}
|
||||
|
||||
// Lower 9 bits are the Physical Cell ID (0–503); upper bits encode serving layer.
|
||||
pub fn get_pci(&self) -> u16 {
|
||||
let raw = match self {
|
||||
Self::V4 { pci_serv_layer, .. } => *pci_serv_layer,
|
||||
Self::V5 { pci_serv_layer, .. } => *pci_serv_layer,
|
||||
};
|
||||
raw & 0x1FF
|
||||
}
|
||||
|
||||
// RSRP lower 12 bits, 1/16 dB steps, -180 dBm base.
|
||||
// Returns whole dBm clamped to i8 for the GSMTAP signal_dbm header field.
|
||||
pub fn get_rsrp_dbm(&self) -> i8 {
|
||||
let raw = match self {
|
||||
Self::V4 { meas_rsrp, .. } => *meas_rsrp,
|
||||
Self::V5 { meas_rsrp, .. } => *meas_rsrp,
|
||||
};
|
||||
let sixteenth_db = -2880_i32 + (raw & 0x0FFF) as i32;
|
||||
(sixteenth_db / 16).clamp(i8::MIN as i32, i8::MAX as i32) as i8
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::diag::{Message, diaglog::LogBody};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lte_ml1_v5_rsrp() {
|
||||
// Probe capture: full diag Message wrapping a 0xb17f log (Version 5, Band 3 / EARFCN 1849).
|
||||
// Constructed as: opcode(1) + pending(1) + outer_len(2) + inner_len(2) +
|
||||
// log_type(2=0xb17f LE) + timestamp(8) + body(40) = 56 bytes total
|
||||
let mut msg_bytes: Vec<u8> = vec![
|
||||
0x10, 0x00, // opcode=Log, pending=0
|
||||
56, 0, 56, 0, // outer_length=56, inner_length=56
|
||||
0x7f, 0xb1, // log_type = 0xb17f (LE)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // timestamp
|
||||
];
|
||||
msg_bytes.extend_from_slice(&[
|
||||
0x05, // version=5
|
||||
0x01, 0x00, 0x00, 0x39, 0x07, 0x00, 0x00, 0x89, 0x00, 0x00, 0x00,
|
||||
0xab, 0xb5, 0x5a, 0x00, 0xab, 0xb5, 0x5a, 0x00,
|
||||
0x1a, 0x69, 0xa4, 0x11, 0x1a, 0x45, 0x0d, 0x00, 0x86, 0xa7, 0xae, 0x02,
|
||||
0x00, 0x00, 0x00, 0x00, 0x80, 0x1c, 0x00, 0x00,
|
||||
]);
|
||||
let msg = Message::from_bytes((&msg_bytes, 0)).expect("Message parse failed").1;
|
||||
if let Message::Log { body: LogBody::LteMl1ServingCellMeas { packet, .. }, .. } = msg {
|
||||
assert_eq!(packet.get_earfcn(), 1849);
|
||||
let rsrp = packet.get_rsrp_dbm();
|
||||
assert!(rsrp <= -44 && rsrp >= -120, "RSRP {rsrp} dBm outside valid LTE range");
|
||||
} else {
|
||||
panic!("unexpected message variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use deku::prelude::*;
|
||||
|
||||
pub mod measurement;
|
||||
pub mod rrc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
@@ -75,6 +76,18 @@ pub enum LogBody {
|
||||
#[deku(count = "hdr_len")]
|
||||
msg: Vec<u8>,
|
||||
},
|
||||
#[deku(id = "0xb17f")]
|
||||
LteMl1ServingCellMeas {
|
||||
version: u8,
|
||||
#[deku(ctx = "*version")]
|
||||
packet: measurement::LteMl1ServingCellMeasPacket,
|
||||
},
|
||||
// Raw bytes; subpacket parsing happens in gsmtap_parser to extract Timing Advance
|
||||
#[deku(id = "0xb062")]
|
||||
LteMacRachResponse {
|
||||
#[deku(count = "hdr_len")]
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DekuRead, DekuWrite)]
|
||||
@@ -228,11 +241,11 @@ pub(crate) mod test {
|
||||
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, 0x0, 0x0, 0x0, 0x0, 0x4, 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,
|
||||
0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0,
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ pub enum DiagDeviceError {
|
||||
ParseMessagesContainerError(deku::DekuError),
|
||||
}
|
||||
|
||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
|
||||
pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 13] = [
|
||||
// Layer 2:
|
||||
log_codes::LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, // 0x5226
|
||||
// Layer 3:
|
||||
@@ -56,6 +56,10 @@ pub const LOG_CODES_FOR_RAW_PACKET_LOGGING: [u32; 11] = [
|
||||
log_codes::LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, // 0xb0ed
|
||||
// User IP traffic:
|
||||
log_codes::LOG_DATA_PROTOCOL_LOGGING_C, // 0x11eb
|
||||
// LTE physical layer serving cell measurements: RSRP, RSRQ, RSSI
|
||||
log_codes::LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C, // 0xb17f
|
||||
// LTE MAC Random Access Channel response: contains Timing Advance
|
||||
log_codes::LOG_LTE_MAC_RACH_RESPONSE_C, // 0xb062
|
||||
];
|
||||
|
||||
const BUFFER_LEN: usize = 1024 * 1024 * 10;
|
||||
|
||||
@@ -153,6 +153,19 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
payload: msg,
|
||||
}))
|
||||
}
|
||||
LogBody::LteMl1ServingCellMeas { packet, .. } => {
|
||||
// frame_number reused for PCI (normally SFN in RRC frames) so all three
|
||||
// serving-cell fields are accessible in Wireshark as gsmtap.* columns.
|
||||
let mut header = GsmtapHeader::new(GsmtapType::QcDiag);
|
||||
header.signal_dbm = packet.get_rsrp_dbm();
|
||||
header.arfcn = packet.get_earfcn().try_into().unwrap_or(0);
|
||||
header.frame_number = packet.get_pci() as u32;
|
||||
Ok(Some(GsmtapMessage {
|
||||
header,
|
||||
payload: vec![],
|
||||
}))
|
||||
}
|
||||
LogBody::LteMacRachResponse { payload } => Ok(parse_rach_response(&payload)),
|
||||
_ => {
|
||||
error!("gsmtap_sink: ignoring unhandled log type: {value:?}");
|
||||
Ok(None)
|
||||
@@ -160,6 +173,108 @@ fn log_to_gsmtap(value: LogBody) -> Result<Option<GsmtapMessage>, GsmtapParserEr
|
||||
}
|
||||
}
|
||||
|
||||
// Parses a 0xb062 RACH response log and reconstructs a 7-byte MAC RAR PDU for Wireshark.
|
||||
// Returns None if the log contains no MSG2 (no Timing Advance was received).
|
||||
fn parse_rach_response(payload: &[u8]) -> Option<GsmtapMessage> {
|
||||
// Outer header: version(u8) + num_subpackets(u8) + reserved(u16)
|
||||
if payload.len() < 4 || payload[0] != 0x01 {
|
||||
return None;
|
||||
}
|
||||
let num_subpackets = payload[1] as usize;
|
||||
let mut offset = 4;
|
||||
|
||||
for _ in 0..num_subpackets {
|
||||
// Subpacket header: id(u8) + version(u8) + size(u16 LE)
|
||||
if offset + 4 > payload.len() {
|
||||
break;
|
||||
}
|
||||
let sp_id = payload[offset];
|
||||
let sp_version = payload[offset + 1];
|
||||
let sp_size = u16::from_le_bytes([payload[offset + 2], payload[offset + 3]]) as usize;
|
||||
if sp_size < 4 {
|
||||
break;
|
||||
}
|
||||
let sp_end = offset + sp_size;
|
||||
if sp_end > payload.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
if sp_id == 0x06 {
|
||||
// RACH Attempt subpacket
|
||||
if let Some(msg) = extract_rach_attempt_gsmtap(&payload[offset + 4..sp_end], sp_version) {
|
||||
return Some(msg);
|
||||
}
|
||||
}
|
||||
|
||||
offset = sp_end;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_rach_attempt_gsmtap(body: &[u8], version: u8) -> Option<GsmtapMessage> {
|
||||
// Per SCAT diagltelogparser.py, RACH Attempt subpacket layouts:
|
||||
// v0x02: hdr=4B, msg1=4B(BBh), msg2=7B(HBHh)
|
||||
// v0x03/0x31: hdr=6B, msg1=4B(BBh), msg2=7B(HBHh)
|
||||
// v0x32: hdr=6B, msg1=7B(BBhHb), msg2=7B(HBHh)
|
||||
// rapid_offset is the header byte holding preamble_index & 0x3F (the RAPID)
|
||||
let (hdr_size, msg1_size, rapid_offset, bitmask_offset) = match version {
|
||||
0x02 => (4usize, 4usize, 0usize, 3usize),
|
||||
0x03 | 0x31 => (6, 4, 2, 5),
|
||||
0x32 => (6, 7, 2, 5),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if body.len() < hdr_size {
|
||||
return None;
|
||||
}
|
||||
let msg_bitmask = body[bitmask_offset];
|
||||
let rapid = body[rapid_offset] & 0x3F;
|
||||
let msg1_present = msg_bitmask & 0x01 != 0;
|
||||
let msg2_present = msg_bitmask & 0x02 != 0;
|
||||
|
||||
if !msg2_present {
|
||||
return None;
|
||||
}
|
||||
|
||||
// MSG2: backoff(u16) + result(u8) + tc_rnti(u16) + ta(u16) = 7 bytes
|
||||
let msg2_offset = hdr_size + if msg1_present { msg1_size } else { 0 };
|
||||
if body.len() < msg2_offset + 7 {
|
||||
return None;
|
||||
}
|
||||
let tc_rnti = u16::from_le_bytes([body[msg2_offset + 3], body[msg2_offset + 4]]);
|
||||
let ta_raw = u16::from_le_bytes([body[msg2_offset + 5], body[msg2_offset + 6]]);
|
||||
// 0xFFFF is a Qualcomm sentinel meaning the RAR was received but TA was not valid
|
||||
if ta_raw == 0xFFFF {
|
||||
return None;
|
||||
}
|
||||
let ta = ta_raw & 0x7FF;
|
||||
|
||||
// Reconstruct 7-byte MAC RAR PDU (3GPP TS 36.321 §6.1.5):
|
||||
// subheader: E=0, T=0, RAPID[5:0]
|
||||
// payload: R(1)|TA[10:3](8) | TA[2:0](3)|ULGrant[19:15](5) | ULGrant[14:7](8) |
|
||||
// ULGrant[6:0](7)|TC-RNTI[15](1) | TC-RNTI[14:7](8) | TC-RNTI[6:0](7)|0(1)
|
||||
//
|
||||
// Use LteMacFramed (0x0f) so Wireshark's mac-lte dissector knows the RNTI type is
|
||||
// RA-RNTI (type=2) and applies the RAR PDU format. The 4-byte framing prefix is:
|
||||
// [RadioType=1(FDD)][Direction=1(DL)][RNTIType=2(RA-RNTI)][0x01=payload-marker]
|
||||
let payload = vec![
|
||||
0x01u8, 0x01, 0x02, 0x01, // framing: FDD, DL, RA-RNTI, payload-marker
|
||||
rapid & 0x3F,
|
||||
((ta >> 3) & 0xFF) as u8,
|
||||
((ta & 0x07) as u8) << 5,
|
||||
0u8, // UL grant zeroed; Wireshark only needs TA and TC-RNTI to decode the RAR
|
||||
((tc_rnti >> 15) & 0x01) as u8,
|
||||
((tc_rnti >> 7) & 0xFF) as u8,
|
||||
((tc_rnti & 0x7F) as u8) << 1,
|
||||
];
|
||||
|
||||
let mut header = GsmtapHeader::new(GsmtapType::LteMacFramed);
|
||||
// Wireshark 4.x does not dispatch GSMTAP type 0x0f to its mac-lte dissector, so
|
||||
// mac-lte.rar.ta is unavailable. TA is also stored in frame_number (gsmtap.frame_nr).
|
||||
header.frame_number = ta as u32;
|
||||
Some(GsmtapMessage { header, payload })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -31,6 +31,10 @@ 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;
|
||||
// Qualcomm ML1 (physical layer) serving cell measurement report: RSRP, RSRQ, RSSI
|
||||
pub const LOG_LTE_ML1_SERVING_CELL_MEAS_AND_EVAL_C: u32 = 0xb17f;
|
||||
// Qualcomm MAC layer RACH response log: contains Timing Advance from Random Access Response
|
||||
pub const LOG_LTE_MAC_RACH_RESPONSE_C: u32 = 0xb062;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user