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.

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
                                                              └─────────────────┘
  1. VirtualNetworkSwitch publishes an EthernetFrame on every forwarded packet. Multiple subscribers can attach without copies — Combine handles the fan-out.
  2. PacketCaptureManager maintains a named pipe (mkfifo at /tmp/secvf-capture-<pid>) and writes each frame as a libpcap-formatted record into it. The pipe has the standard 64 KB kernel buffer.
  3. 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.
  4. The decoder reads tshark's stdout, parses each line into a DecodedFrame, and republishes on a PassthroughSubject. The UI's table view, the stats counters, and the hex panel all subscribe.
Why a FIFO? Two reasons: (1) tshark expects a libpcap source — file or interface — and a FIFO satisfies the file-source path without spilling to disk, and (2) the kernel pipe buffer absorbs short bursts so the switch never has to block on a slow consumer.

Starting a capture

Open Packet Analysis from the menu bar (Monitoring → Packet Analysis) or with P.

  1. Select interfaces. The dropdown lists every running VM by name. Choose all switch traffic to capture the entire bus, or pick specific VMs to scope the tap.
  2. 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).
  3. Start. The "Start capture" button is disabled until tshark is detected on $PATH. The status light goes green; frames begin streaming into the table.
BPF prefilter ≠ display filter. The BPF prefilter is BSD packet filter syntax (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.

FilterMatches
tcpAny TCP segment, regardless of port or direction.
tcp.port == 443TCP traffic on port 443 (either source or dest).
ip.addr == 10.0.100.5Frames where 10.0.100.5 is source or destination.
dns and !mdnsDNS, 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 > 1400Frames near MTU — useful for spotting fragmentation issues.
tcp.flags.syn == 1 and tcp.flags.ack == 0SYN-only — connection openers; great for finding scans.
icmp.type == 8ICMP echo requests (ping).
arpAll 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.

Custom dissectors. If you have a Wireshark Lua dissector, drop it in ~/.config/wireshark/plugins/. SecVF's tshark instance picks it up automatically.

PCAP export

Two paths:

  1. 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.
  2. 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

  1. Tighten the BPF prefilter. Drop multicast/broadcast you don't care about at the kernel.
  2. 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.
  3. Increase buffer size. If you have RAM headroom, bump SecVFCaptureQueueLimit to 200,000 or higher.
  4. Capture to a file directly. "File → Capture to disk…" runs tshark with -w and 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:

  1. Is tshark on PATH? Open Terminal and run which tshark. Empty output means it's not installed (see offline mode or brew install wireshark).
  2. Did SecVF detect it? The status bar shows tshark: ✓ /opt/homebrew/bin/tshark when detected. If it shows ✗ not found, restart SecVF — tshark detection happens at launch.
  3. Permissions on the FIFO? Check ~/.avf/logs/error-audit.log for FIFO creation failed. Usually means /tmp isn't writable — rare but possible on locked-down machines.
  4. tshark crashed on startup? The error log captures stderr from the subprocess. Common cause: a malformed Lua dissector in ~/.config/wireshark/plugins/.
  5. 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.