Wheelhouse docs

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:

BadgeMeaning
detection onlyNo queue rule anywhere in firewall ipv4, or no engine container declared.
in the pathThe engine container is declared and at least one rule has action queue, and the container is up.
engine stoppedThe 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):

  1. queue-options bypass on 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.
  2. nfq.fail-open=true on 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 bypass

The 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).

bash
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-options

The 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.log reader, which reads /var/log/suricata/fast.log on 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.

Updated 2026-09-02 manual ids ips nfqueue suricata