diff --git a/daemon/src/gps.rs b/daemon/src/gps.rs index d25c7a9..2d09011 100644 --- a/daemon/src/gps.rs +++ b/daemon/src/gps.rs @@ -14,10 +14,8 @@ pub struct GpsData { pub timestamp: String, } -/// A single GPS fix recorded in the sidecar file alongside a QMDL recording. #[derive(Serialize, Deserialize)] pub struct GpsRecord { - /// Unix timestamp (seconds) of when this fix was received by the server. pub unix_ts: u32, pub lat: f64, pub lon: f64, diff --git a/daemon/src/pcap.rs b/daemon/src/pcap.rs index e0b6809..46417ff 100644 --- a/daemon/src/pcap.rs +++ b/daemon/src/pcap.rs @@ -10,15 +10,12 @@ use axum::response::{IntoResponse, Response}; use log::error; use rayhunter::diag::DataType; use rayhunter::gsmtap_parser; -use rayhunter::pcap::{GsmtapPcapWriter, KismetGpsPoint}; +use rayhunter::pcap::{GpsPoint, GsmtapPcapWriter}; use rayhunter::qmdl::QmdlReader; use std::sync::Arc; use tokio::io::{AsyncRead, AsyncWrite, duplex}; use tokio_util::io::ReaderStream; -// Streams a pcap file chunk-by-chunk to the client by reading the QMDL data -// written so far. This is done by spawning a thread which streams chunks of -// pcap data to a channel that's piped to the client. #[cfg_attr(feature = "apidocs", utoipa::path( get, path = "/api/pcap/{name}", @@ -72,42 +69,33 @@ pub async fn get_pcap( Ok((headers, body).into_response()) } -/// Loads GPS records for a recording entry. -/// -/// - `gps_mode == 0`: returns empty vec (no GPS) -/// - `gps_mode == 1`: returns a single synthetic record with `unix_ts = 0` (fixed coordinates) -/// - `gps_mode == 2`: loads per-fix records from the GPS sidecar file pub(crate) async fn load_gps_records_for_entry( state: &Arc, entry_index: usize, ) -> Vec { - if state.config.gps_mode == 0 { - return vec![]; + // Always try the per-session sidecar first — it reflects what was actually + // recorded regardless of what the current gps_mode config is. + { + let qmdl_store = state.qmdl_store_lock.read().await; + if let Ok(file) = qmdl_store.open_entry_gps(entry_index).await { + let records = load_gps_records(file).await; + if !records.is_empty() { + return records; + } + } } + // Sidecar missing or empty — fall back to current config. if state.config.gps_mode == 1 { let guard = state.gps_state.read().await; return guard .as_ref() - .map(|g| { - vec![GpsRecord { - unix_ts: 0, // 0 signals fixed/synthetic to the Kismet option builder - lat: g.latitude, - lon: g.longitude, - }] - }) + .map(|g| vec![GpsRecord { unix_ts: 0, lat: g.latitude, lon: g.longitude }]) .unwrap_or_default(); } - // gps_mode == 2: load from sidecar - let qmdl_store = state.qmdl_store_lock.read().await; - match qmdl_store.open_entry_gps(entry_index).await { - Ok(file) => load_gps_records(file).await, - Err(_) => vec![], - } + vec![] } -/// Returns the GPS fix from `records` whose `unix_ts` is closest to `packet_unix_ts`. -/// Returns `None` if `records` is empty. -fn find_nearest_gps(records: &[GpsRecord], packet_unix_ts: u32) -> Option { +fn find_nearest_gps(records: &[GpsRecord], packet_unix_ts: u32) -> Option { if records.is_empty() { return None; } @@ -117,19 +105,10 @@ fn find_nearest_gps(records: &[GpsRecord], packet_unix_ts: u32) -> Option= records.len() { &records[records.len() - 1] } else { - let before = &records[idx - 1]; - let after = &records[idx]; - if packet_unix_ts - before.unix_ts <= after.unix_ts - packet_unix_ts { - before - } else { - after - } + let (before, after) = (&records[idx - 1], &records[idx]); + if packet_unix_ts - before.unix_ts <= after.unix_ts - packet_unix_ts { before } else { after } }; - Some(KismetGpsPoint { - latitude: record.lat, - longitude: record.lon, - timestamp_unix_secs: record.unix_ts, - }) + Some(GpsPoint { latitude: record.lat, longitude: record.lon, unix_ts: record.unix_ts }) } pub async fn generate_pcap_data( diff --git a/daemon/web/src/lib/components/ConfigForm.svelte b/daemon/web/src/lib/components/ConfigForm.svelte index 361945e..284df90 100644 --- a/daemon/web/src/lib/components/ConfigForm.svelte +++ b/daemon/web/src/lib/components/ConfigForm.svelte @@ -783,24 +783,16 @@

GPS Settings

-
- -

