172 lines
5.5 KiB
Python
172 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Summarize all .pcap files into a single unified PCAP metrics CSV.
|
|
Each row represents one scenario (one PCAP file): total bytes/packets sent and received.
|
|
Adds provider, protocol, dnssec_mode, keep_alive columns parsed from filenames.
|
|
"""
|
|
|
|
import argparse
|
|
import socket
|
|
import time
|
|
import csv
|
|
from pathlib import Path
|
|
from typing import List, NamedTuple
|
|
|
|
import dpkt
|
|
|
|
|
|
DEFAULT_LOCAL_IP = "192.168.100.2" # netns veth1 address
|
|
|
|
|
|
class PacketSummary(NamedTuple):
|
|
bytes_sent: int
|
|
bytes_received: int
|
|
packets_sent: int
|
|
packets_received: int
|
|
total_bytes: int
|
|
total_packets: int
|
|
|
|
|
|
def parse_config_from_filename(filename: str) -> dict:
|
|
"""Parse protocol, dnssec_mode, keep_alive from filename like 'dot-trust-persist.pcap'"""
|
|
base = filename.replace(".pcap", "").replace(".PCAP", "")
|
|
parts = base.split("-")
|
|
|
|
protocol = parts[0]
|
|
dnssec_mode = "off"
|
|
keep_alive = 0
|
|
|
|
for part in parts[1:]:
|
|
if part in ("auth", "trust"):
|
|
dnssec_mode = part
|
|
elif part == "persist":
|
|
keep_alive = 1
|
|
|
|
return {
|
|
"protocol": protocol,
|
|
"dnssec_mode": dnssec_mode,
|
|
"keep_alive": keep_alive,
|
|
}
|
|
|
|
|
|
def find_pcap_files(input_dir: Path) -> List[Path]:
|
|
files: List[Path] = []
|
|
for p in input_dir.rglob("*.pcap"):
|
|
if ".bak" not in p.name:
|
|
files.append(p)
|
|
return sorted(files)
|
|
|
|
|
|
def summarize_pcap(pcap_path: Path, local_ip_bytes: bytes) -> PacketSummary:
|
|
"""Read a PCAP file and return aggregated byte/packet counts by direction."""
|
|
bytes_sent = bytes_received = packets_sent = packets_received = 0
|
|
|
|
with open(pcap_path, "rb") as f:
|
|
try:
|
|
reader = dpkt.pcap.Reader(f)
|
|
except ValueError:
|
|
f.seek(0)
|
|
reader = dpkt.pcapng.Reader(f)
|
|
|
|
for _ts, buf in reader:
|
|
try:
|
|
eth = dpkt.ethernet.Ethernet(buf)
|
|
ip = eth.data
|
|
if not isinstance(ip, dpkt.ip.IP):
|
|
continue
|
|
size = len(buf)
|
|
if ip.src == local_ip_bytes:
|
|
bytes_sent += size
|
|
packets_sent += 1
|
|
elif ip.dst == local_ip_bytes:
|
|
bytes_received += size
|
|
packets_received += 1
|
|
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError, AttributeError):
|
|
continue
|
|
|
|
return PacketSummary(
|
|
bytes_sent=bytes_sent,
|
|
bytes_received=bytes_received,
|
|
packets_sent=packets_sent,
|
|
packets_received=packets_received,
|
|
total_bytes=bytes_sent + bytes_received,
|
|
total_packets=packets_sent + packets_received,
|
|
)
|
|
|
|
|
|
def merge_pcap_files(input_dir: Path, output_path: Path, local_ip: str):
|
|
local_ip_bytes = socket.inet_aton(local_ip)
|
|
|
|
pcap_files = find_pcap_files(input_dir)
|
|
if not pcap_files:
|
|
print("No .pcap files found.")
|
|
return
|
|
|
|
print(f"Found {len(pcap_files)} PCAP files")
|
|
print(f"Local IP: {local_ip}\n")
|
|
|
|
output_columns = [
|
|
"id", "provider", "protocol", "dnssec_mode", "keep_alive",
|
|
"bytes_sent", "bytes_received", "packets_sent", "packets_received",
|
|
"total_bytes", "total_packets",
|
|
]
|
|
|
|
total_rows = 0
|
|
t_total = time.time()
|
|
|
|
with open(output_path, "w", newline="", encoding="utf-8") as outfile:
|
|
writer = csv.DictWriter(outfile, fieldnames=output_columns)
|
|
writer.writeheader()
|
|
|
|
for pcap_path in pcap_files:
|
|
provider = pcap_path.parent.name.lower()
|
|
config = parse_config_from_filename(pcap_path.name)
|
|
|
|
print(f" {provider}/{pcap_path.name} "
|
|
f"({config['protocol']}, {config['dnssec_mode']}, persist={config['keep_alive']})")
|
|
|
|
t0 = time.time()
|
|
summary = summarize_pcap(pcap_path, local_ip_bytes)
|
|
dt = time.time() - t0
|
|
|
|
total_rows += 1
|
|
writer.writerow({
|
|
"id": total_rows,
|
|
"provider": provider,
|
|
"protocol": config["protocol"],
|
|
"dnssec_mode": config["dnssec_mode"],
|
|
"keep_alive": config["keep_alive"],
|
|
"bytes_sent": summary.bytes_sent,
|
|
"bytes_received": summary.bytes_received,
|
|
"packets_sent": summary.packets_sent,
|
|
"packets_received": summary.packets_received,
|
|
"total_bytes": summary.total_bytes,
|
|
"total_packets": summary.total_packets,
|
|
})
|
|
|
|
print(f" ✓ {summary.packets_sent:,} sent / {summary.packets_received:,} received "
|
|
f"| {summary.total_bytes:,} B total ({dt:.2f}s)")
|
|
|
|
elapsed = time.time() - t_total
|
|
print(f"\n{'=' * 60}")
|
|
print(f"PCAP metrics merged → {output_path}")
|
|
print(f"Total scenarios: {total_rows}")
|
|
print(f"Completed in {elapsed:.2f}s ({elapsed / 60:.1f} min)")
|
|
print(f"{'=' * 60}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Summarize all .pcap files into a unified CSV.")
|
|
parser.add_argument("input_dir", nargs="?", default=".", help="Input directory (default: .)")
|
|
parser.add_argument("-o", "--output", default="dns_results_pcap.csv", help="Output CSV path")
|
|
parser.add_argument("--local-ip", default=DEFAULT_LOCAL_IP,
|
|
help=f"Local netns veth IP for direction detection (default: {DEFAULT_LOCAL_IP})")
|
|
args = parser.parse_args()
|
|
|
|
merge_pcap_files(Path(args.input_dir), Path(args.output), args.local_ip)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|