moat docs ยท language 1
GitHub

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

ethipip4ip6tcpudpicmpicmp6

Fields

FieldTypePresent when
eth.type16-bit integeralways
vlan.id12-bit integerthe packet has one 802.1Q tag
ip.src, ip.dstaddressIPv4 or IPv6
ip.proto, ip.ttl8-bit integerIPv4 or IPv6 (ip.ttl is the hop limit on IPv6)
ip.len16-bit integerIPv4 or IPv6 (on IPv6 the payload length plus 40, held at 65535)
ip.fragconditionIPv4 or IPv6; true for any fragment
tcp.sport, tcp.dport, tcp.window16-bit integerTCP
tcp.flagsset of flagsTCP
tcp.mss16-bit integerTCP with an MSS option among its first 8 options
udp.sport, udp.dport, udp.len16-bit integerUDP
icmp.type, icmp.code8-bit integerICMP 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 sideOperatorsRight side
integer field== != < <= > >=integer
integer fieldin, not ininteger, range or list
address field== !=address
address fieldin, not inset, prefix, address or list of them
tcp.flags==, !=list of flags ({} means no flags)
tcp.flagshasnon-empty list of flags
payload[OFF:LEN]==, !=integer or string of exactly LEN bytes
payload, payload[OFF:LEN]contains, not containsstring 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.