Skip to content

Commit 7701a1f

Browse files
committed
Add PCAP file parsing support (offline mode)
1 parent 6b5c9b1 commit 7701a1f

File tree

7 files changed

+166
-35
lines changed

7 files changed

+166
-35
lines changed

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ A powerful cross-platform terminal-based application for monitoring and analyzin
1919
### 🌐 **Network Monitoring**
2020
- 🔍 Automatic PTP host discovery on port 319 and 320
2121
- 📡 **Cross-platform packet capture** - Uses libpcap/pcap for promiscuous mode on Linux, macOS, and Windows
22+
- 📄 **PCAP file support** - Read and analyze PTP packets from captured pcap files (offline analysis mode)
2223
- 🌐 **Multicast group membership** - Ensures network interfaces receive multicast PTP traffic
2324
- 🔍 **Full packet analysis** - Records both raw packet data and parsed PTP content
2425
- 🎯 **Smart interface selection** - Automatically filters virtual interfaces while supporting manual override
@@ -28,6 +29,7 @@ A powerful cross-platform terminal-based application for monitoring and analyzin
2829
- 📊 Primary Time Transmitter marked with "PTT" indicator
2930
- 📈 Network statistics and quality metrics
3031
- 🕐 Timing relationship tracking
32+
- ⏸️ **Time reference modes** - Live network uses current system time; pcap mode uses last packet timestamp as reference
3133
- 🌳 **Tree view mode** - Hierarchical display showing transmitter-receiver relationships with proper indentation and PTT (Primary Time Transmitter) indicators
3234
- 🌳 Visual hierarchy mapping of transmitter-receiver relationships
3335
- 🏷️ **VLAN support** - Detects and displays VLAN tags in PTP packets
@@ -45,6 +47,23 @@ A powerful cross-platform terminal-based application for monitoring and analyzin
4547
- 🎨 Color-coded message types (ANNOUNCE, SYNC, DELAY_REQ, PDELAY_REQ, etc.)
4648
- 🌐 **Interface-aware capture** - Tracks which interface each packet was received on
4749

50+
## 📄 PCAP File Analysis
51+
52+
PTP Trace supports offline analysis of PTP traffic from pcap files in offline mode.
53+
54+
### Creating PCAP Files:
55+
```bash
56+
# Capture PTP traffic with tcpdump (Linux/macOS)
57+
sudo tcpdump -i eth0 -w ptp_capture.pcap 'udp port 319 or udp port 320'
58+
59+
# Capture with Wireshark (all platforms)
60+
# Filter: udp.port == 319 or udp.port == 320
61+
# Save as: ptp_capture.pcap
62+
63+
# Analyze the captured file
64+
./target/release/ptp-trace --pcap-file ptp_capture.pcap
65+
```
66+
4867
## Demo
4968

5069
![Demo](demo.gif)
@@ -53,7 +72,7 @@ A powerful cross-platform terminal-based application for monitoring and analyzin
5372

5473
### 📋 Prerequisites
5574
- 🦀 Rust 1.70.0 or later
56-
- 🔧 **Administrator privileges required** - Needed for promiscuous mode packet capture
75+
- 🔧 **Administrator privileges required** - Needed for promiscuous mode packet capture (in live capture mode)
5776
- 🌐 Network interfaces with PTP traffic (ports 319/320)
5877
- 📦 **Platform-specific requirements**:
5978
- **Linux**: libpcap-dev (`sudo apt install libpcap-dev`)
@@ -77,6 +96,9 @@ sudo ./target/release/ptp-trace
7796
### ⚙️ Command Line Options
7897

7998
```bash
99+
# 📄 Analyze packets from pcap file (offline mode, no admin privileges needed)
100+
./target/release/ptp-trace --pcap-file capture.pcap
101+
80102
# 🌐 Monitor specific interface (requires root)
81103
sudo ./target/release/ptp-trace --interface eth0
82104

@@ -97,11 +119,16 @@ sudo ./target/release/ptp-trace --update-interval 500
97119
# 🎨 Use Matrix theme
98120
sudo ./target/release/ptp-trace --theme matrix
99121

122+
# 📄 Analyze pcap file with custom theme and faster updates
123+
./target/release/ptp-trace --pcap-file capture.pcap --theme matrix --update-interval 250
124+
100125
# 🐛 Enable debug mode
101126
sudo ./target/release/ptp-trace --debug
102127

103-
# 🔧 Combine options
128+
# 🔧 Combine options for live monitoring
104129
sudo ./target/release/ptp-trace --interface eth0 --interface eth1 --theme matrix --update-interval 500
130+
131+
# Note: --interface and --pcap-file options are mutually exclusive
105132
```
106133

