Protocols
Every port has a protocol. New ports start as tcp or udp; set http, ftp or dns yourself for protocol-aware rules. A wrong guess would break the service, which is why vulnboxd does not guess.
ports:
"8080":
protocol: httptcp
Each direction of a connection is one continuous stream. contains and regex find a pattern wherever it lands in it, even split across reads or megabytes apart, in constant memory.
- Block closes the connection.
larger_thancounts everything one side has sent over the connection.replacerewrites the stream as it passes.
"1337":
protocol: tcp
rules:
- name: shell payloads
match: { regex: '/bin/(ba)?sh|/etc/passwd' }
- name: oversized upload
match: { larger_than: 64K }http
Requests and responses are parsed, so rules can use method, path, headers, headers_opt, body and status, on top of contains and regex over the whole message.
- Block answers with
set_status(default 403),set_bodyandset_headers. - Rules can change headers, status and body, route requests elsewhere or mirror them.
A few things vulnboxd does on every http port:
Accept-Encodingis removed from requests, so responses come back uncompressed and body rules can read them.- Hop-by-hop headers (
Connection,Keep-Alive,Upgradeand the like) are not forwarded, as a proxy should not; WebSocket upgrades are the exception. - Connections to the service are kept and reused for the next client, which saves a TCP handshake per request. If a service cannot cope with that, set
pool_upstream: falsefor the service or the port. - A request whose headers take longer than
timeouts.http_request(30 s) to arrive, or whose body does, gets a 408.
h2c
An http port serves whichever version the client speaks. A connection that opens with the http/2 preface is served as cleartext http/2 (h2c), anything else as http/1.1. vulnboxd then talks the same version to the service, since a service reached over h2c generally speaks nothing else. Rules see the same requests and responses either way.
WebSockets
WebSocket connections on http ports are proxied too. The upgrade request and response go through the http rules as usual. After that, each data frame is matched like an http message with:
methodset toWEBSOCKET,pathset to the path of the upgrade request,- the frame's payload as
body, and forcontainsandregex.
- name: no shell commands over the socket
match: { method: WEBSOCKET, path: '^/ws', body: '(?i)\b(bash|sh|nc)\b' }Blocking closes the connection. Compression (permessage-deflate) is not negotiated, so payloads stay readable. Frames are checked one at a time, so a pattern split across the frames of one fragmented message is not matched.
ftp
Each line of the control connection is matched on its own, in both directions: commands going in, replies coming out.
- Block on a command answers it with
550and the session carries on. Blocking a reply closes the session. - Data connections use ports of their own, which are handled like any other port: plain
tcpunless you say otherwise.
"2121":
protocol: ftp
rules:
- name: no site commands
match: { regex: '(?i)^SITE ' }udp
Each datagram is matched on its own.
- Block drops the datagram.
larger_thancounts each datagram.
dns
Datagrams are parsed as DNS, so rules can also use qname, a regex over the lower-cased query name, and qtype, such as A, TXT or ANY.
- Block answers the query with
REFUSED.
"53/udp":
protocol: dns
rules:
- name: data exfiltration over txt records
match: { qtype: TXT }
- name: only our own zone
not: { qname: '(^|\.)example\.ctf$' }Timeouts
tcp, ftp, http and WebSocket connections that see no traffic for timeouts.idle (2 minutes) are closed. There are deliberately no connection limits: in many games all traffic arrives through one NATed address, so any cap would hit the checker too.