Configuring Traffic Mirroring on Linux with tc-mirred: Copy Packets to IDS/IPS Without Disrupting Service

Network tutorial - IT technology blog
Network tutorial - IT technology blog

I once spent nearly 3 days tracing an intermittent packet loss issue that only appeared during peak hours — around 6 to 8 PM. Everything was perfectly fine during the day, but once that window hit, users would complain nonstop. The problem was that running tcpdump continuously on the production server wasn’t an option — it hit CPU and disk too hard. I needed a more passive approach: capturing traffic without the server even knowing.

That’s when I discovered Traffic Mirroring with tc-mirred. Instead of directly interfering with the traffic flow, you copy all packets to a separate interface and forward them to the IDS machine. The production server is completely unaffected — exactly how a SPAN port works on a switch, but running entirely in software.

What is tc-mirred and How Does It Work?

tc (Traffic Control) is the core Linux kernel tool for managing queues, shaping, and filtering packets. The mirred (Mirror and Redirect) module is a tc action that provides two operating modes:

  • mirror: Copies the packet to another interface while the original continues as normal — this is the mode we use for IDS/IPS
  • redirect: Fully redirects the packet to another interface; the original packet is dropped

High-level architecture of the solution:

[Client] ──→ [eth0: Production server] ──→ [Internet/Backend]
                      │
                      │ (mirror copy, does not affect original traffic)
                      ↓
              [eth1: Monitoring interface]
                      │
                      ↓
              [IDS/IPS: Suricata / Zeek / Wireshark]

The key difference compared to running tcpdump or Wireshark directly on the server: mirroring operates at the kernel level, overhead is very low, and the IDS receives packets in raw form — including packets that were dropped by iptables.

Setting Up the Environment

The server needs at least 2 network interfaces:

  • eth0: The primary interface handling real traffic
  • eth1: The interface connected to the IDS/IPS machine — no IP needed, just needs to be UP

Bring the monitoring interface up:

ip link set eth1 up
ip link show eth1   # Check state: UP

Verify the act_mirred kernel module is loaded:

lsmod | grep mirred

# If no output, load manually:
modprobe act_mirred

Configuring Traffic Mirroring Step by Step

Mirror ingress traffic (incoming)

The most commonly misunderstood part: for ingress traffic, Linux requires adding a special ingress qdisc (handle ffff:) — this is a kernel pseudo-qdisc for processing incoming packets before the routing decision.

# Step 1: Add ingress qdisc to eth0
tc qdisc add dev eth0 ingress

# Step 2: Add mirror filter for all incoming IP traffic
tc filter add dev eth0 parent ffff: \
  protocol ip \
  u32 match u32 0 0 \
  action mirred egress mirror dev eth1

Parameter breakdown:

  • parent ffff:: Attaches the filter to the ingress qdisc
  • u32 match u32 0 0: Matches all packets (bitmask 0 at offset 0 = match everything)
  • action mirred egress mirror dev eth1: Copies a duplicate out to eth1

Mirror egress traffic (outgoing)

Egress requires an extra step — you must create a parent qdisc first, because Linux cannot attach filters to the default noqueue or pfifo_fast:

# Step 1: Replace the default qdisc with prio
tc qdisc add dev eth0 root handle 1: prio

# Step 2: Add mirror filter for egress
tc filter add dev eth0 parent 1: \
  protocol ip \
  u32 match u32 0 0 \
  action mirred egress mirror dev eth1

Script to mirror both directions

The IDS needs to see the full session — both request and response. Here’s a script that handles both ingress and egress mirroring:

#!/bin/bash
# /usr/local/bin/setup-mirror.sh

PROD_IF="eth0"
MONITOR_IF="eth1"

# Ensure monitoring interface is UP
ip link set $MONITOR_IF up

# === Ingress mirror (incoming traffic) ===
tc qdisc add dev $PROD_IF ingress
tc filter add dev $PROD_IF parent ffff: \
  protocol ip u32 match u32 0 0 \
  action mirred egress mirror dev $MONITOR_IF

# === Egress mirror (outgoing traffic) ===
tc qdisc add dev $PROD_IF root handle 1: prio
tc filter add dev $PROD_IF parent 1: \
  protocol ip u32 match u32 0 0 \
  action mirred egress mirror dev $MONITOR_IF

echo "[OK] Traffic mirroring active: $PROD_IF → $MONITOR_IF"

Verifying mirroring is active

# Show all qdiscs on eth0
tc qdisc show dev eth0

# Show ingress filters
tc filter show dev eth0 ingress

# Show egress filters
tc filter show dev eth0

# Monitor packet counters (run a few times — Sent count should increase)
tc -s filter show dev eth0 ingress

On the IDS machine, use tcpdump to confirm packets are being received:

tcpdump -i eth1 -n -c 50
# You should see traffic similar to capturing directly on eth0

Practical Tips for Using tc-mirred

Selective mirroring by port or IP

When the server has high throughput, mirroring everything can overwhelm the monitoring interface. Filtering by port or source IP significantly reduces the load:

# Mirror only TCP port 443 (ingress)
tc filter add dev eth0 parent ffff: \
  protocol ip \
  u32 match ip dport 443 0xffff \
  action mirred egress mirror dev eth1

# Mirror only traffic from a specific IP
tc filter add dev eth0 parent ffff: \
  protocol ip \
  u32 match ip src 10.0.0.100/32 \
  action mirred egress mirror dev eth1

Cleaning up when stopping mirroring

#!/bin/bash
# /usr/local/bin/cleanup-mirror.sh

PROD_IF="eth0"

# Remove ingress qdisc (takes all filters with it)
tc qdisc del dev $PROD_IF ingress 2>/dev/null

# Remove root qdisc; kernel restores defaults automatically
tc qdisc del dev $PROD_IF root 2>/dev/null

echo "[OK] Mirroring stopped, defaults restored"

Auto-restart after reboot with systemd

tc configuration does not persist across reboots — that’s something I got caught out by the first time. Create a systemd service to automate it:

# /etc/systemd/system/traffic-mirror.service
[Unit]
Description=Traffic Mirroring via tc-mirred
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/setup-mirror.sh
ExecStop=/usr/local/bin/cleanup-mirror.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
chmod +x /usr/local/bin/setup-mirror.sh /usr/local/bin/cleanup-mirror.sh
systemctl daemon-reload
systemctl enable --now traffic-mirror
systemctl status traffic-mirror

Conclusion

tc-mirred is one of those underrated tools that solves a real-world problem that other approaches can’t: passive monitoring with zero interference with real traffic and no need for switch hardware that supports SPAN ports.

After that packet loss debugging session, I set up Suricata running in passive IDS mode on eth1 — receiving mirrored traffic, analyzing and alerting, but never touching production. The result: we identified a clear pattern where certain clients were retransmitting excessively during peak hours due to an aging switch being overloaded at the access layer. Without mirroring, that pattern would have been invisible.

One practical note: mirroring does add a small CPU overhead — typically under 2% on modern servers. However, if your interface is handling very high throughput (10 Gbps+), benchmark first before applying to production to avoid any surprises.

Share: