Getting started Testing with cases
Testing with cases run your rules in the kernel before they see real traffic.
moat test rules.moat cases.toml compiles the file, loads it into the
running kernel without attaching it to any interface, and runs each case
with BPF_PROG_TEST_RUN. The verifier that checks it is the one that
will check it on the interface.
[sets]
verified = ["192.0.2.10"] # added to the set before the cases run
banned = ["198.51.100.77 expired"] # inserted with an expiry in the past
[[case]]
name = "query from an unverified source"
packet = { proto = "udp", src = "192.0.2.50", dport = 27015, payload = "hex:ffffffff" }
expect = { verdict = "drop", rule = "query" }
rule is optional; "default" names the default action.
When a case fails
moat explain shows which rule decided and why.
$ moat explain rules.moat cases.toml [CASE...]
For the last packet of each case, or only of the cases named, it prints the packet as moat parsed it, then every rule up to the one that decided, each with why it did not match or what it did:
query with the wrong header
packet: UDP over IPv4 192.0.2.8:40000 > 198.51.100.1:27015, 4 bytes of payload
strict_mode: no match, strict is false
bad_flags: no match, tcp.flags == {fin, syn}, but the packet is UDP over IPv4
probes: no match, tcp.dport in {23, 2323}: the packet has no TCP header
query: no match, payload[0:4] == 0xffffffff, but the packet has 0x00ffffff there
no rule decides: the default, pass
FAIL query with the wrong header: got pass by default, want drop
Every case still runs, so limits and sets fill as they do in moat test,
and a rule over its rate says so: query: matches, over the rate: adds 192.0.2.7 to banned, drop. moat explain always runs in
moat's model of the program and says so on its last
line. It exits with 1 when a case fails, and 2 for a usage error or a
case name that is not in the file.
Without a kernel
$ moat test rules.moat cases.toml --model
$ moat test rules.moat --pcap capture.pcap [cases.toml] --model
--model runs the cases in moat's model of the program instead: a Go
version of what the generated program does, with the same packet
parsing, the same tests, the same rate limits with the same rounding,
set expiry, adds that never shorten an entry, and audit mode. It needs no
kernel and no root, so it works on macOS and in unprivileged CI. The
summary line ends with (model), as in 9 of 9 cases passed (model).
Off Linux, moat test uses the model on its own and says so on stderr.
On Linux without the privileges to load a program, the error suggests
--model.
moat's own tests check the model against the kernel packet by packet, but the model is not the program. On Linux, run moat test without --model before trusting a ruleset. The model also keeps every rate limit key and set entry, where the kernel's maps evict the least recently used ones when they are full.
Packets
Keys: proto (tcp, udp, icmp, icmp6, other), src, dst,
sport, dport, ttl, flags (for example "syn,ack"), window,
mss, icmp_type, icmp_code, vlan, frag (a fragment other than
the first), payload (text, or hex:..., which may contain spaces) and
udp_len. raw = "hex:..." gives the whole frame instead, for malformed
packets. Unknown keys are errors. A key left out gets a default; a
written 0 for sport, dport, ttl or window means 0.
Time
Time is simulated, so tests of limits and expiring entries are exact. A
run starts at time zero; time = "1500ms" sends a case's packet that
long after the start, and without it a case follows the previous one.
repeat = 20 and every = "10ms" send the packet several times. The
expected verdict and rule are those of the last packet, and drops = 3
checks how many of them were dropped. every needs repeat above 1, and
drops cannot be more than the packets sent. A top-level audit = true
runs the cases in audit mode, and a [vars] table sets variables.
Captures and timing
$ moat test rules.moat --pcap capture.pcap
$ moat bench rules.moat cases.toml
With --pcap, every packet of a capture runs through the rules with the
capture's timestamps as the clock, and moat prints how many packets each
rule decided. It reads pcap and pcapng files with Ethernet, raw IPv4 or
IPv6, loopback (as macOS writes lo0) or Linux cooked frames; a record
too short for its own header is skipped and counted. A case file can
follow the capture, moat test FILE --pcap CAPTURE CASES: its [sets],
[vars] and audit apply, and it cannot hold [[case]] entries then
(exit 2).
moat bench runs each case through the production build and prints the
nanoseconds per run. --repeat N sets how many runs, 100000 by default,
and --pcap CAPTURE times the packets of a capture instead of the
cases.
moat test exits with 1 when the rules have errors or a case fails, 2
when the command line or the case file is wrong, and 3 when the kernel
rejects the program.