Skip to content

Mangle: Packet Marking and Traffic Classification

Mangle: Packet Marking and Traffic Classification

Section titled “Mangle: Packet Marking and Traffic Classification”

The RouterOS mangle table is the classification layer between the network and your policies. It does not accept or drop packets — instead it marks them so that other subsystems (queues, routing, NAT) can act on those marks. A mangle rule that matches a packet can assign a connection-mark, packet-mark, or routing-mark, or it can rewrite header fields such as DSCP or TTL.

Mangle is the foundation for:

  • QoS — packet marks drive queue-tree priorities and rates
  • Policy-based routing (PBR) — routing marks select which routing table a packet uses
  • DSCP remarking — modify the DSCP/ToS byte for interoperability with upstream equipment

Mark all DNS traffic for a priority queue and route VoIP via a secondary gateway:

# Mark DNS connections and their packets
/ip firewall mangle
add chain=prerouting protocol=udp dst-port=53 \
action=mark-connection new-connection-mark=dns-conn passthrough=yes
add chain=prerouting connection-mark=dns-conn \
action=mark-packet new-packet-mark=dns-pkt passthrough=no
# Mark VoIP for policy routing
add chain=prerouting protocol=udp dst-port=5060,10000-20000 \
action=mark-routing new-routing-mark=voip-table passthrough=no

Mangle has five chains, each running at a different point in the packet flow:

ChainTraversed byTypical use
preroutingAll incoming packets, before routingClassify inbound traffic; PBR routing marks
inputPackets destined for the router itselfMark traffic to the router
forwardPackets routed through the routerMark transit traffic
outputPackets originating from the routerMark router-generated traffic
postroutingAll outgoing packets, after routingFinal classification before egress
  • PBR routing marks — must be set in prerouting (before the routing lookup)
  • QoS packet marksprerouting or forward both work; use forward to skip locally-generated traffic
  • Upload shapingpostrouting catches packets as they leave, ensuring marks are set after NAT
  • Router’s own trafficoutput chain

Attaches a mark to the connection tracking entry. All subsequent packets in the same connection inherit this mark, which you can then match with connection-mark.

/ip firewall mangle add \
chain=prerouting \
src-address=192.168.10.0/24 \
action=mark-connection \
new-connection-mark=lan-conn \
passthrough=yes

mark-connection is almost always used with passthrough=yes so that a mark-packet rule can run immediately after.

Attaches a mark to the individual packet. Queue trees and simple queues match on packet-mark.

/ip firewall mangle add \
chain=prerouting \
connection-mark=lan-conn \
action=mark-packet \
new-packet-mark=lan-pkt \
passthrough=no

Sets the routing table used for the forwarding lookup. The named table must exist under /ip route with the matching routing-table value.

/ip firewall mangle add \
chain=prerouting \
src-address=192.168.20.0/24 \
action=mark-routing \
new-routing-mark=isp2-table \
passthrough=no

Rewrites the 6-bit DSCP field in the IP header. Use this to enforce a DSCP policy at the network edge or to remark traffic before it leaves your network.

# Remark VoIP to EF (Expedited Forwarding, DSCP 46)
/ip firewall mangle add \
chain=postrouting \
protocol=udp \
dst-port=10000-20000 \
action=change-dscp \
new-dscp=46

Modifies the IP TTL value. Useful for hiding the hop count behind a CPE or for forcing packets to expire at a specific point.

/ip firewall mangle add \
chain=postrouting \
out-interface=ether1 \
action=change-ttl \
new-ttl=set:64

new-ttl accepts set:<value>, increment:<n>, or decrement:<n>.

Offloads an established connection to the FastPath, bypassing most firewall and queue processing. Primarily used in the filter table but also available in mangle.

fasttrack-connection in mangle bypasses queue processing. Do not use it on connections you intend to shape with queues.

Removes IP options from the packet header. Used to prevent IP source routing and similar abuses.

/ip firewall mangle add \
chain=prerouting \
action=strip-ipv4-options \
passthrough=yes

RouterOS connection tracking assigns every packet a connection-state: new, established, related, or invalid. Exploiting this in mangle rules avoids running expensive matchers (port checks, protocol inspection, address lists) on every packet.

Mark the connection once — on the first (new) packet — then derive the packet mark from the connection mark on all subsequent packets:

/ip firewall mangle
# Step 1: Mark the CONNECTION only on new packets
# (runs once per connection, not on every packet)
add chain=prerouting connection-state=new protocol=tcp dst-port=443 \
action=mark-connection new-connection-mark=https-conn passthrough=yes
# Step 2: Mark every PACKET using the connection mark
# (simple lookup — no protocol/port re-evaluation)
add chain=prerouting connection-mark=https-conn \
action=mark-packet new-packet-mark=https-pkt passthrough=no

