Inline IPS#
The IDS watches copies of traffic and cannot drop. Inline prevention needs the engine in the packet path, which on Linux means NFQUEUE: a firewall rule hands matched packets to a kernel queue, a userspace Suricata verdicts each one, and the packets it accepts continue. The Inline IPS panel at the top of the IDS page reports whether that arrangement exists, builds the plan that creates it, and takes it apart again. All of it is ordinary configuration — a container declaration and a firewall rule — so it diffs, commits and rolls back like everything else, and it works even when the agent is running off-router.
What the panel shows#
The panel header carries one of three badges, computed by
agent/ids.go ipsStatus:
| Badge | Meaning |
|---|---|
| detection only | No queue rule anywhere in firewall ipv4, or no engine container declared. |
| in the path | The engine container is declared and at least one rule has action queue, and the container is up. |
| engine stopped | The same, with the container declared but not running. |
When it is in the path, a key-value block lists the engine's state and every queue rule as
chain/rule, marking any that lacks bypass.
A queue rule without bypass gets a red band of its own: if the engine stops, that traffic
is dropped. That is the failure this design exists to avoid, and the panel names the rules
that have it.
The two fail-open layers#
Both are deliberate, and both were verified on the bench on 2026-08-29
(agent/ids.go):
queue-options bypasson the firewall rule. With no consumer on the queue, traffic still flows. Without it, the same rule black-holes the traffic when nothing is reading the queue.nfq.fail-open=trueon the engine. The container is started with-q <n> --set nfq.mode=accept --set nfq.fail-open=true.
Between them, a stopped or crashed engine passes traffic rather than blocking it. That is the right trade for a router: an IPS that takes the link down when it dies is a worse outage than the attack it was watching for.
The two ways in#
The Enable inline IPS button opens a chooser with two modes, and the safe one is first.
Route an accept rule (safe)#
The list offers every rule in firewall ipv4 forward filter whose action is accept,
labelled with its description or its match. Converting one re-points traffic that is
already permitted through the engine: same match, same permitted traffic, now inspected.
No new access is opened, because a terminal queue rule that then accepts is exactly the
accept it replaced.
The plan for converting rule 100:
set container name suricata-ips image docker.io/jasonish/suricata:latest
set container name suricata-ips allow-host-networks
set container name suricata-ips capability net-admin
set container name suricata-ips capability net-raw
set container name suricata-ips command '-q 0 --set nfq.mode=accept --set nfq.fail-open=true'
set container name suricata-ips volume log source /config/apps/suricata-ips/log
set container name suricata-ips volume log destination /var/log/suricata
set firewall ipv4 forward filter rule 100 action queue
set firewall ipv4 forward filter rule 100 queue 0
set firewall ipv4 forward filter rule 100 queue-options bypassThe container operations are omitted when container name suricata-ips already exists, so
adding a second queue rule later stages only the firewall half.
New scoped rule#
The alternative adds a fresh queue rule matching all inbound traffic on an interface you name. The mode's own text warns against it, and the plan comes back with two warnings the panel prints in full:
- a queue rule is terminal, so matched traffic the engine accepts passes and does not meet later firewall rules — scope it to traffic you already permit, or convert an existing accept rule instead;
- Suricata accepts by default until a ruleset is loaded, so an interface-wide queue rule effectively permits that traffic until you load one.
The new rule takes the next free number in the chain — ten past the highest existing rule.
A third case exists: asking for a plan with neither a rule nor an interface stages the engine only, and says so — nothing is inspected until something routes traffic to the queue.
The plan endpoint#
The panel does not apply anything. It calls POST /api/ids/ips/plan, which returns the
operations, the same operations rendered as commands, the warnings, the image name, whether
the image is already on the router, and the log directory. The route is writeable —
operator role — and it changes nothing by itself
(agent/ids.go handleIPSPlan,
agent/main.go).
curl -sk -X POST https://<router>:8443/api/ids/ips/plan \
-H "Authorization: Bearer wh_…" -H 'Content-Type: application/json' \
-d '{"queue":0,"chain":"forward","rule":"100"}'Stage IPS puts the returned operations in the staging area and reminds you to use commit-confirm.
Why the engine is a container#
VyOS drives its own Suricata in af-packet mode, which is detection. The inline engine is
therefore a separate process, and it is declared as a container in the configuration tree so
that it diffs, commits, rolls back and survives an image upgrade like every other object —
and so the whole flow works from an agent that is not running on the router. It runs with
host networking and the net-admin and net-raw capabilities, which is what lets it reach
the host's netfilter queue.
One detail is a fix rather than a choice: the arguments are passed as command, not as
arguments. VyOS' quadlet backend emits arguments only when a command is set too, so on
that backend the engine started with the image's default entrypoint and never attached to
the queue — while the status here said it was in the path. command works on both backends.
Its logs are bind-mounted to /config/apps/suricata-ips/log on the router.
Taking it out#
Disable IPS stages, for each queue rule:
delete container name suricata-ips
set firewall ipv4 forward filter rule 100 action accept
delete firewall ipv4 forward filter rule 100 queue
delete firewall ipv4 forward filter rule 100 queue-optionsThe rule goes back to a plain accept rather than losing its action: for a converted rule
that is what it was before, and for a rule the IPS flow created it is the harmless form
until you delete it. Deleting the action outright left a rule with none, which the router
refuses to commit. The toast after staging says exactly that, and tells you to delete any
rule the IPS flow created.
Read the diff before committing. A rule that reverts to accept is permitting traffic that
was, a moment ago, being inspected.
Limits worth knowing#
- IPv4 only. Every path the plan writes is under
firewall ipv4. - One queue number. The panel asks for queue 0 and the engine is started with
-q 0. - Status is inferred from configuration, not from the engine. In the path means a queue rule exists and the container is declared; whether Suricata has a ruleset loaded is a separate question, and an engine with no rules accepts everything.
- Alerts still come from the IDS page's
fast.logreader, which reads/var/log/suricata/fast.logon the router — the inline engine's own logs are under/config/apps/suricata-ips/log.
See also#
- IDS — the detection engine, its groups and its alerts.
- Firewall — rules — the chain the queue rule lives in.
- Worked rulesets — the accept rules that are the safe conversion targets.
- Apps — pulling the engine image before you commit.
- Commit-confirm — use it for this change.
Checked against#
agent/ids.go,
ui/src/pages/Ids.tsx,
agent/apps.go,
agent/main.go,
PLAN.md.