Skip to content

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.

yaml
pcap:
  enabled: true
  listen: 127.0.0.1:4242
  interface: any
  ports: 9000-9999
  filter: not port 22

vulnboxctl 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:

sh
sudo vulnboxctl install          # writes the unit; keeps the existing config
sudo systemctl enable --now vulnboxd-pcap

vulnboxctl 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:

sh
# 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.

text
<init.capture_dir>/tulip.json
<init.capture_dir>/shovel.yaml

init.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:

sh
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:

yaml
pcap:
  ports: 9000,9100-9200,8000

That 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 ​

settingdefault
pcap.enabledfalserun it at all. The setup turns it on and installs the service
pcap.listen127.0.0.1:4242where readers connect. pcap-broker's own port
pcap.interfaceanywhich interface to capture, or any for all of them
pcap.portsthe game'sonly capture these ports: single ports and ranges in any mix, 9000,9100-9200. Empty captures every port
pcap.filtera pcap filter for what to leave out, such as not port 22
pcap.commandthe whole capture command, for when the above is not enough
pcap.buffer_mb64how 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:

yaml
pcap:
  command: dumpcap -i eth0 -w - -P

A 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:

sh
systemctl status vulnboxd-pcap
journalctl -fu vulnboxd-pcap

A 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:

yaml
# Written by vulnboxctl: one entry per service listener.
services:
  notes-notes:
    host: "10.60.13.2"
    port: 9100

Readers 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.