3 AM, an Alert, and 50 Connections You Can’t Explain
The alert fired at 3:12 AM: outbound bandwidth on a production server spiked from its normal 20 Mbps to 290 Mbps. SSH in, run netstat -an | grep ESTABLISHED, and there they are — 50+ connections to IP addresses that have no business being there.
That night I did what most engineers do: opened Wireshark, captured a few minutes of traffic, and manually clicked through thousands of packets trying to find a pattern. It took 40 minutes to identify the source. Not because the problem was hard — because the tooling was wrong for the job.
The fix was a 150-line Python script using PyShark. It now runs on every production node I manage. When an alert fires, I get a formatted report in my terminal within two minutes instead of forty.
What PyShark Actually Is
PyShark is a Python wrapper around TShark — the command-line engine that powers Wireshark’s packet dissection. When you use Wireshark’s GUI, TShark is what’s actually parsing the wire data underneath. PyShark exposes that same engine through a clean Python API.
The critical difference from raw socket captures or Scapy: PyShark gives you already-dissected packets. You don’t manually unpack bytes to find a destination port. It’s just packet.tcp.dstport. HTTP method? packet.http.request_method. DNS query? packet.dns.qry_name. All of Wireshark’s 3,000+ protocol dissectors, accessible from Python.
In my real-world experience, this is one of the essential skills to master if you work with network infrastructure. Once you can analyze traffic programmatically, you stop reacting to incidents and start catching patterns before they become incidents.
PyShark supports two capture modes:
- LiveCapture — captures packets in real-time from a network interface
- FileCapture — reads from an existing
.pcapfile, ideal for post-incident analysis when you saved a dump earlier
Building the Automated Traffic Analyzer
Setting Up Your Environment
PyShark needs TShark installed as a backend. On Ubuntu/Debian:
# Install TShark (standalone, no Wireshark GUI needed)
sudo apt-get install tshark
# Allow non-root capture — add your user to the wireshark group
sudo usermod -aG wireshark $USER
newgrp wireshark
# Install PyShark
pip install pyshark
On CentOS/RHEL: sudo dnf install wireshark-cli. The group name may be wireshark or pcap depending on distribution.
Capturing Packets in Real-Time
Start with the simplest useful capture — just print source/destination for all TCP traffic:
import pyshark
capture = pyshark.LiveCapture(interface='eth0', bpf_filter='tcp')
capture.sniff(timeout=60)
for packet in capture:
try:
src = packet.ip.src
dst = packet.ip.dst
dport = packet.tcp.dstport
print(f"{src} -> {dst}:{dport}")
except AttributeError:
pass # Skip non-IP or malformed packets
The bpf_filter='tcp' uses Berkeley Packet Filter syntax — the same as tcpdump. Filtering at capture time reduces memory usage significantly on busy interfaces.
Aggregating Connections and Detecting Anomalies
Raw packet-by-packet output isn’t what you need during an incident. You need aggregation: which destinations are getting the most traffic? Which ports? Is any single IP receiving an unusually high connection count?
import pyshark
from collections import defaultdict, Counter
import time
def analyze_traffic(interface='eth0', duration=120, threshold=20):
"""
Capture traffic for `duration` seconds.
Flag any destination IP with more than `threshold` connections.
"""
connection_counts = Counter()
bytes_per_ip = defaultdict(int)
port_counts = Counter()
# BPF filter: only outbound TCP, excluding private subnets
bpf = 'tcp and not (dst net 10.0.0.0/8 or dst net 192.168.0.0/16 or dst net 172.16.0.0/12)'
capture = pyshark.LiveCapture(
interface=interface,
bpf_filter=bpf
)
print(f"[*] Capturing on {interface} for {duration}s...")
capture.sniff(timeout=duration)
for packet in capture:
try:
dst_ip = packet.ip.dst
dst_port = packet.tcp.dstport
pkt_len = int(packet.length)
connection_counts[dst_ip] += 1
bytes_per_ip[dst_ip] += pkt_len
port_counts[dst_port] += 1
except AttributeError:
continue
anomalies = {
ip: count
for ip, count in connection_counts.items()
if count > threshold
}
return connection_counts, bytes_per_ip, port_counts, anomalies
Generating a Human-Readable Report
After the capture window closes, format the data into something you can actually read at 3 AM:
def generate_report(connection_counts, bytes_per_ip, port_counts, anomalies):
lines = []
lines.append("=" * 60)
lines.append("NETWORK TRAFFIC ANALYSIS REPORT")
lines.append(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}")
lines.append("=" * 60)
lines.append("\n[TOP 10 DESTINATION IPs BY CONNECTION COUNT]")
for ip, count in connection_counts.most_common(10):
mb = bytes_per_ip[ip] / (1024 * 1024)
lines.append(f" {ip:<20} {count:>6} conns {mb:>8.2f} MB")
lines.append("\n[TOP 10 DESTINATION PORTS]")
for port, count in port_counts.most_common(10):
lines.append(f" Port {port:<8} {count:>6} packets")
if anomalies:
lines.append("\n[!! ANOMALIES — HIGH CONNECTION COUNT]")
for ip, count in sorted(anomalies.items(), key=lambda x: -x[1]):
lines.append(f" !! {ip} — {count} connections")
else:
lines.append("\n[OK] No anomalies above threshold")
report = "\n".join(lines)
print(report)
fname = f"traffic_report_{time.strftime('%Y%m%d_%H%M%S')}.txt"
with open(fname, 'w') as f:
f.write(report)
print(f"\n[*] Report saved to {fname}")
return report
The Full Runnable Script
Wire everything together with a CLI interface:
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='PyShark Traffic Analyzer')
parser.add_argument('--interface', default='eth0')
parser.add_argument('--duration', type=int, default=120)
parser.add_argument('--threshold', type=int, default=20)
args = parser.parse_args()
conn_counts, bytes_ip, port_counts, anomalies = analyze_traffic(
interface=args.interface,
duration=args.duration,
threshold=args.threshold
)
generate_report(conn_counts, bytes_ip, port_counts, anomalies)
# Basic 2-minute capture on eth0
sudo python3 traffic_analyzer.py
# Longer capture with higher anomaly threshold
sudo python3 traffic_analyzer.py --interface ens3 --duration 300 --threshold 50
Analyzing a Saved pcap File
If you captured a pcap during an incident with tcpdump, analyze it after the fact by swapping LiveCapture for FileCapture:
def analyze_pcap(filepath):
# display_filter uses Wireshark syntax — more expressive than BPF
capture = pyshark.FileCapture(
filepath,
display_filter='tcp and not ip.dst == 192.168.0.0/16'
)
for packet in capture:
try:
ts = packet.sniff_time
src = packet.ip.src
dst = packet.ip.dst
port = packet.tcp.dstport
print(f"{ts} | {src} -> {dst}:{port}")
except AttributeError:
continue
capture.close() # Always close FileCapture explicitly
Notice display_filter uses Wireshark’s display filter syntax rather than BPF — you can write things like 'http.request.method == "POST"' or 'dns.qry.name contains "evil"'. That’s the real power of TShark as a backend.
Adding Production-Grade Checks
The basic version handles most incidents. For production monitoring, add these:
Flag traffic to known suspicious ports:
SUSPICIOUS_PORTS = {'4444', '6666', '6667', '31337', '1337', '9001', '8080'}
def check_suspicious_ports(port_counts):
hits = {p: c for p, c in port_counts.items() if str(p) in SUSPICIOUS_PORTS}
if hits:
print(f"[!! ALERT] Traffic detected on suspicious ports: {hits}")
return hits
Continuous windowed monitoring — wrap the capture in a loop, capture 5-minute windows, compare each against the previous one. A 3× spike from one window to the next is more meaningful than an absolute threshold.
Webhook alerting — when anomalies are detected, POST to a Slack or Telegram webhook. Combine with a cron job and you have passive continuous monitoring without a full SIEM stack.
Geolocation context — the geoip2 library maps IPs to countries using a local MaxMind database. Seeing 80% of connections suddenly routing to a country your users don’t live in is a clearer signal than a connection count alone.
What This Replaces (and What It Doesn’t)
This tool handles automated triage, baseline comparison, and report generation — the repetitive parts of network investigation that don’t benefit from a human clicking through a GUI.
Wireshark still wins when you need to follow a specific TCP stream, inspect payload bytes, or reconstruct an HTTP session visually. Keep it installed. But for the first 10 minutes of any network incident — where you need to understand the shape of the traffic before diving deep — a scripted PyShark capture beats manual filtering every time.
The script that started as a 3 AM fix has run on my servers for six months. It’s caught three unusual traffic patterns that would have gone unnoticed until they hit billing or bandwidth caps. That’s the payoff: automated analysis doesn’t sleep when you do.

