Network & capture · Packet analysis
Packet analysis
SecVF taps the in-process virtual switch, pipes every Ethernet frame to a headless tshark, and parses its JSON output live for the UI. Wireshark-grade filtering, no external Wireshark required.
On this page
The capture pipeline
The capture path is four stages, all running concurrently inside the SecVF process:
┌─────────────┐ EthernetFrame ┌──────────────┐ bytes via FIFO ┌─────────┐
│ Virtual │ ────────────────▶│ Capture │ ────────────────▶│ tshark │
│ Switch │ (Combine) │ Manager │ (mkfifo + fd) │ (-T ek) │
└─────────────┘ └──────────────┘ └────┬────┘
│ JSON
▼
┌─────────────────┐
│ DecodedFrame │ ──▶ UI
│ Subject │ ──▶ stats
└─────────────────┘
- VirtualNetworkSwitch publishes an
EthernetFrameon every forwarded packet. Multiple subscribers can attach without copies — Combine handles the fan-out. - PacketCaptureManager maintains a named pipe (
mkfifoat/tmp/secvf-capture-<pid>) and writes each frame as a libpcap-formatted record into it. The pipe has the standard 64 KB kernel buffer. - tshark runs as a child process with the FIFO as its input file (
-r <fifo>) and EK JSON as its output (-T ek). EK is one JSON object per packet, line-delimited, and easy to stream-parse. - The decoder reads tshark's stdout, parses each line into a
DecodedFrame, and republishes on aPassthroughSubject. The UI's table view, the stats counters, and the hex panel all subscribe.
Starting a capture
Open Packet Analysis from the menu bar (Monitoring → Packet Analysis) or with ⌘⇧P.
- Select interfaces. The dropdown lists every running VM by name. Choose
all switch trafficto capture the entire bus, or pick specific VMs to scope the tap. - Set the BPF prefilter (optional). This is the kernel-level filter applied before the frame enters the capture queue. Use it to drop high-volume noise you never want — e.g.
not (host 224.0.0.0/4 or ip6 multicast). - Start. The "Start capture" button is disabled until tshark is detected on
$PATH. The status light goes green; frames begin streaming into the table.
tcp and port 443) and runs at capture time — dropped frames are gone forever. The display filter is Wireshark's grammar (tcp.port == 443) and runs over already-captured frames; you can change it without losing data.
Display filters
The filter bar at the top of the panel accepts the full Wireshark display-filter language. Every operator and field tshark understands works here.
| Filter | Matches |
|---|---|
tcp | Any TCP segment, regardless of port or direction. |
tcp.port == 443 | TCP traffic on port 443 (either source or dest). |
ip.addr == 10.0.100.5 | Frames where 10.0.100.5 is source or destination. |
dns and !mdns | DNS, but exclude multicast DNS chatter. |
http.request.method == "POST" | HTTP POST requests only — useful for finding C2 callbacks. |
tls.handshake.extensions_server_name contains "evil" | SNI-based filtering — pre-decryption visibility into TLS hostnames. |
frame.len > 1400 | Frames near MTU — useful for spotting fragmentation issues. |
tcp.flags.syn == 1 and tcp.flags.ack == 0 | SYN-only — connection openers; great for finding scans. |
icmp.type == 8 | ICMP echo requests (ping). |
arp | All ARP frames — the layer 2 conversation. |
Saved filters
Click the bookmark icon next to the filter bar to save the current filter with a name. Saved filters persist in ~/Library/Preferences/com.daxxsec.SecVF.plist under the SavedDisplayFilters array; portable across machines if you copy the plist.
Filter performance
Display filters are evaluated in tshark, not the SecVF UI. Practical implications:
- Field-based filters (
tcp.port == 443) are fast — tshark indexes them as it parses. - String-content filters (
frame contains "...") scan every frame body — slow on busy captures. - Compound filters (
and/or) short-circuit. Put cheap predicates first.
Protocol statistics
The Stats sidebar tallies every decoded frame by transport protocol. The numbers are always live and always reflect the current display filter — apply a filter, and the stats narrow to that subset.
Counted protocols (one row each):
- TCP, UDP — segment count, byte total, conn-tuple count
- DNS — query/response split, top queried names
- ARP — request/reply split, sender/target IP histograms
- ICMP / ICMPv6 — by type
- HTTP — method count, response code histogram
- TLS — by record type (handshake / app data / alert)
Click any row to set a filter for that protocol — quick way to drill down. Right-click for "Filter as exclusion" — sets the inverse.
Layer-by-layer decode
Selecting a frame in the table populates two side panels:
- Decoded layers — Ethernet → IP → TCP/UDP → application protocol. Each layer is a collapsible tree; clicking a field highlights the corresponding bytes in the hex view.
- Hex view — raw bytes, 16 per row, with ASCII column. The selected field's bytes are highlighted in the accent colour.
The decoded layers are exactly what tshark's -T ek emits. SecVF doesn't re-decode — it presents the upstream tshark dissection. This means you get every protocol Wireshark supports (~3000), including obscure ones, with no SecVF-side maintenance.
~/.config/wireshark/plugins/. SecVF's tshark instance picks it up automatically.
PCAP export
Two paths:
- File → Export PCAP… writes the entire capture buffer to a pcapng file. The file is uncompressed and importable into Wireshark, Zeek, Suricata, or anything that reads pcap.
- Right-click selection → Export selected writes only the highlighted frames. Useful for sharing a small slice of an investigation.
SecVF writes pcapng (modern pcap), not legacy pcap. If you need legacy format for an older tool, convert with editcap:
editcap -F libpcap input.pcapng output.pcap
Auto-rotation
Long captures rotate to disk to bound memory. The default is 100,000 frames in RAM; once exceeded, the oldest 50,000 spill to ~/.avf/Captures/<session>-rotN.pcapng and free their slot. Tweak in Settings → Capture → Buffer size.
Performance & backpressure
The pipeline is designed not to drop frames silently. Two places where backpressure matters:
Switch tap (Combine)
The frames publisher uses PassthroughSubject, which has no buffer. Subscribers run on a dedicated capture queue at .userInitiated QoS. If the queue depth grows past the configured threshold (SecVFCaptureQueueLimit in the plist, default 50,000), frames are tagged as dropped in the audit log — never silently lost. The drop count shows in the status bar.
FIFO & tshark
Linux/Darwin pipe buffer is 64 KB by default. At 1500-byte MTU that's ~42 frames before the writer would block. SecVF batches frames into the FIFO at 1 ms intervals and drops with logging if tshark falls behind for more than 250 ms.
Practical numbers on M2 Pro (32 GB):
- Sustained capture: ~110,000 packets/sec without drops
- UI-rendered table: 30 fps refresh, 1000 visible rows max (virtualized scroll)
- Memory: ~120 MB per 100,000 frames in buffer; rotation engages above that
Tuning for high-rate captures
- Tighten the BPF prefilter. Drop multicast/broadcast you don't care about at the kernel.
- Disable layer decode rendering. The hex/decoded panels do work even when collapsed; toggle them off in the View menu when you're chasing throughput.
- Increase buffer size. If you have RAM headroom, bump
SecVFCaptureQueueLimitto 200,000 or higher. - Capture to a file directly. "File → Capture to disk…" runs tshark with
-wand skips the in-memory buffer entirely. You lose live UI feedback but gain near-line-rate writes.
Working offline (no tshark)
Even without tshark, the packet panel still functions in basic mode:
- Frame metadata is shown (timestamp, source/dest MAC, EtherType, length).
- IP and TCP/UDP headers are decoded by SecVF directly (no third-party dissector).
- No application-protocol decode (no HTTP/DNS/TLS layer).
- No display filter language — only field-equality filters on the basic-decode fields.
Install tshark to unlock everything: brew install wireshark, then restart SecVF.
When capture won't start
Walk this list top-to-bottom:
- Is tshark on PATH? Open Terminal and run
which tshark. Empty output means it's not installed (see offline mode orbrew install wireshark). - Did SecVF detect it? The status bar shows
tshark: ✓ /opt/homebrew/bin/tsharkwhen detected. If it shows✗ not found, restart SecVF — tshark detection happens at launch. - Permissions on the FIFO? Check
~/.avf/logs/error-audit.logforFIFO creation failed. Usually means/tmpisn't writable — rare but possible on locked-down machines. - tshark crashed on startup? The error log captures stderr from the subprocess. Common cause: a malformed Lua dissector in
~/.config/wireshark/plugins/. - The VM isn't running. The interfaces dropdown only lists running VMs. Start the VM first, then start capture.
If none of the above: open Troubleshooting for the full diagnostic flow.