-
Notifications
You must be signed in to change notification settings - Fork 431
Signal strength and timing advance capture #1061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
5bc049a
9873b2c
375efff
32d67ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| use crate::diag::*; | ||
| use crate::gsmtap::*; | ||
| use crate::gsmtap::{GsmtapType, *}; | ||
|
|
||
| use log::error; | ||
| use thiserror::Error; | ||
|
|
@@ -152,9 +152,179 @@ 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) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 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> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally this would be expressible in deku but I understand that it's fairly finnicky to do so. Still, I would suggest to make this function and the one it calls more obviously panic-free. You have a lot of places where bounds checks are done entirely separately from access. For example, instead of doing this: if payload.len() < i + 4 {
return None;
}
let x = payload[i];
let y = payload[i + 3];you may as well write: let x = payload.get(i)?;
let y = payload.get(i + 3)?;...which should be optimized to the same exact thing. currently when rayhunter panics, the entire thing just shuts down. we could make it more resilient at a higher level, but so far I think we've done well with avoiding panics in the parser. |
||
| // 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::parse_rach_response; | ||
| use crate::gsmtap::GsmtapType; | ||
|
|
||
| // Builds a minimal 0xb062 payload: outer header + one RACH Attempt subpacket (version 0x03). | ||
| // v0x03 body layout: hdr=6B [_, _, rapid, _, _, bitmask], then MSG2=7B [backoff(2), result(1), tc_rnti(2), ta(2)] | ||
| fn make_rach_v03_payload(ta_raw: u16, bitmask: u8) -> Vec<u8> { | ||
| let rapid: u8 = 43; | ||
| let tc_rnti: u16 = 0x1234; | ||
| let [ta_lo, ta_hi] = ta_raw.to_le_bytes(); | ||
| let [rnti_lo, rnti_hi] = tc_rnti.to_le_bytes(); | ||
| // sp_size covers the 4-byte subpacket header + 6-byte body header + 7-byte MSG2 = 17 | ||
| vec![ | ||
| 0x01, 0x01, 0x00, 0x00, // outer: version=1, num_subpackets=1, reserved | ||
| 0x06, 0x03, 17, 0x00, // subpacket: id=0x06, version=0x03, size=17 LE | ||
| 0x00, 0x00, rapid, 0x00, 0x00, bitmask, // body header (6 bytes) | ||
| 0x00, 0x00, 0x01, rnti_lo, rnti_hi, ta_lo, ta_hi, // MSG2 (7 bytes) | ||
| ] | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_rach_response_valid_ta() { | ||
| let payload = make_rach_v03_payload(42, 0x02); // 0x02 = msg2 present, msg1 absent | ||
| let msg = parse_rach_response(&payload).expect("expected a GsmtapMessage for valid TA"); | ||
| assert_eq!(msg.header.gsmtap_type, GsmtapType::LteMacFramed); | ||
| // TA stored in frame_number for Wireshark compatibility (gsmtap.frame_nr) | ||
| assert_eq!(msg.header.frame_number, 42); | ||
| // MAC RAR PDU: 4-byte framing prefix + 7-byte RAR PDU = 11 bytes | ||
| assert_eq!(msg.payload.len(), 11); | ||
| // Verify TA encoding in RAR PDU bytes 5–6 (TA[10:3] and TA[2:0]) | ||
| // ta=42: ta>>3=5 in byte[5], (ta&7)<<5 = 2<<5 = 0x40 in byte[6] | ||
| assert_eq!(msg.payload[5], 5); | ||
| assert_eq!(msg.payload[6], 0x40); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_rach_response_ffff_sentinel_returns_none() { | ||
| // 0xFFFF means RAR was received but TA was not valid; must be dropped | ||
| let payload = make_rach_v03_payload(0xFFFF, 0x02); | ||
| assert!(parse_rach_response(&payload).is_none()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_rach_response_no_msg2_returns_none() { | ||
| // bitmask=0x01 means only MSG1 present; no TA available | ||
| let payload = make_rach_v03_payload(42, 0x01); | ||
| assert!(parse_rach_response(&payload).is_none()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem right,
rrc_relis defined asB(for byte) on both v4 and v5 in scat: https://github.com/fgsect/scat/blob/a289d84d81c85612860ef5408c72a0b6306854d0/src/scat/parsers/qualcomm/diagltelogparser.py#L125-L129There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed