Kismet GPS option section per packet dcriped, refactoring to reduce loc

This commit is contained in:
Carlos Guerra
2026-03-29 16:22:07 +02:00
committed by Will Greenberg
parent adb316e2d7
commit 66f0c2a336
5 changed files with 78 additions and 259 deletions
+21 -131
View File
@@ -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<u8> {
// --- 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::<u8>::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<T>
@@ -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::<u8>::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(())