Capturing traffic
vulnboxd can capture every packet the box sees and serve it over PCAP-over-IP, so the tools that pick attacks and flags out of traffic read it live over a socket instead of scraping files off the box.
pcap:
enabled: true
listen: 127.0.0.1:4242
interface: any
ports: 9000-9999
filter: not port 22vulnboxctl setup-interactive asks for this, and vulnboxctl install writes the service when it is on.
Turning it on later
The unit is written when pcap.enabled is set at install time, so a team that decides a day into the game needs one more step:
sudo vulnboxctl install # writes the unit; keeps the existing config
sudo systemctl enable --now vulnboxd-pcapvulnboxctl install is safe to run again: it keeps your config and only puts what is missing in place.
Reading the stream
Anything that speaks PCAP-over-IP works:
# Locally on the box, or through ssh from your laptop.
nc 127.0.0.1 4242 | tcpdump -r - -nn
ssh vulnbox 'nc 127.0.0.1 4242' | tshark -r -The header is always written little-endian, with microsecond timestamps and a snapshot length of 65535, whatever the capture produced; the link type is the capture's own. That is pcap-broker's normalisation, so every consumer sees one format. A capture with nanosecond timestamps is rewritten to microseconds, losing the last three digits — the only thing that is not passed through untouched.
Nothing is written to disk. With no reader connected the packets are read and dropped, so leaving capture on all game costs no space; it also means you only have what someone was listening for.
The tools' own configuration
Separately from the broker, vulnboxctl init writes the files tulip and shovel want, so captured traffic is filed under a service name rather than a port number: one entry per public port, named after the service, at the address in init.vulnbox_ip.
<init.capture_dir>/tulip.json
<init.capture_dir>/shovel.yamlinit.capture_dir defaults to the working directory. They are rewritten whenever the setup runs, so a service added later ends up in them. They describe your services, not the broker, so where to read the traffic from is still yours to point the tool at.
What ends up in the capture
interface, ports and filter build a tcpdump command, plus one term that is always added. With the config at the top of this page:
tcpdump -i any -n --immediate-mode -s 65535 -U -w - \
'(not port 22) and portrange 9000-9999 and not (host 127.0.0.1 and tcp port 4242)'Three parts, ANDed, so each only narrows what is kept: your filter, your ports, and the broker's own connections, which are always left out.
ports keeps the game in and the box out. A vulnbox talks to itself constantly — containers to their databases, the proxy to its backends, the daemon to GitHub — and none of that is what you want to look through afterwards. Game modes that know where their services are fill this in: ECSC_2026 gives 9000-9999, because during the ECSC everything else is internal traffic. Setting it yourself wins over the game's, and an empty value captures every port.
Single ports and ranges mix freely, for a game that does not keep its services together:
pcap:
ports: 9000,9100-9200,8000That becomes (port 9000 or portrange 9100-9200 or port 8000). Each part covers tcp and udp, and a bad one — a range that ends before it starts, a port of 0 — is refused when the config is read rather than when the capture fails to start.
The broker's own traffic is always excluded. Serving a packet is itself traffic, so a capture that saw its own PCAP-over-IP connections would serve those too, and serving those would make more again. vulnboxd knows its listen address and leaves it out.
If you bind to every address
With listen: 0.0.0.0:4242 the address says nothing about which side is the broker, so the whole port has to be excluded everywhere rather than just the broker's own connections. Binding to one address keeps the exclusion narrow.
With the proxy in the path
Every intercepted service is reached through the proxy, so on the wire each exchange exists twice: the client to the public port, and vulnboxd to the container on a loopback port from backend_port_base (20000 and up).
The default ports narrowing keeps the second one out, which is what you want: tulip and shovel see one flow per exchange, from the real client address to the real service port. If you widen ports or clear it, expect every intercepted request to appear twice, the inner copy from 127.0.0.1.
Settings
| setting | default | |
|---|---|---|
pcap.enabled | false | run it at all. The setup turns it on and installs the service |
pcap.listen | 127.0.0.1:4242 | where readers connect. pcap-broker's own port |
pcap.interface | any | which interface to capture, or any for all of them |
pcap.ports | the game's | only capture these ports: single ports and ranges in any mix, 9000,9100-9200. Empty captures every port |
pcap.filter | a pcap filter for what to leave out, such as not port 22 | |
pcap.command | the whole capture command, for when the above is not enough | |
pcap.buffer_mb | 64 | how far behind one reader may fall before it is disconnected |
Keep it off the public internet
The stream is every packet the box sees: your flags, your traffic and your patches going past. Leave listen on loopback and read it over your team's vpn or an ssh tunnel, or bind it to the vpn address. Nothing about PCAP-over-IP asks who is connecting.
Where all of this is not enough, command replaces it entirely — and then what it captures is your affair, including keeping its own traffic out. It has to write classic pcap to its output, as tcpdump -w - does:
pcap:
command: dumpcap -i eth0 -w - -PA pcapng stream is refused with a message saying so rather than read as nonsense. See vulnboxd-pcap for the command-line and environment overrides.
Its own service
The broker is a separate binary and a separate service, vulnboxd-pcap, tied to nothing of the daemon's:
systemctl status vulnboxd-pcap
journalctl -fu vulnboxd-pcapA gap in the capture is a gap in what the team can look at afterwards, so upgrading vulnboxd leaves it running, and restarting it does not touch the proxy. If its capture command dies it alerts and systemd tries three times before leaving the unit failed, so a capture that cannot work is visible rather than looping in silence.
Slow readers cannot stall the capture
Each reader gets its own buffer, pcap.buffer_mb (64 MB). One that stops reading — a laptop that went to sleep, an analysis tool that fell over — fills its own buffer and is then disconnected with a log line. The capture and every other reader carry on.
A plain list of the services
Set init.services_file and the setup also writes every service listener as a name, an address and a port — for the tools that want to be told where the services are rather than work it out from a compose file:
# Written by vulnboxctl: one entry per service listener.
services:
notes-notes:
host: "10.60.13.2"
port: 9100Readers across a network
Each consumer's socket has Nagle off (TCP_NODELAY), and that is not a detail. A pcap record is a hundred-odd bytes; with Nagle on, a reader on another box gets one segment per delayed acknowledgement — a few hundred packets a second — while a reader on the box itself sees everything, because there a round trip costs nothing. Two readers, one stream, one of them quietly starved: an assembler on another machine was getting a fraction of what Suricata beside the capture got, with no packet lost anywhere and nothing in any log to say so.
Whatever is already queued for a consumer goes out in one write, so a busy capture does not become one small segment per packet either.
Compared with pcap-broker
It is a reimplementation of fox-it's pcap-broker and sends the same bytes, so anything already pointed at one works unchanged: a client connects, is sent a pcap file header, and then every packet as it is captured. PCAP_COMMAND and LISTEN_ADDRESS work too, as do --cmd and --listen. Its sockets are set up the same way, Nagle included — Go turns that off for every connection it makes, so pcap-broker never had to think about it. It is built in so there is nothing extra to install and it is configured from the same file as the rest.
One behaviour is deliberately not copied: pcap-broker writes to its clients inside the packet loop, so one stalled reader backs the capture up for everyone. That loss would be silent and global.