Files
dtsstreaming/pcap_analyzer.py

489 lines
20 KiB
Python

#!/usr/bin/env python3
"""
PCAP Analyzer for IRIG106 Chapter 10 and IEEE1588 PTP frames
Analyzes ethernet traffic with Chapter 10 streaming data and PTP frames
"""
from datetime import datetime
from typing import Dict, List, Optional, Tuple
import argparse
import statistics
from chapter10_packet import Chapter10Packet
from ptp_packet import PTPPacket
from tmats_packet import TMATSPacket, TMATSAssembler
try:
import scapy.all as scapy
from scapy.layers.inet import IP, UDP
from scapy.layers.l2 import Ether
except ImportError:
print("Error: scapy library not found. Install with: pip install scapy")
exit(1)
try:
import pandas as pd
except ImportError:
print("Error: pandas library not found. Install with: pip install pandas")
exit(1)
try:
import numpy as np
except ImportError:
print("Error: numpy library not found. Install with: pip install numpy")
exit(1)
class PcapAnalyzer:
"""Main analyzer class for PCAP files"""
def __init__(self, pcap_file: str):
self.pcap_file = pcap_file
self.ch10_packets: List[Chapter10Packet] = []
self.ptp_packets: List[PTPPacket] = []
self.tmats_assembler = TMATSAssembler()
def analyze(self):
"""Analyze the PCAP file"""
print(f"Analyzing PCAP file: {self.pcap_file}")
try:
packets = scapy.rdpcap(self.pcap_file)
except Exception as e:
print(f"Error reading PCAP file: {e}")
return
print(f"Total packets: {len(packets)}")
for i, packet in enumerate(packets):
if i % 1000 == 0:
print(f"Processing packet {i}...")
self._process_packet(packet, i)
print(f"Found {len(self.ch10_packets)} Chapter 10 packets")
print(f"Found {len(self.ptp_packets)} PTP packets")
print(f"Found {self.tmats_assembler.get_frame_count()} TMATS frames")
def _process_packet(self, packet, packet_index):
"""Process individual packet"""
if not packet.haslayer(IP) or not packet.haslayer(UDP):
return
udp_layer = packet[UDP]
src_port = udp_layer.sport
dst_port = udp_layer.dport
payload = bytes(udp_layer.payload)
original_frame_num = packet_index + 1 # Frame numbers are 1-based
# Check for PTP packets (port 319 or 320)
if src_port in [319, 320] or dst_port in [319, 320]:
ptp_packet = PTPPacket(packet)
if ptp_packet.ptp_header:
self.ptp_packets.append(ptp_packet)
# Check for potential Chapter 10 packets (need at least 28 bytes for header + prefix)
if len(payload) >= 28:
ch10_packet = Chapter10Packet(packet, original_frame_num)
if ch10_packet.ch10_header:
self.ch10_packets.append(ch10_packet)
# Check for TMATS packets (can be smaller, includes continuation frames)
if len(payload) >= 12:
tmats_packet = TMATSPacket(packet, original_frame_num)
if tmats_packet.is_tmats:
self.tmats_assembler.add_frame(tmats_packet)
def display_ch10_summary(self):
"""Display Chapter 10 summary statistics"""
if not self.ch10_packets:
print("No Chapter 10 packets found")
return
print("\n" + "="*80)
print("CHAPTER 10 SUMMARY")
print("="*80)
# Basic counts
total_packets = len(self.ch10_packets)
print(f"Total Chapter 10 packets: {total_packets}")
# Time span
if total_packets > 0:
start_time = min(pkt.timestamp for pkt in self.ch10_packets)
end_time = max(pkt.timestamp for pkt in self.ch10_packets)
duration = end_time - start_time
print(f"Time span: {duration:.3f} seconds")
print(f"Start time: {datetime.fromtimestamp(start_time).strftime('%H:%M:%S.%f')[:-3]}")
print(f"End time: {datetime.fromtimestamp(end_time).strftime('%H:%M:%S.%f')[:-3]}")
# Channel distribution
channels = {}
data_types = {}
for pkt in self.ch10_packets:
if pkt.ch10_header is not None:
ch_id = pkt.ch10_header['channel_id']
data_type = pkt.ch10_header['data_type']
channels[ch_id] = channels.get(ch_id, 0) + 1
data_types[data_type] = data_types.get(data_type, 0) + 1
print(f"\nChannel distribution:")
for ch_id in sorted(channels.keys()):
count = channels[ch_id]
percentage = (count / total_packets) * 100
print(f" Channel {ch_id}: {count} packets ({percentage:.1f}%)")
print(f"\nData type distribution:")
for data_type in sorted(data_types.keys()):
count = data_types[data_type]
percentage = (count / total_packets) * 100
print(f" Type {data_type}: {count} packets ({percentage:.1f}%)")
# Size statistics
sizes = [pkt.packet_size for pkt in self.ch10_packets]
data_lengths = [pkt.ch10_header['data_length'] for pkt in self.ch10_packets if pkt.ch10_header is not None]
print(f"\nPacket size statistics:")
print(f" Average: {statistics.mean(sizes):.1f} bytes")
print(f" Min: {min(sizes)} bytes")
print(f" Max: {max(sizes)} bytes")
print(f" Total data: {sum(data_lengths):,} bytes")
# Rate calculations
if duration > 0:
packet_rate = total_packets / duration
data_rate = sum(data_lengths) / duration
print(f"\nRate statistics:")
print(f" Packet rate: {packet_rate:.1f} packets/sec")
print(f" Data rate: {data_rate/1024:.1f} KB/sec")
def display_ch10_table(self):
"""Display Chapter 10 packets in table format"""
if not self.ch10_packets:
print("No Chapter 10 packets found")
return
print("\n" + "="*120)
print("CHAPTER 10 PACKET ANALYSIS")
print("="*120)
# Create DataFrame for better table display
data = []
for i, pkt in enumerate(self.ch10_packets):
if pkt.ch10_header is not None:
header = pkt.ch10_header
data.append({
'Packet#': i+1,
'Timestamp': datetime.fromtimestamp(pkt.timestamp).strftime('%H:%M:%S.%f')[:-3],
'Src IP': pkt.src_ip,
'Dst IP': pkt.dst_ip,
'Src Port': pkt.src_port,
'Dst Port': pkt.dst_port,
'Channel ID': header['channel_id'],
'Seq Num': header['sequence_number'],
'Data Type': header['data_type'],
'Pkt Length': header['packet_length'],
'Data Length': header['data_length'],
'Flags': header['packet_flags'],
'Size': pkt.packet_size
})
df = pd.DataFrame(data)
print(df.to_string(index=False))
def display_ptp_summary(self):
"""Display PTP summary statistics"""
if not self.ptp_packets:
print("No PTP packets found")
return
print("\n" + "="*80)
print("IEEE1588 PTP SUMMARY")
print("="*80)
# Basic counts
total_packets = len(self.ptp_packets)
print(f"Total PTP packets: {total_packets}")
# Time span
if total_packets > 0:
start_time = min(pkt.timestamp for pkt in self.ptp_packets)
end_time = max(pkt.timestamp for pkt in self.ptp_packets)
duration = end_time - start_time
print(f"Time span: {duration:.3f} seconds")
print(f"Start time: {datetime.fromtimestamp(start_time).strftime('%H:%M:%S.%f')[:-3]}")
print(f"End time: {datetime.fromtimestamp(end_time).strftime('%H:%M:%S.%f')[:-3]}")
# Message type distribution
msg_types = {}
domains = {}
sources = {}
for pkt in self.ptp_packets:
if pkt.ptp_header is not None:
msg_type = pkt.ptp_header['message_type']
domain = pkt.ptp_header['domain_number']
source = pkt.src_ip
msg_types[msg_type] = msg_types.get(msg_type, 0) + 1
domains[domain] = domains.get(domain, 0) + 1
sources[source] = sources.get(source, 0) + 1
print(f"\nMessage type distribution:")
for msg_type in sorted(msg_types.keys()):
count = msg_types[msg_type]
percentage = (count / total_packets) * 100
print(f" {msg_type}: {count} packets ({percentage:.1f}%)")
print(f"\nDomain distribution:")
for domain in sorted(domains.keys()):
count = domains[domain]
percentage = (count / total_packets) * 100
print(f" Domain {domain}: {count} packets ({percentage:.1f}%)")
print(f"\nSource IP distribution:")
for source in sorted(sources.keys()):
count = sources[source]
percentage = (count / total_packets) * 100
print(f" {source}: {count} packets ({percentage:.1f}%)")
# Rate calculations
if duration > 0:
packet_rate = total_packets / duration
print(f"\nRate statistics:")
print(f" Overall PTP rate: {packet_rate:.1f} packets/sec")
# Sync message rate
sync_count = msg_types.get('Sync', 0)
if sync_count > 0:
sync_rate = sync_count / duration
print(f" Sync message rate: {sync_rate:.1f} packets/sec")
def display_ptp_table(self):
"""Display PTP packets in table format"""
if not self.ptp_packets:
print("No PTP packets found")
return
print("\n" + "="*100)
print("IEEE1588 PTP PACKET ANALYSIS")
print("="*100)
data = []
for i, pkt in enumerate(self.ptp_packets):
if pkt.ptp_header is not None:
header = pkt.ptp_header
data.append({
'Packet#': i+1,
'Timestamp': datetime.fromtimestamp(pkt.timestamp).strftime('%H:%M:%S.%f')[:-3],
'Src IP': pkt.src_ip,
'Dst IP': pkt.dst_ip,
'Message Type': header['message_type'],
'Domain': header['domain_number'],
'Sequence ID': header['sequence_id'],
'Flags': header['flags'],
'Correction': header['correction_field'],
'Interval': header['log_message_interval']
})
df = pd.DataFrame(data)
print(df.to_string(index=False))
def statistical_analysis(self):
"""Perform statistical analysis for intermittent issue detection"""
print("\n" + "="*80)
print("STATISTICAL ANALYSIS")
print("="*80)
if self.ch10_packets:
self._analyze_ch10_statistics()
if self.ptp_packets:
self._analyze_ptp_statistics()
def _analyze_ch10_statistics(self):
"""Analyze Chapter 10 packet statistics"""
print("\nChapter 10 Statistics:")
print("-" * 40)
# Timing analysis
timestamps = [pkt.timestamp for pkt in self.ch10_packets]
if len(timestamps) > 1:
intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)]
print(f"Packet count: {len(self.ch10_packets)}")
print(f"Time span: {timestamps[-1] - timestamps[0]:.3f} seconds")
print(f"Average interval: {statistics.mean(intervals)*1000:.3f} ms")
print(f"Min interval: {min(intervals)*1000:.3f} ms")
print(f"Max interval: {max(intervals)*1000:.3f} ms")
print(f"Std deviation: {statistics.stdev(intervals)*1000:.3f} ms")
# Detect potential issues
mean_interval = statistics.mean(intervals)
std_interval = statistics.stdev(intervals)
outliers = []
outlier_frames = []
for i, interval in enumerate(intervals):
if abs(interval - mean_interval) > 3 * std_interval:
outliers.append(interval * 1000) # Convert to ms
# Get the original frame number of the second packet in the interval
original_frame = self.ch10_packets[i + 1].original_frame_num
outlier_frames.append(original_frame)
if outliers:
print(f"WARNING: {len(outliers)} timing outliers detected!")
print(f"Outlier details:")
for i, (frame_num, interval_ms) in enumerate(zip(outlier_frames, outliers)):
if i < 10: # Show first 10 outliers
print(f" Frame {frame_num}: {interval_ms:.3f} ms interval")
elif i == 10:
print(f" ... and {len(outliers) - 10} more outliers")
# Channel ID analysis
channel_ids = [pkt.ch10_header['channel_id'] for pkt in self.ch10_packets if pkt.ch10_header is not None]
unique_channels = set(channel_ids)
print(f"Unique channels: {sorted(unique_channels)}")
for ch_id in unique_channels:
count = channel_ids.count(ch_id)
print(f" Channel {ch_id}: {count} packets")
# Sequence number analysis
seq_numbers = [pkt.ch10_header['sequence_number'] for pkt in self.ch10_packets if pkt.ch10_header is not None]
if len(set(seq_numbers)) < len(seq_numbers):
duplicates = len(seq_numbers) - len(set(seq_numbers))
print(f"WARNING: {duplicates} duplicate sequence numbers detected!")
# Check for sequence gaps
seq_gaps = []
valid_packets = [pkt for pkt in self.ch10_packets if pkt.ch10_header is not None]
for i in range(1, len(seq_numbers)):
expected = (seq_numbers[i-1] + 1) % 256
if seq_numbers[i] != expected:
original_frame = valid_packets[i].original_frame_num
seq_gaps.append((original_frame, seq_numbers[i-1], seq_numbers[i]))
if seq_gaps:
print(f"WARNING: {len(seq_gaps)} sequence number gaps detected!")
print(f"Sequence gap details:")
for i, (frame_num, prev, curr) in enumerate(seq_gaps):
if i < 10: # Show first 10 gaps
print(f" Frame {frame_num}: expected {(prev + 1) % 256}, got {curr}")
elif i == 10:
print(f" ... and {len(seq_gaps) - 10} more gaps")
def _analyze_ptp_statistics(self):
"""Analyze PTP packet statistics"""
print("\nPTP Statistics:")
print("-" * 40)
# Message type distribution
msg_types = [pkt.ptp_header['message_type'] for pkt in self.ptp_packets if pkt.ptp_header is not None]
unique_types = set(msg_types)
print(f"Total PTP packets: {len(self.ptp_packets)}")
print("Message type distribution:")
for msg_type in unique_types:
count = msg_types.count(msg_type)
print(f" {msg_type}: {count} packets")
# Timing analysis for Sync messages
sync_packets = [pkt for pkt in self.ptp_packets if pkt.ptp_header is not None and pkt.ptp_header['message_type'] == 'Sync']
if len(sync_packets) > 1:
sync_times = [pkt.timestamp for pkt in sync_packets]
sync_intervals = [sync_times[i+1] - sync_times[i] for i in range(len(sync_times)-1)]
print(f"\nSync message analysis:")
print(f" Count: {len(sync_packets)}")
print(f" Average interval: {statistics.mean(sync_intervals)*1000:.3f} ms")
print(f" Min interval: {min(sync_intervals)*1000:.3f} ms")
print(f" Max interval: {max(sync_intervals)*1000:.3f} ms")
print(f" Std deviation: {statistics.stdev(sync_intervals)*1000:.3f} ms")
def display_tmats_content(self):
"""Display assembled TMATS content"""
if self.tmats_assembler.get_frame_count() == 0:
print("No TMATS frames found")
return
print("\n" + "="*80)
print("TMATS (TELEMETRY ATTRIBUTES TRANSFER STANDARD) CONTENT")
print("="*80)
print(f"TMATS frames found: {self.tmats_assembler.get_frame_count()}")
# Assemble the TMATS content
assembled_content = self.tmats_assembler.assemble()
if assembled_content:
print(f"TMATS files found: {self.tmats_assembler.get_file_count()}")
print(f"Total TMATS length: {len(assembled_content)} characters")
print("\nTMATS Content:")
print("-" * 80)
# The assembled content is already cleaned by the assembler
lines = assembled_content.split('\n')
for line in lines:
if line.strip(): # Only print non-empty lines
print(line)
print("-" * 80)
# Show some statistics
attribute_lines = [line for line in lines if '\\' in line and ':' in line]
comment_lines = [line for line in lines if line.strip().startswith('COMMENT:')]
print(f"Total lines: {len([l for l in lines if l.strip()])}")
print(f"Attribute lines: {len(attribute_lines)}")
print(f"Comment lines: {len(comment_lines)}")
else:
print("No TMATS content could be assembled")
def main():
parser = argparse.ArgumentParser(description='Analyze PCAP files with Chapter 10 and PTP data')
parser.add_argument('pcap_file', help='Path to PCAP file')
parser.add_argument('--ch10-only', action='store_true', help='Show only Chapter 10 analysis')
parser.add_argument('--ptp-only', action='store_true', help='Show only PTP analysis')
parser.add_argument('--stats-only', action='store_true', help='Show only statistical analysis')
parser.add_argument('--summary-only', action='store_true', help='Show only summary information')
parser.add_argument('--no-tables', action='store_true', help='Skip detailed packet tables')
parser.add_argument('--tmats', action='store_true', help='Display TMATS (Telemetry Attributes Transfer Standard) content')
parser.add_argument('--tmats-only', action='store_true', help='Show only TMATS content')
args = parser.parse_args()
analyzer = PcapAnalyzer(args.pcap_file)
analyzer.analyze()
# Handle TMATS-only mode
if args.tmats_only:
analyzer.display_tmats_content()
return
# Show summaries first
if not args.stats_only:
if not args.ptp_only:
analyzer.display_ch10_summary()
if not args.ch10_only:
analyzer.display_ptp_summary()
# Show detailed tables unless suppressed
if not args.stats_only and not args.summary_only and not args.no_tables:
if not args.ptp_only:
analyzer.display_ch10_table()
if not args.ch10_only:
analyzer.display_ptp_table()
# Show TMATS content if requested
if args.tmats:
analyzer.display_tmats_content()
# Show statistical analysis
if not args.summary_only:
analyzer.statistical_analysis()
if __name__ == '__main__':
main()