How it works
Discovery
Every second, vulnboxd looks through services_dir (default /srv). Each directory with one of these files is a service, named after the directory:
compose.yamlcompose.ymldocker-compose.ymldocker-compose.yaml
Directories starting with a dot, and those listed in exclude, are skipped. A service whose directory disappears is dropped and its ports released.
Moving ports behind the proxy
For each service, vulnboxd reads the compose file after substituting variables from .env and the environment, and collects every port that is published on the host. It then writes an override to /var/lib/vulnboxd/<service>/compose.override.yml that republishes each of those ports on loopback, starting at 127.0.0.1:20000, and listens on the original public port itself.
The service's own compose file is never modified. The override uses ports: !override, so the original port list is replaced rather than merged with.
Use vulnboxctl compose
Because the override lives outside the service directory, a plain docker compose up would start the containers with their original ports, which then clash with the proxy. Use vulnboxctl compose <service> ... instead: it runs the same command with the override applied.
A port can be left alone with intercept: false in the rules file, for the whole service or one port. It is then published exactly as the compose file says, with no proxy and no rules.
The rules file
The first time a service is discovered, vulnboxd writes a vulnboxd-rules.yml into its directory. It lists every public port, with its protocol set to tcp or udp, and a header comment that documents every option. After that vulnboxd never writes it again, so your edits and comments stay.
If the service is in a git repository, the new file is committed as vulnboxd and pushed, rebasing onto the remote if it moved on. The whole team can then edit rules through git. Only that one file is committed; anything else someone has staged is left alone.
A port the file does not mention is still proxied. The file is a starting point, not the list of ports vulnboxd knows about: a port published later is picked up from the compose file, moved behind the proxy at the end of the tick like any other, and treated as plain tcp or udp with whatever service-wide rules: you have written. Add it to the file when you want rules or a protocol of its own. The reverse is only a warning: a port in the rules file that the compose file does not publish is logged and ignored.
A service added mid-game is discovered within a second, gets a rules file of its own written, committed and pushed the same way, and is deployed at the end of the tick.
What happens when something changes
| you change | picked up | takes effect |
|---|---|---|
| the rules | within a second | immediately, open connections included |
| the rules, but the file is invalid | within a second | not at all; the error is logged and the previous rules stay |
protocol | within a second | on the next connection; open ones keep the protocol they started with |
intercept or which ports are published | within a second | at the end of the tick, with a redeploy |
| code, pushed to git | on the next poll (poll_interval) | built right away, deployed at the end of the tick |
the compose file or .env | within a second | ports at the end of the tick; code on the next deploy |
Open connections switch to the new rules on their next read, request or message. On a tcp stream, a match that was part way through starts over.
Rules are not reloaded while the end-of-tick window is open, so that compiling them never holds up a deploy; a change made then is picked up just after.
Editing only the rules file never causes a rebuild: it is left out of the fingerprint vulnboxd uses to decide whether a service changed.
Git
Every poll_interval (default 10 s), vulnboxd fetches each service's repository. If only the remote moved, the branch is fast-forwarded. If both moved, local commits are replayed onto the remote, with any uncommitted changes stashed for the duration and put back afterwards. If that does not apply cleanly, everything is put back as it was and the conflict is logged for a human to resolve.
A repository without an upstream branch is left alone: local commits only.
vulnboxd drives git through libgit2, and authenticates with the deploy key recorded for the repository by init, falling back to an ssh agent.
Starting and restarting
On startup every service is deployed once, in the first window, so the containers match the current override. If vulnboxd is restarted and finds the containers already running with the override it would write, it takes them over as they are, without redeploying.
While vulnboxd is not running, intercepted ports have nothing listening on them. Run it under systemd with Restart=always.