Firewall (filter rules)
File: src/topics/firewall.j. Path: /ip/firewall/filter (FIREWALL_PATH).
Background
Filter rules decide which packets live and which die. Every rule sits in a chain - which traffic it looks at:
CHAIN_INPUT("input"): traffic to the router itself (its SSH, its DNS, its WinBox).CHAIN_FORWARD("forward"): traffic through the router (LAN to internet, LAN to LAN).CHAIN_OUTPUT("output"): traffic the router itself originates.
Rules are evaluated top to bottom; the first match wins. The action says what happens: ACTION_ACCEPT, ACTION_DROP (silent), ACTION_REJECT (tells the sender).
The rule builder
A FirewallRule is built value-semantically: start with chain+action, refine with with* helpers (each returns a fresh copy), then create it on the router:
def r as mt.FirewallRule init mt.firewallRule(mt.CHAIN_FORWARD, mt.ACTION_DROP);
$r = mt.withProtocol($r, "tcp");
$r = mt.withSrcAddress($r, "10.0.0.0/8");
$r = mt.withDstPort($r, "445");
$r = mt.withComment($r, "no smb forwarding");
def id as string init mt.addFirewallRule($c, $r);Refiners: withProtocol, withSrcAddress, withDstAddress, withSrcAddressList, withDstAddressList (match a whole named set - see addresslist.md), withSrcPort, withDstPort (single "80", list "80,443", range "8000-8100"), withInInterface, withOutInterface, withComment, withDisabled. Unset fields are simply omitted from the rule - an empty builder matches everything in its chain.
Validation happens before the wire: unknown actions and protocols are rejected with the allowed values listed, and a port match without a protocol fails with "call withProtocol(rule, \"tcp\") ... first" (RouterOS's own error for that is much less helpful).
Comments as handles
Give every rule a distinct comment - it is the friendly handle for management without ids:
mt.disableFirewallRuleByComment($c, "no smb forwarding"); # pause it
mt.enableFirewallRuleByComment($c, "no smb forwarding"); # resume
mt.removeFirewallRuleByComment($c, "no smb forwarding"); # deleteOrdering
The chains are walked top to bottom and stop at the first match, so position is the policy. addFirewallRule appends, which means a rule added after a broad drop sits below it and never fires. Lift it back:
def wg as string init mt.allowService($c, "udp", 13231, "wireguard in");
mt.moveFirewallRule($c, $wg, $dropNonLanId); # by idAddressed by the comment handles instead, which survive a rule being re-added:
mt.moveFirewallRuleByComment($c, "wireguard in", "drop everything else");Pass "" as the destination to send a rule to the bottom, and reach for the generic verbs from core.md for the ends of any ordered list:
mt.moveFirewallRuleByComment($c, "log leftovers", ""); # to the bottom
mt.moveRuleToTop($c, mt.FIREWALL_PATH, $establishedId); # the cheap match firstThe same pair exists for the other ordered tables - moveNatRule / moveNatRuleByComment (nat.md), moveMangleRule / moveMangleRuleByComment (mangle.md), and moveRawRule / moveRawRuleByComment (raw.md).
Shortcuts
# accept a service to the router (input chain)
mt.allowService($c, "tcp", 22, "ssh management");
# drop everything a source sends to the router (input chain)
mt.blockAddress($c, "203.0.113.7", "known scanner");Reading back
def rules as list of mt.FirewallRule init mt.firewallRules($c);
for (def fr in $rules) {
io.printf("%s %s %s -> %s %s\n",
$fr.chain, $fr.action, $fr.srcAddress, $fr.dstAddress, $fr.comment);
}removeFirewallRule(c, id) deletes by id when you have one.
Pitfalls
- Order matters and routeros appends. A new rule lands at the end of its chain. If an earlier rule already dropped the traffic, your new accept never fires - fix it with
moveFirewallRule/moveFirewallRuleByComment(see Ordering), not by deleting and re-adding. - Do not lock yourself out. Adding a drop rule on
inputthat matches your own management connection disconnects you. Add your accept rules (e.g.allowService) before broad drops. - A port match (
withDstPort) needs a protocol; icmp has no ports. dropis silent (the sender waits for a timeout);rejectanswers. For internet-facing rules,dropreveals less.- Custom chains are allowed (
firewallRule("mychain", ...)+jumpaction) but managing the jump targets is up to you.