{#if config.gps_mode === 2} - POST latitude, longitude, and timestamp to /api/gps from - any device on the network. + POST latitude, longitude, and timestamp to /api/gps from any device on the network. {:else if config.gps_mode === 1} GPS coordinates are fixed to the values below. {:else} @@ -808,47 +800,19 @@ {/if}

- {#if config.gps_mode === 1}
- - + +

Decimal degrees, -90 to 90

-
- - + +

Decimal degrees, -180 to 180

{/if} diff --git a/daemon/web/src/routes/+page.svelte b/daemon/web/src/routes/+page.svelte index ede0eba..0324db4 100644 --- a/daemon/web/src/routes/+page.svelte +++ b/daemon/web/src/routes/+page.svelte @@ -291,46 +291,34 @@
{#if gps_mode !== 0} -
- - - GPS Status - - {#if gps_data} - - - - - - - - - - - - - - - -
Latitude{gps_data.latitude.toFixed(6)}
Longitude{gps_data.longitude.toFixed(6)}
GPS Timestamp{gps_data.timestamp}
- {:else} - Awaiting GPS data... - {/if} -
+
+ + + GPS Status + + {#if gps_data} + + + + + + + + + + + + + + + +
Latitude{gps_data.latitude.toFixed(6)}
Longitude{gps_data.longitude.toFixed(6)}
GPS Timestamp{gps_data.timestamp}
+ {:else} + Awaiting GPS data... + {/if} +
{/if}
diff --git a/lib/src/pcap.rs b/lib/src/pcap.rs index da62434..497ede0 100644 --- a/lib/src/pcap.rs +++ b/lib/src/pcap.rs @@ -6,8 +6,7 @@ use crate::gsmtap::GsmtapMessage; use chrono::prelude::*; use deku::prelude::*; use pcap_file_tokio::pcapng::PcapNgWriter; -use pcap_file_tokio::pcapng::RawBlock; -use pcap_file_tokio::pcapng::blocks::enhanced_packet::EnhancedPacketBlock; +use pcap_file_tokio::pcapng::blocks::enhanced_packet::{EnhancedPacketBlock, EnhancedPacketOption}; use pcap_file_tokio::pcapng::blocks::interface_description::InterfaceDescriptionBlock; use pcap_file_tokio::pcapng::blocks::section_header::{SectionHeaderBlock, SectionHeaderOption}; use pcap_file_tokio::{Endianness, PcapError}; @@ -27,87 +26,11 @@ pub enum GsmtapPcapError { Deku(#[from] DekuError), } -/// A GPS fix to embed in each PCAP packet as a Kismet-compatible custom option. -/// -/// Set `timestamp_unix_secs = 0` to signal a fixed/synthetic coordinate (no real GPS time). -pub struct KismetGpsPoint { +pub struct GpsPoint { pub latitude: f64, pub longitude: f64, - pub timestamp_unix_secs: u32, -} - -// Block type constant for Enhanced Packet Block (pcapng spec §4.3) -const ENHANCED_PACKET_BLOCK: u32 = 0x00000006; - -/// Serialises a Kismet GPS custom option into a byte buffer suitable for -/// appending directly to an EPB body. -/// -/// Wire layout (section is big-endian; GPS custom payload is little-endian per -/// Kismet convention): -/// -/// option_code u16 BE = 2989 (custom binary, non-copyable) -/// option_len u16 BE = 24 (PEN 4 + payload 20) -/// pen u32 BE = 55922 (Kismet IANA PEN) -/// --- custom payload (20 bytes, all fields little-endian) --- -/// magic u8 = 0x47 -/// version u8 = 0x01 -/// fields_len u16 LE = 16 (bitmask + lon + lat + ts) -/// bitmask u32 LE = 0x26 (lon=0x2, lat=0x4, gps_time=0x20) -/// longitude i32 LE fixed37 (degrees × 1e7) -/// latitude i32 LE fixed37 (degrees × 1e7) -/// gps_time u32 LE unix seconds (0 = fixed/unknown) -/// --- end-of-options marker --- -/// end_code u16 BE = 0 -/// end_len u16 BE = 0 -fn build_gps_option_bytes(gps: &KismetGpsPoint) -> Vec { - // --- opt_comment (code 1): human-readable GPS for Wireshark --- - // Format chosen to match what Kismet writes so tools see a consistent label. - let comment = if gps.timestamp_unix_secs == 0 { - format!("GPS fixed lat={:.7} lon={:.7}", gps.latitude, gps.longitude) - } else { - format!("GPS lat={:.7} lon={:.7} ts={}", gps.latitude, gps.longitude, gps.timestamp_unix_secs) - }; - let comment_bytes = comment.as_bytes(); - let comment_pad = (4 - (comment_bytes.len() % 4)) % 4; - - // --- Kismet GPS custom option (code 2989) --- - let lon_fixed: i32 = (gps.longitude * 1e7) as i32; - let lat_fixed: i32 = (gps.latitude * 1e7) as i32; - - // Custom payload: 20 bytes, all little-endian (Kismet convention) - let fields_len: u16 = 16; // bitmask(4) + lon(4) + lat(4) + ts(4) - let bitmask: u32 = 0x2 | 0x4 | 0x20; // lon | lat | gps_time - let mut payload = Vec::::with_capacity(20); - payload.push(0x47); // magic - payload.push(0x01); // version - payload.extend_from_slice(&fields_len.to_le_bytes()); - payload.extend_from_slice(&bitmask.to_le_bytes()); - payload.extend_from_slice(&lon_fixed.to_le_bytes()); - payload.extend_from_slice(&lat_fixed.to_le_bytes()); - payload.extend_from_slice(&gps.timestamp_unix_secs.to_le_bytes()); - // payload is exactly 20 bytes, already 4-byte aligned - - // option_len = PEN (4) + payload (20) = 24, also 4-byte aligned - let gps_opt_len: u16 = 24; - - let mut out = Vec::new(); - - // opt_comment header + value + padding (big-endian section) - out.extend_from_slice(&1u16.to_be_bytes()); // option_code = 1 - out.extend_from_slice(&(comment_bytes.len() as u16).to_be_bytes()); // option_len - out.extend_from_slice(comment_bytes); - out.extend_from_slice(&[0u8; 3][..comment_pad]); // padding to 4-byte boundary - - // Kismet GPS option header + PEN + custom payload (big-endian section, LE payload) - out.extend_from_slice(&2989u16.to_be_bytes()); // option_code - out.extend_from_slice(&gps_opt_len.to_be_bytes()); - out.extend_from_slice(&55922u32.to_be_bytes()); // PEN - out.extend_from_slice(&payload); - - // end-of-options marker (big-endian) - out.extend_from_slice(&0u16.to_be_bytes()); - out.extend_from_slice(&0u16.to_be_bytes()); - out + /// Unix timestamp of the GPS fix. 0 means fixed/synthetic (no real GPS time). + pub unix_ts: u32, } pub struct GsmtapPcapWriter @@ -186,7 +109,7 @@ where &mut self, msg: GsmtapMessage, timestamp: Timestamp, - gps: Option<&KismetGpsPoint>, + gps: Option<&GpsPoint>, ) -> Result<(), GsmtapPcapError> { let duration = timestamp .to_datetime() @@ -223,56 +146,23 @@ where data.extend(&udp_header.to_bytes()?); data.extend(&msg_bytes); - match gps { - None => { - // Fast path: delegate to the library's standard EPB writer. - let packet = EnhancedPacketBlock { - interface_id: 0, - timestamp: duration, - original_len: data.len() as u32, - data: Cow::Owned(data), - options: vec![], - }; - self.writer.write_pcapng_block(packet).await?; - } - Some(gps_point) => { - // GPS path: build a raw EPB body so we can append the Kismet - // custom option directly. The pcap-file-tokio crate does not - // expose the inner option types publicly, so we must write the - // option bytes manually and wrap them in a RawBlock. - // - // All standard pcapng multi-byte fields are big-endian (section - // header declares Endianness::Big); the GPS custom payload uses - // little-endian per the Kismet convention. - let pad_len = (4 - (data.len() % 4)) % 4; - let ts_nanos = duration.as_nanos(); - let ts_high = (ts_nanos >> 32) as u32; - let ts_low = (ts_nanos & 0xFFFFFFFF) as u32; - - let gps_bytes = build_gps_option_bytes(gps_point); - - // Body = EPB fixed header (20 B) + data + padding + gps options - let body_len = 20 + data.len() + pad_len + gps_bytes.len(); - let mut body = Vec::::with_capacity(body_len); - body.extend_from_slice(&0u32.to_be_bytes()); // interface_id - body.extend_from_slice(&ts_high.to_be_bytes()); // timestamp high - body.extend_from_slice(&ts_low.to_be_bytes()); // timestamp low - body.extend_from_slice(&(data.len() as u32).to_be_bytes()); // captured_len - body.extend_from_slice(&(data.len() as u32).to_be_bytes()); // original_len - body.extend_from_slice(&data); - body.extend_from_slice(&[0u8; 3][..pad_len]); // padding - body.extend_from_slice(&gps_bytes); - - let block_total_len = (12 + body.len()) as u32; - let raw = RawBlock { - type_: ENHANCED_PACKET_BLOCK, - initial_len: block_total_len, - body: Cow::Owned(body), - trailer_len: block_total_len, - }; - self.writer.write_raw_block(&raw).await?; - } + let mut options = vec![]; + if let Some(p) = gps { + let comment = if p.unix_ts == 0 { + format!("GPS fixed lat={:.7} lon={:.7}", p.latitude, p.longitude) + } else { + format!("GPS lat={:.7} lon={:.7} ts={}", p.latitude, p.longitude, p.unix_ts) + }; + options.push(EnhancedPacketOption::Comment(Cow::Owned(comment))); } + let packet = EnhancedPacketBlock { + interface_id: 0, + timestamp: duration, + original_len: data.len() as u32, + data: Cow::Owned(data), + options, + }; + self.writer.write_pcapng_block(packet).await?; self.ip_id = self.ip_id.wrapping_add(1); Ok(())