Skip to content

Signal strength and timing advance capture#1061

Closed
CGurity wants to merge 4 commits into
EFForg:mainfrom
South-Lighthouse:gsm-timing-advance
Closed

Signal strength and timing advance capture#1061
CGurity wants to merge 4 commits into
EFForg:mainfrom
South-Lighthouse:gsm-timing-advance

Conversation

@CGurity
Copy link
Copy Markdown
Contributor

@CGurity CGurity commented May 31, 2026

Pull Request Checklist

  • The Rayhunter team has recently expressed interest in reviewing a PR for this. (Not sure if recently TBH)
    • If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
  • Added or updated any documentation as needed to support the changes in this PR. No mods needed in documentation
  • Code has been linted and run through cargo fmt.
  • If any new functionality has been added, unit tests were also added.
  • CONTRIBUTING.md has been read.
  • Your pull request is fewer than ~400 lines of code.

You must check one of:

  • No generative AI (including LLMs) tools were used to create this PR.
  • Generative AI was used to create this PR. I certify that I have read and understand the code, and that all comments and descriptions were authored by myself and are not the product of generative AI. Some comments are highly technical, so they were generated by AI and tweaked/validated by me

Relevant for issues #736 #756 and #154

The code allows the capture of signal strength and timing advances and their addition to the PCAP files in a way Wireshark understands, making the analysis of tower geolocation cases possible, especially when combined with the GPS data capture.

The code was tested in my city for a few days to make sure the data makes sense. Results are valid and consistent. I posted a utility here https://github.com/South-Lighthouse/fade-rayhunter-utility-scripts where I drop a PCAP with GPS coordinates, and the script will extract GPS coordinates, timing advances, signal strength, and tower identity to ask opencellid/google/beacondb for the stored location of the actual towers. A couple of examples are shared in the project Mattermost (screenshot below).

image

The relevant disclaimer: I used Claude Code a lot to propose where to look for the right info in the radio captures, debug stuff, the code with utility in the referenced external repo, and the text below with a walkthrough of the changes made. What I can totally vouch for is that I validated the code while authorizing changes, and I tested the resulting code, as you can check in the examples shared over Mattermost.

Technical walkthrough for reviewers

This PR adds collection and PCAP export of two new Qualcomm diag log codes to enable tower geolocation analysis. The goal: given a capture with GPS coordinates, an analyst can verify that a tower is physically located where it claims to be by cross-checking signal strength and timing advance against its known coordinates, among other potential checks.

Log code 0xb17f — LTE serving cell measurements

Qualcomm's physical layer emits this log periodically with RSRP (signal strength), PCI (Physical Cell ID), and EARFCN (frequency channel) for the camped cell. The binary layout was sourced from [SCAT's diagltelogparser.py](https://github.com/fgsect/scat/blob/master/src/scat/parsers/qualcomm/diagltelogparser.py), which is the canonical open-source reference for Qualcomm diag formats. Two struct versions exist in the wild (V4/V5), mainly differing in field sizes — the LteMl1ServingCellMeasPacket enum in diag.rs handles both.

These three values are stored in GSMTAP header fields that happen to be spare for this frame type: signal_dbm → RSRP, arfcn → EARFCN, frame_number → PCI. This makes all three queryable in Wireshark without needing a custom dissector.

Log code 0xb062 — LTE MAC Random Access Response (Timing Advance)

Every time the device connects to a cell, the network sends a Random Access Response containing a Timing Advance value — how much earlier the device needs to transmit so its signals arrive on time at the base station. Since TA is proportional to round-trip propagation time, it is an independent distance estimate (~78 m per unit in LTE).

The parser walks the subpacket structure in gsmtap_parser.rs to extract the TA and TC-RNTI from MSG2, then reconstructs a valid MAC RAR PDU (3GPP TS 36.321 §6.1.5) for inclusion in the PCAP. Qualcomm uses ta = 0xFFFF as a sentinel for "RAR received but TA not valid" — those are dropped. Because Wireshark 4.x doesn't dispatch GSMTAP type 0x0f to its mac-lte dissector, the TA is also stored in frame_number so it's accessible as gsmtap.frame_nr without any plugin.

Copy link
Copy Markdown
Collaborator

@untitaker untitaker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't check the protocol details, as i'm not familar enough with them.

Comment thread lib/src/gsmtap_parser.rs

// 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> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Comment thread lib/src/diag.rs Outdated
pub enum LteMl1ServingCellMeasPacket {
#[deku(id = "4")]
V4 {
rrc_release: u16,
Copy link
Copy Markdown
Collaborator

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_rel is defined as B (for byte) on both v4 and v5 in scat: https://github.com/fgsect/scat/blob/a289d84d81c85612860ef5408c72a0b6306854d0/src/scat/parsers/qualcomm/diagltelogparser.py#L125-L129

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@untitaker
Copy link
Copy Markdown
Collaborator

General comment about testing: If you used scat as a reference implementation, can we also port their parser tests into this codebase?

@CGurity
Copy link
Copy Markdown
Contributor Author

CGurity commented Jun 3, 2026

General comment about testing: If you used scat as a reference implementation, can we also port their parser tests into this codebase?

TBH, mostly because I'm not experienced enough with testing, so I'm a bit scared of making mistakes that would require more time to address than just shipping functional ones made for this use case. Otherwise, I'm happy to have that port included down the road (or if someone more confident with this wants to do it).

@wgreenberg wgreenberg self-requested a review June 3, 2026 18:20
@wgreenberg
Copy link
Copy Markdown
Collaborator

wgreenberg commented Jun 4, 2026

@CGurity thanks for this! you couldn't have known this since i never pushed it, but i had a very similar branch (minus RACH packets) in the works months ago, but left it on the backburner until i finished the compressed QMDL branch. i just rebased it here: https://github.com/EFForg/rayhunter/tree/rssi

you and i ended up with a very similar deku implementation for the serving cell measurement packets, but my branch has two major elements i'd love to see used here:

  1. ported SCAT tests (like what @untitaker requested)
  2. a refactor to help reduce the size of diag.rs

i locally tested your branch, and it works great with the SCAT tests, but throws a ton of warning: 4 leftover bytes when parsing Message warnings when parsing serving cell measurement packets. please let me know if it's too much of an ask, but would you be able to rebase your work on my branch, and add the RACH packet parsing there?

fwiw i agree with @untitaker that i'd greatly prefer we use deku to parse RACH packets, and then serialize them back to binary when writing the gsmtap body

@wgreenberg
Copy link
Copy Markdown
Collaborator

if that feels like too much, i could take the reigns and rebase your branch on mine, then do the deku conversation for RACH packets myself

@CGurity
Copy link
Copy Markdown
Contributor Author

CGurity commented Jun 5, 2026

Hello @wgreenberg ! Great to know that I wasn't too astray from the idea you have in mind. TBH, if you can take care of the rebase and it is not an issue for you, I would prefer that. I'm still moving very carefully with the code, and I'm scared to break something you did. If you are too busy or truly don't want to do it, let me know, and I can set aside time to try my best.

@wgreenberg
Copy link
Copy Markdown
Collaborator

Hello @wgreenberg ! Great to know that I wasn't too astray from the idea you have in mind. TBH, if you can take care of the rebase and it is not an issue for you, I would prefer that. I'm still moving very carefully with the code, and I'm scared to break something you did. If you are too busy or truly don't want to do it, let me know, and I can set aside time to try my best.

no problem at all, i'll do that & open a new PR. thanks for getting the ball rolling on this!

@wgreenberg wgreenberg closed this Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants