简体中文 / [English]


AIO Ep22. Configuring Port Mirroring on PVE Host and Suricata IDS Firewall

 

This article is currently an experimental machine translation and may contain errors. If anything is unclear, please refer to the original Chinese version. I am continuously working to improve the translation.

Background

My HomeLab PVE host has long been running a chaotic mix of workloads — from simple SMB/Syncthing file sharing, various scrapers and databases, to Windows/Linux VMs for development.

Although I’ve already configured proper internal firewall rules in the PVE console, I still notice unexplained and hard-to-trace internet traffic in Netdata monitoring. I’ve used tcpdump to investigate some of it, but it’s time-consuming and tedious.

So I decided to set up enterprise-grade traffic monitoring and analysis capabilities — something that can listen to all internal network traffic, perform DPI (Deep Packet Inspection) to understand application-level protocols, log events, and provide a dashboard for inspection.

An IDS (Intrusion Detection System) is a common feature in such network analysis tools. It automatically flags suspicious traffic based on rules, helping detect misconfigurations or signs of malicious intrusion.


At first, I tried some truly late-night enterprise-grade products, like FortiGate and Sophos free trial VMs. But the experience wasn’t satisfying — their configurations were complex and obscure, and event logging/viewing wasn’t clear enough.

Eventually, I settled on the open-source Suricata paired with the frontend Evebox. While Evebox’s UI is a bit barebones, it basically meets my needs and has very low CPU and memory usage.

Installation

LXC Container and Port Mirroring

Create a Debian 12 LXC container on PVE, name it suricata for example. First, add a normal network interface eth0 attached to vmbr0, and assign it an IP address for regular access to the container.

In the PVE Dashboard, go to Node - System - Network, and create a Linux bridge vmbr1. This bridge will be used solely for traffic mirroring — don’t assign an IP address or gateway, and don’t bind any physical interface yet.

Run the following script on the PVE host to mirror all traffic from VMs, containers, and the host itself to the vmbr1 bridge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
apply_tc_rules() {
local port_name=$1

echo "Applying tc rules to $port_name"
tc qdisc del dev $port_name root
tc qdisc del dev $port_name ingress

tc qdisc add dev $port_name ingress
tc filter add dev $port_name parent ffff: protocol all u32 match u8 0 0 action mirred egress mirror dev vmbr1
tc qdisc add dev $port_name root handle 1: prio
tc filter add dev $port_name parent 1: protocol all u32 match u8 0 0 action mirred egress mirror dev vmbr1
}

apply_tc_to_fwpr_ports() {
local bridge_port="vmbr0"
apply_tc_rules $bridge_port
for port in /sys/class/net/$bridge_port/brif/fwpr*; do
if [ -d "$port" ]; then
port_name=$(basename "$port")
if [[ $port_name == fwpr* ]]; then
apply_tc_rules $port_name
fi
fi
done
}

apply_tc_to_fwpr_ports

The syntax of these tc (traffic control) commands is admittedly bizarre — take for instance tc filter add dev vmbr0 parent ffff: protocol all u32 match u8 0 0 action mirred egress mirror dev vmbr1. Not many commands have such long and cryptic parameter lists. Since I don’t fully understand it myself, I won’t go into detail. Interested readers can look up tc usage — this was the simplest method I found for port mirroring on Linux.

Back to the topic: the apply_tc_rules function mirrors bidirectional traffic from a given port to vmbr1, and apply_tc_to_fwpr_ports applies this rule to vmbr0 and all interfaces bridged to it.

After running the script, all traffic on vmbr0 gets copied to vmbr1. (Note: if new VMs or containers are added later, you’ll need to re-run the script or manually apply the rules to new interfaces.)

Now, add a second network interface eth1 to the suricata container, connected to vmbr1. No IP configuration is needed. This way, inside the container, you can listen to all host traffic via eth1. You can verify this with tcpdump.

Suricata

As mentioned earlier, Suricata is a high-performance, open-source network analysis and threat detection engine. We’ll run it inside the LXC container, listening on eth1.

The official Debian package is outdated, so follow the official guide to compile from source: https://docs.suricata.io/en/latest/install.html. Be sure to run make install-conf to generate default configuration files.

