Language Conditions
Conditions decide which packets a rule sees.
A condition combines protocol tests, comparisons on header fields and
payload matches with and, or, not and parentheses.
Protocol tests
ethipip4ip6tcpudpicmpicmp6Fields
| Field | Type | Present when |
|---|---|---|
eth.type | 16-bit integer | always |
vlan.id | 12-bit integer | the packet has one 802.1Q tag |
ip.src, ip.dst | address | IPv4 or IPv6 |
ip.proto, ip.ttl | 8-bit integer | IPv4 or IPv6 (ip.ttl is the hop limit on IPv6) |
ip.len | 16-bit integer | IPv4 or IPv6 (on IPv6 the payload length plus 40, held at 65535) |
ip.frag | condition | IPv4 or IPv6; true for any fragment |
tcp.sport, tcp.dport, tcp.window | 16-bit integer | TCP |
tcp.flags | set of flags | TCP |
tcp.mss | 16-bit integer | TCP with an MSS option among its first 8 options |
udp.sport, udp.dport, udp.len | 16-bit integer | UDP |
icmp.type, icmp.code | 8-bit integer | ICMP or ICMPv6 |
A test on a header the packet does not have is false, the negated forms too. not udp.dport == 53 also matches every packet that is not UDP; write udp and not udp.dport == 53 if that is the intent.
Comparisons
| Left side | Operators | Right side |
|---|---|---|
| integer field | == != < <= > >= | integer |
| integer field | in, not in | integer, range or list |
| address field | == != | address |
| address field | in, not in | set, prefix, address or list of them |
tcp.flags | ==, != | list of flags ({} means no flags) |
tcp.flags | has | non-empty list of flags |
payload[OFF:LEN] | ==, != | integer or string of exactly LEN bytes |
payload, payload[OFF:LEN] | contains, not contains | string or 0x... bytes, 1 to 32 bytes |
Matching the payload
payload[OFF:LEN] compares a slice at a fixed place. payload contains
finds text or bytes anywhere in the first 256 bytes, or in a window of up
to 512.
rule source_query {
when udp.dport == 27015 and payload[0:4] == 0xffffffff
then limit 10/s per ip.src burst 20
}
rule admin_probe {
when tcp.dport == 80 and payload contains "GET /admin"
then drop
}
A slice reads LEN bytes (1 to 32) starting OFF bytes after the transport header, ending no later than byte 1500. Packets too short for the slice do not match.
payload[OFF:LEN] contains TEXT looks in a window of LEN bytes from OFF,
and the window must end by byte 512. not contains is true only when the
packet carries at least OFF plus the text's length in payload bytes, so a
SYN, a bare ACK or an empty datagram is neither.
The end of the payload comes from the IP length, so the Ethernet padding
of a short frame is not payload: payload contains never matches padding
bytes, and an empty datagram padded to 60 bytes still has no payload.
A file can use at most 16 distinct text and window pairs: the kernel checks each search on its own when loading. Using the same text and window in several rules counts once.
IPv6 and VLANs
On IPv6, moat skips up to four hop-by-hop, routing, fragment and
destination options headers. vlan.id is the first 802.1Q or 802.1ad
tag; with two tags the inner one is not read and the packet has no IP
fields.
With rx VLAN offload on, which is how most NICs ship, the driver strips the 802.1Q tag before XDP runs. On Linux 6.8 or newer, in native mode, with a driver that reports stripped tags (veth, mlx5, ice), moat load builds the program to ask the driver for the tag, so vlan.id still matches; a tag left in the frame wins. Otherwise tagged frames reach the rules untagged and vlan.id never matches: turn the offload off with ethtool -K eth0 rxvlan off. moat load warns when the rules test vlan.id in that case, and moat doctor --iface says which case an interface is in.
Objects from moat build and moat test never ask the driver for the
tag, since that ties a program to one interface.