Without connection-state=new, the mark-connection rule fires on every packet — unnecessary since the connection tracking entry is already marked after the first packet.

ApproachCPU costWhen mark is set
mark-connection + connection-state=newLow — runs once per connectionFirst packet only
mark-connection without state filterMedium — runs on every packetEvery packet (redundant after first)
mark-packet with port matchers, no connection markHigh — evaluates all matchers per packetEvery packet

In asymmetric routing scenarios (where reply packets take a different path), connection-state=established,related may never be seen on a given router. In that case, mark every packet with port matchers rather than relying on connection tracking:

# Asymmetric: use direct packet-mark on every packet
add chain=prerouting protocol=tcp dst-port=443 \
action=mark-packet new-packet-mark=https-pkt passthrough=no

To mark the return direction of a connection, match on connection-mark in a separate chain. Because the destination of the reply is the original source, use forward or postrouting depending on where you need the mark:

# Mark outbound VoIP connections
add chain=prerouting connection-state=new protocol=udp dst-port=10000-20000 \
action=mark-connection new-connection-mark=voip-conn passthrough=yes
# Mark packets in both directions
add chain=prerouting connection-mark=voip-conn \
action=mark-packet new-packet-mark=voip passthrough=no
add chain=postrouting connection-mark=voip-conn \
action=mark-packet new-packet-mark=voip passthrough=no

Every mangle action has a passthrough flag that controls whether rule evaluation continues after a match:

passthroughBehaviour
yesSet mark and continue evaluating subsequent rules
noSet mark and stop — no further mangle rules are evaluated for this packet

Use passthrough=yes on mark-connection rules so that a mark-packet rule can fire on the same packet. Use passthrough=no as the final action to avoid unnecessary rule checks.

# Pattern: connection mark with passthrough=yes, packet mark with passthrough=no
add chain=prerouting protocol=tcp dst-port=443 \
action=mark-connection new-connection-mark=https-conn passthrough=yes
add chain=prerouting connection-mark=https-conn \
action=mark-packet new-packet-mark=https-pkt passthrough=no

Mangle shares all matchers with the firewall filter. The most commonly used in classification rules:

MatcherExample valueNotes
src-address192.168.1.0/24Source IP or subnet
dst-address10.0.0.0/8Destination IP or subnet
protocoltcp, udp, icmpIP protocol
src-port / dst-port80, 443, 5060-5090Requires protocol set
in-interfaceether1, vlan10Ingress interface
out-interfaceether2Egress interface (postrouting/output)
connection-stateestablished,relatedConnection tracking state
connection-markvoip-connMatch on a previously set connection mark
packet-markbulk-pktMatch on a previously set packet mark
dscp46DSCP value (0–63)
packet-size0-100Bytes, useful for marking small (interactive) packets
layer7-protocol<l7 protocol name>Deep-packet inspection — high CPU cost

Mark traffic into three priority classes for use with a queue tree:

/ip firewall mangle
# VoIP — highest priority
add chain=prerouting protocol=udp dst-port=5060,10000-20000 \
action=mark-connection new-connection-mark=voip-conn passthrough=yes
add chain=prerouting connection-mark=voip-conn \
action=mark-packet new-packet-mark=voip passthrough=no
# Interactive — medium priority (SSH, DNS, HTTP/S small packets)
add chain=prerouting protocol=tcp dst-port=22,80,443 \
packet-size=0-512 \
action=mark-connection new-connection-mark=interactive-conn passthrough=yes
add chain=prerouting connection-mark=interactive-conn \
action=mark-packet new-packet-mark=interactive passthrough=no
# Bulk — everything else (default, mark-packet=bulk-pkt)
add chain=prerouting \
action=mark-packet new-packet-mark=bulk passthrough=no

Then reference the marks in a queue tree:

/queue tree
add name=voip parent=global packet-mark=voip priority=1
add name=interactive parent=global packet-mark=interactive priority=4
add name=bulk parent=global packet-mark=bulk priority=8

Route traffic from one subnet via ISP2, all other traffic via ISP1 (default route):

# Create a routing table for ISP2
/routing table add name=isp2-table fib
# Add ISP2 default route in that table
/ip route add dst-address=0.0.0.0/0 gateway=203.0.113.1 routing-table=isp2-table
# Mangle: mark LAN2 traffic to use isp2-table
/ip firewall mangle add \
chain=prerouting \
src-address=192.168.2.0/24 \
action=mark-routing \
new-routing-mark=isp2-table \
passthrough=no

Honour DSCP from trusted internal hosts, remark untrusted external traffic to Best Effort (DSCP 0):

# Trust internal DSCP — no action needed, leave it
# Remark inbound external traffic to BE
/ip firewall mangle add \
chain=prerouting \
in-interface=ether1-wan \
action=change-dscp \
new-dscp=0 \
comment="Remark untrusted inbound to Best Effort"
# Re-mark internal VoIP to EF before egress
/ip firewall mangle add \
chain=postrouting \
src-address=192.168.1.0/24 \
protocol=udp dst-port=10000-20000 \
action=change-dscp new-dscp=46 \
comment="EF for VoIP egress"

Mark upload connections in postrouting (after NAT, so correct source addresses are visible):

/ip firewall mangle
add chain=postrouting out-interface=ether1-wan \
src-address=192.168.0.0/16 \
action=mark-connection new-connection-mark=upload-conn passthrough=yes
add chain=postrouting connection-mark=upload-conn \
action=mark-packet new-packet-mark=upload-pkt passthrough=no

PCC (Per Connection Classifier) hashes selected packet header fields — source address, destination address, and/or ports — to distribute connections evenly across two or more uplinks while keeping each individual connection pinned to a single WAN interface (avoiding TCP resets from IP changes mid-flow).

The pattern is:

  1. Mark inbound WAN connections to their respective ISP (for reply symmetry)
  2. Use PCC to split new LAN-originated connections 50/50 between ISPs
  3. Convert connection marks to routing marks
  4. Install per-ISP default routes in dedicated routing tables

Assumptions: ether1 = WAN1 (gateway 1.1.1.1), ether2 = WAN2 (gateway 2.2.2.1), bridge = LAN (192.168.88.0/24).

# Step 1: Create routing tables
/routing table
add name=to-wan1 fib
add name=to-wan2 fib
# Step 2: Install per-table default routes with gateway health check
/ip route
add dst-address=0.0.0.0/0 gateway=1.1.1.1 routing-table=to-wan1 check-gateway=ping
add dst-address=0.0.0.0/0 gateway=2.2.2.1 routing-table=to-wan2 check-gateway=ping
# Main table routes (for router itself and fallback)
add dst-address=0.0.0.0/0 gateway=1.1.1.1 distance=1 check-gateway=ping
add dst-address=0.0.0.0/0 gateway=2.2.2.1 distance=2 check-gateway=ping
# Step 3: Mangle — mark inbound WAN connections for reply symmetry
/ip firewall mangle
add chain=prerouting in-interface=ether1 connection-state=new \
action=mark-connection new-connection-mark=conn-wan1 passthrough=yes \
comment="Tag connections entering via WAN1"
add chain=prerouting in-interface=ether2 connection-state=new \
action=mark-connection new-connection-mark=conn-wan2 passthrough=yes \
comment="Tag connections entering via WAN2"
# Step 4: Mangle — PCC split for LAN-originated new connections
add chain=prerouting in-interface=bridge connection-state=new \
dst-address-type=!local \
per-connection-classifier=both-addresses-and-ports:2/0 \
action=mark-connection new-connection-mark=conn-wan1 passthrough=yes \
comment="PCC bucket 0 -> WAN1"
add chain=prerouting in-interface=bridge connection-state=new \
dst-address-type=!local \
per-connection-classifier=both-addresses-and-ports:2/1 \
action=mark-connection new-connection-mark=conn-wan2 passthrough=yes \
comment="PCC bucket 1 -> WAN2"
# Step 5: Mangle — convert connection marks to routing marks
add chain=prerouting in-interface=bridge \
connection-mark=conn-wan1 action=mark-routing \
new-routing-mark=to-wan1 passthrough=no
add chain=prerouting in-interface=bridge \
connection-mark=conn-wan2 action=mark-routing \
new-routing-mark=to-wan2 passthrough=no
# Step 6: Apply same routing marks to router-originated traffic
add chain=output connection-mark=conn-wan1 \
action=mark-routing new-routing-mark=to-wan1 passthrough=no
add chain=output connection-mark=conn-wan2 \
action=mark-routing new-routing-mark=to-wan2 passthrough=no
# Step 7: NAT — masquerade per WAN interface
/ip firewall nat
add chain=srcnat out-interface=ether1 action=masquerade
add chain=srcnat out-interface=ether2 action=masquerade

How check-gateway=ping provides failover: When a gateway stops responding to pings, RouterOS marks its routes as inactive. Traffic marked for the dead ISP’s routing table finds no active default route in that table and falls back to the main table, using the surviving ISP.

How Mangle Interacts with Queues and Routing

Section titled “How Mangle Interacts with Queues and Routing”

Simple queues and queue trees match on packet-mark. The mark is checked at the point where the packet enters the queue:

  • Simple queue — evaluated top-down; first matching packet-mark wins
  • Queue tree — hierarchical; parent queues aggregate child queues by packet mark

Ensure packet marks are set before the queuing point in the packet flow. prerouting and forward marks are visible to both simple queues and queue trees on the router.

mark-routing selects a named routing table (/routing table). This runs after the prerouting mangle chain and before the main routing lookup. If no route is found in the named table, the packet falls back to the main table unless fib was explicitly configured without a fallback.

Check rule order — mangle rules are evaluated top-down. A passthrough=no rule earlier in the chain can prevent later rules from running.

# View mangle rules with statistics
/ip firewall mangle print stats

Look at the bytes and packets columns. A rule with zero packets has never matched.

Verify:

  1. The rule is in prerouting (not forward)
  2. The routing table exists: /routing table print
  3. A route exists in that table: /ip route print where routing-table=<name>
# Check if the routing mark is being applied
/ip firewall mangle print stats where action=mark-routing

If FastTrack is enabled in the filter table, marked connections are bypassed at the FastTrack stage — the queue never sees them. Either disable FastTrack for connections you want to shape, or use connection-mark=no-mark as a FastTrack guard:

# Only FastTrack unmarked connections
/ip firewall filter add chain=forward \
connection-state=established,related \
connection-mark=no-mark \
action=fasttrack-connection

Inspect the connection table to confirm marks are being set:

/ip firewall connection print where connection-mark~"."

This shows all tracked connections that have a non-empty connection mark.

mark-routing only affects the routing lookup if it is set before that lookup runs. The routing lookup happens after prerouting mangle. Placing mark-routing in forward or postrouting has no effect on which table is used — the routing decision is already made.

Chainmark-routing works?Reason
preroutingYes (forwarded traffic)Before routing lookup
outputYes (router-originated)Before routing lookup for local traffic
forwardNoAfter routing lookup
postroutingNoAfter routing lookup

passthrough=yes on a terminal marking rule allows subsequent rules to overwrite the mark. This leads to silent re-marking bugs that are hard to diagnose. Use passthrough=no on the last rule that sets a mark for a given traffic class. Reserve passthrough=yes for mark-connection rules that are immediately followed by a mark-packet rule.

# Wrong: passthrough=yes on terminal rule — later rules can overwrite the mark
add chain=prerouting protocol=tcp dst-port=443 \
action=mark-packet new-packet-mark=https passthrough=yes # BUG
# Correct: passthrough=no stops evaluation after the mark is set
add chain=prerouting protocol=tcp dst-port=443 \
action=mark-packet new-packet-mark=https passthrough=no # OK

Missing connection-state=new on mark-connection

Section titled “Missing connection-state=new on mark-connection”

Without connection-state=new, a mark-connection rule fires on every packet in the connection — even though the connection tracking entry is already marked after the first packet. This adds unnecessary CPU work on high-throughput links.

# Inefficient: runs the matcher on every packet
add chain=prerouting protocol=udp dst-port=5060 \
action=mark-connection new-connection-mark=sip-conn passthrough=yes
# Efficient: matcher runs once per connection
add chain=prerouting connection-state=new protocol=udp dst-port=5060 \
action=mark-connection new-connection-mark=sip-conn passthrough=yes

FastTrack bypasses mangle marks and queues

Section titled “FastTrack bypasses mangle marks and queues”

When FastTrack is active in the filter table, connections offloaded to the FastPath bypass mangle processing entirely — queue trees never see those packets. FastTrack also only works with the main routing table, so policy-routed connections that are fasttracked will silently use the wrong gateway.

# Guard: only FastTrack connections with no routing or queue marks
/ip firewall filter
add chain=forward connection-state=established,related \
connection-mark=no-mark action=fasttrack-connection
add chain=forward connection-state=established,related action=accept

RouterOS enforces a hard limit of 4096 unique packet marks. Exceeding this limit produces a bad new packet mark error and the rule is silently ignored. Use a flat, small set of named marks (e.g., voip, interactive, bulk) rather than per-host or per-port marks.

Asymmetric routing and invalid-state drops

Section titled “Asymmetric routing and invalid-state drops”

A common firewall template includes:

/ip firewall filter add chain=forward connection-state=invalid action=drop

In asymmetric routing topologies (where forward and return paths differ), valid return packets may appear invalid to the router because it never saw the SYN. Dropping them silently breaks those flows. Remove or narrow this rule when asymmetric routing is intentional.