Set up the systemd service file /etc/systemd/system/suricata.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Suricata Intrusion Detection Service
After=syslog.target network-online.target

[Service]
# Environment file to pick up $OPTIONS.
#EnvironmentFile=-/etc/default/suricata
ExecStartPre=/bin/rm -f /usr/local/var/run/suricata.pid
ExecStart=/usr/local/bin/suricata -c /usr/local/etc/suricata/suricata.yaml --af-packet --pidfile /usr/local/var/run/suricata.pid $OPTIONS
ExecReload=/bin/kill -USR2 $MAINPID

[Install]
WantedBy=multi-user.target

Modify the Suricata config file /usr/local/etc/suricata/suricata.yaml as needed:

1
2
3
# (Configuration is long; refer to comments and adjust as needed. Only essential parts shown below)
af-packet:
- interface: eth1 # Change to the interface you want to monitor

Before starting, use suricata-update to download the default set of detection rules provided by Suricata.

Then run systemctl daemon-reload and systemctl enable --now suricata to start the Suricata service. If everything goes smoothly, Suricata should now be running:

1
2
3
4
5
6
# systemctl status suricata
● suricata.service - Suricata Intrusion Detection Service
Loaded: loaded (/etc/systemd/system/suricata.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-07-14 12:59:51 UTC; 1s ago
Process: 2792 ExecStartPre=/bin/rm -f /usr/local/var/run/suricata.pid exited, status=0/SUCCESS)
Main PID: 2793 (Suricata-Main)

By default, Suricata writes events to eve.json under /usr/local/var/log/suricata/. This is a continuously growing JSON file, so you’ll need to set up logrotate yourself.

My /etc/logrotate.d/suricata config for reference:

1
2
3
4
5
6
7
8
9
10
11
12
/usr/local/var/log/suricata/*.log /usr/local/var/log/suricata/*.json
{
daily
rotate 1
missingok
nocompress
create 640 root root
sharedscripts
postrotate
/bin/kill -HUP `cat /usr/local/var/run/suricata.pid 2>/dev/null` 2>/dev/null || true
endscript
}

After setup, test rotation manually with logrotate -f /etc/logrotate.d/suricata.

Suricata installation is now complete. It offers many advanced features — see the official documentation: https://docs.suricata.io/en/latest/what-is-suricata.html

EveBox

EveBox is a frontend that consumes and processes Suricata events.

Download the binary from the official site and place it in /usr/local/bin/evebox.

Set up a systemd service /etc/systemd/system/evebox.service to run EveBox and consume Suricata events:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Unit]
Description=EveBox Server
After=network-online.target suricata.service
Wants=network-online.target
Requires=suricata.service
Documentation=https://evebox.org/docs/

[Service]
Type=simple
ExecStart=/usr/local/bin/evebox server -c /etc/evebox/config.yaml
Restart=on-failure
RestartSec=5s
LimitNOFILE=65535
User=root
Group=root

[Install]
WantedBy=multi-user.target

Create the config file /etc/evebox/config.yaml (full example available here):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# EveBox Server configuration file.
http:
host: 0.0.0.0
tls:
enabled: true

authentication:
required: false

database:
# Database type: elasticsearch, sqlite.
type: sqlite
retention:
# Only keep events for the past 7 days.
days: 7

# Maximum database size.
size: "15 GB"

# The server can process a log file directly, eliminating the need for a
# separate agent if running on the same machine.
input:
# Toggle to disable input without commenting it out.
enabled: true

# Suricata EVE file patterns to monitor and read.
paths:
- "/usr/local/var/log/suricata/eve.json"
- "/usr/local/var/log/suricata/eve.*.json"

Start the service with systemd, and you can access the web interface directly. The JSON data from Suricata will be stored by EveBox in /var/lib/evebox/config.sqlite, automatically rotated based on the 7-day and 15 GB limits.

List of captured eventsList of captured events

A monitoring dashboardA monitoring dashboard

An example TLS event, with different fields identified for various traffic typesAn example TLS event, with different fields identified for various traffic types

This article is licensed under the CC BY-NC-SA 4.0 license.

Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/22-network-ids/
If this article was helpful or interesting to you, consider buy me a coffee¬_¬
Feel free to comment in English below o/