107134
## 🎮 Controls
@@ -146,10 +173,11 @@ Choose from multiple built-in themes. See the output of `ptp-trace --help` to ge
146173

147174
### 🗺️ **Future Roadmap**
148175
- 📤 **Data export** - JSON, PCAP output formats for raw packet data
149-
- 🔍 **Advanced filtering** - Search and filter capabilities
176+
- 🔍 **Advanced filtering** - Search and filter capabilities for both live and pcap modes
150177
- 📊 **Enhanced analytics** - Statistical analysis of timing data
151178
- 🔧 **Configuration management** - Save/load application settings
152179
- 📦 **Packet inspection tools** - Hex dump viewer for raw packet analysis
180+
- 🎬 **PCAP enhancements** - Playback controls, time range selection, and analysis reports
153181

154182
## 🛠️ Development
155183

src/app.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ impl App {
134134
update_interval: Duration,
135135
debug: bool,
136136
theme_name: crate::themes::ThemeName,
137-
raw_socket_receiver: crate::socket::RawSocketReceiver,
137+
raw_socket_receiver: crate::source::RawSocketReceiver,
138138
) -> Result<Self> {
139139
let ptp_tracker = PtpTracker::new(raw_socket_receiver)?;
140140
let theme = crate::themes::Theme::new(theme_name);
@@ -1187,6 +1187,10 @@ impl App {
11871187
self.modal_packet.as_ref()
11881188
}
11891189

1190+
pub fn get_reference_timestamp(&self) -> Option<std::time::SystemTime> {
1191+
self.ptp_tracker.raw_socket_receiver.get_last_timestamp()
1192+
}
1193+
11901194
fn scroll_modal_up(&mut self) {
11911195
if self.modal_scroll_offset > 0 {
11921196
self.modal_scroll_offset -= 1;

src/main.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod app;
66
mod bounded_vec;
77
mod oui_map;
88
mod ptp;
9-
mod socket;
9+
mod source;
1010
mod themes;
1111
mod types;
1212
mod ui;
@@ -52,9 +52,13 @@ pub struct Cli {
5252
command: Option<Commands>,
5353

5454
/// Network interface(s) to monitor. Can be specified multiple times. If not specified, monitors all interfaces.
55-
#[arg(short, long)]
55+
#[arg(short, long, conflicts_with = "pcap_file")]
5656
interface: Vec<String>,
5757

58+
/// Read packets from a pcap file instead of network interfaces. In pcap mode, timestamps are shown relative to the last packet in the file
59+
#[arg(short = 'f', long, value_name = "FILE", conflicts_with = "interface")]
60+
pcap_file: Option<String>,
61+
5862
/// Update interval in milliseconds
5963
#[arg(short, long, default_value = "1000")]
6064
update_interval: u64,
@@ -93,8 +97,12 @@ async fn main() -> Result<()> {
9397
ThemeName::Default
9498
});
9599

96-
// Create raw socket receiver early to fail fast if network setup is problematic
97-
let raw_socket_receiver = socket::create(&cli.interface).await?;
100+
// Create packet source (either from network interfaces or pcap file)
101+
let raw_socket_receiver = if let Some(pcap_path) = &cli.pcap_file {
102+
source::create_pcap(pcap_path).await?
103+
} else {
104+
source::create_socket(&cli.interface).await?
105+
};
98106

99107
// Initialize the application
100108
let update_interval = Duration::from_millis(cli.update_interval);

src/ptp.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::Result;
22
use std::{
33
collections::HashMap,
44
net::IpAddr,
5-
time::{Duration, Instant},
5+
time::{Duration, Instant, SystemTime},
66
};
77

88
use crate::{
@@ -378,7 +378,7 @@ pub struct PtpHost {
378378
pub ip_addresses: HashMap<IpAddr, Vec<String>>,
379379
pub domain_number: Option<u8>,
380380
pub last_version: Option<PtpVersion>,
381-
pub last_seen: Instant,
381+
pub last_seen: SystemTime,
382382

383383
pub announce_count: u32,
384384
pub sync_count: u32,
@@ -404,7 +404,7 @@ impl PtpHost {
404404
clock_identity,
405405
ip_addresses: HashMap::new(),
406406
domain_number: None,
407-
last_seen: Instant::now(),
407+
last_seen: SystemTime::now(),
408408

409409
announce_count: 0,
410410
sync_count: 0,
@@ -430,7 +430,7 @@ impl PtpHost {
430430
self.domain_number = Some(header.domain_number);
431431
self.last_version = Some(header.version);
432432
self.last_correction_field = Some(header.correction_field);
433-
self.last_seen = Instant::now();
433+
self.last_seen = SystemTime::now();
434434
}
435435

436436
pub fn get_vendor_name(&self) -> Option<&'static str> {
@@ -445,8 +445,9 @@ impl PtpHost {
445445
matches!(self.state, PtpHostState::TimeReceiver(_))
446446
}
447447

448-
pub fn time_since_last_seen(&self) -> Duration {
449-
Instant::now().duration_since(self.last_seen)
448+
pub fn time_since_last_seen(&self, reference_time: Option<SystemTime>) -> Duration {
449+
let reference = reference_time.unwrap_or_else(|| SystemTime::now());
450+
reference.duration_since(self.last_seen).unwrap_or_default()
450451
}
451452

452453
pub fn add_ip_address(&mut self, ip: IpAddr, interface: String) {
@@ -542,15 +543,15 @@ mod tests {
542543
pub struct PtpTracker {
543544
hosts: HashMap<ClockIdentity, PtpHost>,
544545
last_packet: Instant,
545-
raw_socket_receiver: crate::socket::RawSocketReceiver,
546+
pub raw_socket_receiver: crate::source::RawSocketReceiver,
546547
// Track recent sync/follow-up senders per domain for transmitter-receiver correlation
547548
recent_sync_senders: HashMap<u8, Vec<(ClockIdentity, Instant)>>,
548549
// Track interfaces for determining inbound interface of packets
549550
interfaces: Vec<(String, std::net::Ipv4Addr)>,
550551
}
551552

552553
impl PtpTracker {
553-
pub fn new(raw_socket_receiver: crate::socket::RawSocketReceiver) -> Result<Self> {
554+
pub fn new(raw_socket_receiver: crate::source::RawSocketReceiver) -> Result<Self> {
554555
let interfaces = raw_socket_receiver.get_interfaces().to_vec();
555556
Ok(Self {
556557
hosts: HashMap::new(),
@@ -585,7 +586,7 @@ impl PtpTracker {
585586
}
586587
}
587588

588-
async fn handle_raw_packet(&mut self, raw_packet: std::sync::Arc<crate::socket::RawPacket>) {
589+
async fn handle_raw_packet(&mut self, raw_packet: std::sync::Arc<crate::source::RawPacket>) {
589590
let msg = match PtpMessage::try_from(raw_packet.ptp_payload.as_slice()) {
590591
Ok(m) => m,
591592
Err(_) => return, // Invalid message
@@ -610,6 +611,8 @@ impl PtpTracker {
610611

611612
sending_host.total_messages_sent_count += 1;
612613
sending_host.update_from_ptp_header(msg.header());
614+
// Update last_seen with packet timestamp
615+
sending_host.last_seen = raw_packet.timestamp;
613616

614617
match msg {
615618
PtpMessage::Announce(msg) => {
@@ -807,7 +810,7 @@ impl PtpTracker {
807810
}
808811
}
809812

810-
pub fn get_local_ips(&self) -> Vec<std::net::IpAddr> {
813+
pub fn get_local_ips(&self) -> Vec<IpAddr> {
811814
self.interfaces
812815
.iter()
813816
.map(|(_, ip)| std::net::IpAddr::V4(*ip))

src/socket.rs renamed to src/source.rs

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,55 @@ pub struct RawPacket {
3030
pub ptp_payload: Vec<u8>,
3131
}
3232

33+
pub enum PacketSource {
34+
Socket {
35+
receiver: mpsc::UnboundedReceiver<RawPacket>,
36+
interfaces: Vec<(String, Ipv4Addr)>,
37+
_multicast_sockets: Vec<Socket>,
38+
},
39+
Pcap {
40+
packets: Vec<RawPacket>,
41+
current_index: usize,
42+
last_timestamp: Option<SystemTime>,
43+
},
44+
}
45+
3346
pub struct RawSocketReceiver {
34-
pub receiver: mpsc::UnboundedReceiver<RawPacket>,
35-
pub interfaces: Vec<(String, Ipv4Addr)>,
36-
pub _multicast_sockets: Vec<Socket>,
47+
source: PacketSource,
3748
}
3849

3950
impl RawSocketReceiver {
4051
pub fn try_recv(&mut self) -> Option<RawPacket> {
41-
self.receiver.try_recv().ok()
52+
match &mut self.source {
53+
PacketSource::Socket { receiver, .. } => receiver.try_recv().ok(),
54+
PacketSource::Pcap {
55+
packets,
56+
current_index,
57+
..
58+
} => {
59+
if *current_index < packets.len() {
60+
let packet = packets[*current_index].clone();
61+
*current_index += 1;
62+
Some(packet)
63+
} else {
64+
None
65+
}
66+
}
67+
}
4268
}
4369

4470
pub fn get_interfaces(&self) -> &[(String, Ipv4Addr)] {
45-
&self.interfaces
71+
match &self.source {
72+
PacketSource::Socket { interfaces, .. } => interfaces,
73+
PacketSource::Pcap { .. } => &[],
74+
}
75+
}
76+
77+
pub fn get_last_timestamp(&self) -> Option<SystemTime> {
78+
match &self.source {
79+
PacketSource::Socket { .. } => None,
80+
PacketSource::Pcap { last_timestamp, .. } => *last_timestamp,
81+
}
4682
}
4783
}
4884

@@ -352,7 +388,7 @@ async fn capture_on_interface(
352388
Ok(())
353389
}
354390

355-
pub async fn create(ifnames: &[String]) -> Result<RawSocketReceiver> {
391+
pub async fn create_socket(ifnames: &[String]) -> Result<RawSocketReceiver> {
356392
// Get interfaces to monitor
357393
let target_interfaces = if ifnames.is_empty() {
358394
// Default to all available interfaces
@@ -423,8 +459,49 @@ pub async fn create(ifnames: &[String]) -> Result<RawSocketReceiver> {
423459
);
424460

425461
Ok(RawSocketReceiver {
426-
receiver,
427-
interfaces: target_interfaces,
428-
_multicast_sockets: multicast_sockets,
462+
source: PacketSource::Socket {
463+
receiver,
464+
interfaces: target_interfaces,
465+
_multicast_sockets: multicast_sockets,
466+
},
467+
})
468+
}
469+
470+
pub async fn create_pcap(pcap_path: &str) -> Result<RawSocketReceiver> {
471+
use pcap::Capture;
472+
473+
// Open pcap file
474+
let mut cap = Capture::from_file(pcap_path)?;
475+
476+
let mut packets = Vec::new();
477+
let mut last_timestamp: Option<SystemTime> = None;
478+
479+
// Read all packets from pcap file
480+
while let Ok(packet) = cap.next_packet() {
481+
if let Some(raw_packet) = process_ethernet_packet(&packet, "pcap") {
482+
// Track the latest timestamp for reference
483+
if last_timestamp.is_none() || raw_packet.timestamp > last_timestamp.unwrap() {
484+
last_timestamp = Some(raw_packet.timestamp);
485+
}
486+
packets.push(raw_packet);
487+
}
488+
}
489+
490+
println!(
491+
"Loaded {} PTP packets from pcap file: {}",
492+
packets.len(),
493+
pcap_path
494+
);
495+
496+
if let Some(last_ts) = last_timestamp {
497+
println!("Last packet timestamp: {:?}", last_ts);
498+
}
499+
500+
Ok(RawSocketReceiver {
501+
source: PacketSource::Pcap {
502+
packets,
503+
current_index: 0,
504+
last_timestamp,
505+
},
429506
})
430507
}

src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1070,7 +1070,7 @@ impl Display for PtpMessage {
10701070
#[derive(Debug, Clone)]
10711071
pub struct ParsedPacket {
10721072
pub ptp: PtpMessage,
1073-
pub raw: std::sync::Arc<crate::socket::RawPacket>,
1073+
pub raw: std::sync::Arc<crate::source::RawPacket>,
10741074
}
10751075

10761076
#[test]

0 commit comments

Comments
 (0)