- NFQUEUE allows Netfilter to delegate filtering and marking decisions to user-space processes, enabling dynamic IP firewalls and routers.
- Suricata provides a multi-process IDS/IPS engine with support for NFQUEUE, AF_PACKET and rules compatible with Snort and Emerging Threats.
- Integrating NFQUEUE with Suricata, databases, Memcached or Pfsense allows you to build advanced security and routing solutions with free software.
- Performance depends largely on thread design and user-space logic, making it key to optimize and carefully select the traffic to be inspected.

If you work with networks on GNU/Linux ( best Linux distributions for security and privacy ) and are interested in going beyond the typical static firewall, you're probably curious about how to combine Netfilter, NFQUEUE, and Suricata to build a truly flexible IDS/IPS without spending a fortune on proprietary hardware. That's precisely the area we'll explore in this article, blending low-level elements (kernel, queues, C) with high-level tools (Suricata, rules, MySQL, Memcached, pfSense).
The underlying idea is very powerful: leverage the fact that the kernel (see how to optimize the Linux kernel ) can queue packets into user space and let a custom program decide what to do with them. This can be used for traffic filtering (advanced firewalling, IPS), dynamic routing, or integrating business logic (databases, caches, web application attack detection, VoIP, etc.). And if we add Suricata as a multi-process IDS/IPS engine, we have a very robust combination for environments ranging from labs to high-traffic data centers.
NFQUEUE and Netfilter: elevating the firewall to user space
In a typical GNU/Linux system, Netfilter/iptables (or nftables) rules are usually used as static policies that reside entirely in kernel space . Frontends and appliances (including many solutions based on Netfilter or BSD's Packet Filter) store the configuration in text, XML, or SQLite files, and when something changes, they regenerate and reload the rules. This is flexible, but the logic remains a kind of snapshot of the firewall with minor dynamic tweaks (connection-per-second limits, conntrack, country matching, layer 7 if available, etc.).
What NFQUEUE proposes is a game-changer: instead of the kernel always making the final decision, we can delegate that decision to a user process . The kernel queues the packet in a numbered queue, and an application using the libnetfilter_queue library retrieves it, analyzes it, and returns a verdict: accept, discard, or even mark it for policy routing. It's like having a programmable "judge" written in C, Python, or Perl on top of the firewall.
The beauty of this is that our program can literally do whatever we want: query /dev/urandom, a database, a web service, a distributed cache, or a sophisticated algorithm before responding to the kernel. In architectural terms, the firewall ceases to be a simple set of static rules and becomes a pipeline where Netfilter, queues, and user applications fit together like pieces of a puzzle.
NFQUEUE consists of two parts: the NFQUEUE target in iptables , which sends packets to a specific queue, and the libnetfilter_queue user library , which allows you to read those packets and issue a verdict. It's not a simple sniffer like tcpdump: here we have the ability to directly decide on the path the packet takes.
Basic iptables configuration with NFQUEUE
From an iptables point of view, using NFQUEUE is quite straightforward: you add a rule to the chain you are interested in to send to the queue the packets that meet certain criteria (source/destination IP, ports, states, extra modules such as GeoIP or layer7 if available, etc.).
For example, if we want to send to NFQUEUE all pings that arrive at the host itself:
iptables -I INPUT -p icmp -j NFQUEUE
This sends incoming ICMP packets to queue 0 (unless otherwise specified). We could specify another queue with something like `--queue-num 3` . When listing the rules with counters (`iptables -L -n -v -x`), we'll see the counters increase, indicating that packets are being queued . An important detail: if there are packets in the queue and no user process retrieves and processes them, the default behavior is to deny them, so an application failure, by design, results in traffic being blocked.
Programming against libnetfilter_queue: the “hello world” in C
To join the queue from user space, the libnetfilter_queue library is used (which in turn relies on libnfnetlink). On distributions like Debian, simply install the development packages:
apt-get install libnfnetlink-dev libnetfilter-queue-dev gcc
The skeleton of a minimal program that always accepts packets consists of a few very clear steps: open the library, unbind any existing handlers, bind to the AF_INET protocol, create the queue with a callback function, define the copy mode, and enter a receive loop . The callback is executed for each queued packet, extracting the ID and returning the verdict.
In practice, the flow is something like this: `nfq_open` to get the handle, `nfq_unbind_pf` to clean it up, `nfq_bind_pf` to associate with AF_INET, `nfq_create_queue` to register the callback in queue 0, `nfq_set_mode` to indicate whether we want the metadata or the entire packet, a loop with `recv()` over the descriptor, and `nfq_handle_packet` to process each packet . Upon exiting, the queue is destroyed with `nfq_destroy_queue` and the handle is closed with `nfq_close`.
This type of "hello world" allows for a clean measurement of the impact of NFQUEUE. If we compile the example with something like:
gcc -o nftest code.c -lnfnetlink -lnetfilter_queue
And if we queue the traffic from an iperf (for example, TCP port 5001 in INPUT and OUTPUT), we'll see that code that simply accepts packets barely affects performance on a gigabit network . However, there's a key detail: printing information in the callback (printf, fflush, etc.) significantly penalizes throughput, as can be seen when comparing iperf with and without screen debugging.
Advanced NFQUEUE options: bypass, balance, and fail-open
NFQUEUE incorporates several interesting iptables options that modify the default behavior of queues and that should be known before entering a production or high-performance environment, since they affect how user application failure or queue filling is handled.
The `--queue-bypass` command allows you to ensure that if no process is listening to the queue, packets are not dropped but instead forwarded to the next hop in the iptables chain. This can be useful if you want the system to "fail open" when the user service is unavailable, although from a security perspective it's a double-edged sword.
The `--queue-balance` option allows you to distribute packets across a range of queues (for example, from 0 to 3) and then have multiple independent processes or threads consuming from each queue . Netfilter's code ensures that packets from the same flow always end up in the same queue, which greatly simplifies maintaining consistency in the decision logic.
There's also the `--fail-open` mode , which controls what happens when the queue fills up because the user process is running too slowly. Enabling it causes the kernel to accept packets directly instead of dropping them, preventing massive traffic disruptions. Again, this can be a security issue, because if we want to make decisions on a case-by-case basis, losing decision packets means failing to meet that objective.
To monitor what is happening with the queues, Netfilter exposes information in the pseudo-fs /proc/net/netfilter/nfnetlink_queue , which can be easily queried from scripts or monitoring tools.
Integrating business logic: testing with Memcached and MySQL
Once the "hello world" process is under control, the next natural step is to enrich the callback with calls to external systems . A typical experiment involves deciding whether to accept or reject a packet based on whether the source IP address appears in any backend, such as a MySQL database or a Memcached cache.
In the case of Memcached, the daemon is installed (apt-get install memcached) and a key, for example authorized , is loaded with the IP address we're interested in. We can do this with a simple echo and netcat, and then verify with a get command that the value is correctly stored. From there, the NFQUEUE program, in addition to obtaining the packet ID, receives the entire packet using NFQNL_COPY_PACKET , extracts the IP header (struct iphdr), and converts the source address to a string with inet_ntop.
To avoid wasting time opening connections with each packet, the Memcached connection is initialized only once in the main method (memcached_create, memcached_server_list_append, memcached_server_push), and the handler is stored in global variables. In the callback, memcached_get is called with the desired key, the source IP address is compared to the retrieved value, and if they match, NF_ACCEPT is returned; otherwise, NF_DROP is returned. If the key does not exist or there is an error, the packet is dropped as a conservative policy.
Using iperf, this strategy reduces throughput to approximately 140 Mbit/s on a gigabit network , and it's observed that the queue begins to experience losses (indicated, for example, by symbols within the code itself). In other words, simply calling a cache service per packet already incurs a significant cost, although it remains viable for medium traffic volumes if optimized.
With MySQL, the approach is similar but more complex: the server and client library are installed, a database (for example, nfqueue) is created with a simple table called authorized(ip varchar(50)), and the allowed IP address is inserted. In the program, `mysql_init` and `mysql_real_connect` are run at startup, and in the callback, a query like `select * from authorized where ip like 'xxxx'` is constructed. If the query executes successfully and a row is found, the package is accepted; otherwise, it is discarded.
With MySQL query caching enabled, tests yield around 188 Mbit/s , which drops to 103 Mbit/s when query caching is disabled. These figures, while far from gigabits, demonstrate that even with the least elegant approach (single-threaded, without optimization) , respectable traffic volumes can be handled using database-driven or caching-based decisions.
Performance, multithreading, and CPU usage
Tests with iperf, Memcached, and MySQL clearly show that the performance ceiling isn't imposed so much by NFQUEUE itself as by the logic we add in user space and how we implement it. An executable that only returns NF_ACCEPT achieves almost a gigabit without breaking a sweat; as soon as we introduce I/O or network calls, the throughput drops, and the CPU of the NFQUEUE machine, the Memcached daemon, or MySQL is pushed to its limits.
From an architectural standpoint, this has two implications. On the one hand, it confirms that delegating firewall decisions to user applications for significant traffic volumes is perfectly viable , provided the actual costs of each call are taken into account. On the other hand, it demonstrates that to approach the platform's maximum capabilities , multithreading or multiprocessing must be considered . NFQUEUE allows traffic to be distributed across multiple queues; we could launch several copies of our app, each listening to a different queue, and leverage multiple cores without the hassle of pthreads or massive forks.
Another obvious optimization would be to limit which traffic passes through NFQUEUE . In the tests, the entire iperf flow was being queued, but in a real-world scenario, we could queue only packets with a NEW state, allow ESTABLISHED/RELATED packets to pass through, and reserve the expensive logic for logins or suspicious patterns.
Ultimately, CPU usage and thread design are key: if the user process falls short, the queue fills up and we have to resort to things like fail-open or accept drops, losing some of the fine control that this approach aims for.
Dynamic routing with Netfilter branding
NFQUEUE is not limited to simply saying "accept" or "throw." It can also be used to apply Netfilter (fwmark) flags to packets and combine them with ip rule and iproute2 to create highly flexible, almost lightweight VRF-style political routing schemes.
The procedure, broadly speaking, would be: define several routing tables in /etc/iproute2/rt_tables , for example slow and fast; assign each table a different default route (one via fiber and another via a more limited link); use ip rule to specify that packets with fwmark 1 go to the fast table, those with fwmark 2 to slow, etc.; and finally, use NFQUEUE to mark the packets appropriately before returning the verdict.
To set a verdict from the callback, `nfq_set_verdict2` is used , which is like `nfq_set_verdict` but allows you to set a verdict value that `ip rule` will then see. Combining all of this, you can build an IP router that decides where to route based on arbitrary criteria: from absurd things like even/odd packet size to external inputs such as traffic prediction algorithms, social media events, or signals from monitoring systems.
The result is a system in which the kernel continues to forward packets at the usual rate, but the exact path each flow takes is delegated to external software that can change its mind in real time without touching static rules.
NFQUEUE and Suricata: High-level IPS in GNU/Linux
All of the above can be programmed by hand in C, but when it comes to intrusion detection and deep packet inspection, the sensible option is usually to rely on a mature IDS/IPS engine . That's where Suricata comes in, which was born precisely as a multi-process alternative to Snort, with IPS capabilities from the start and a focus heavily on leveraging the many CPU cores available today.
Suricata is written from scratch and distributed under the GPLv2 license ; the Open Information Security Foundation (OISF) maintains both the engine and a fairly comprehensive ecosystem of rules and documentation. Unlike Snort 2.x, which inherited a single-threaded core and was patched onto it, Suricata was designed to divide the workload across multiple threads: capture, decoding, detection, and output, with different load-sharing strategies.
At a functional level, Suricata provides native support for IPv6, layer 7 inspection (very advanced HTTP via HTP library), protocol recognition independent of ports , flow reconstruction, and a very powerful system of session variables (flowbits) to correlate different stages of an attack spread across several TCP connections.
An additional strength is its compatibility with Snort rules and its ability to use both Sourcefire VRT and Emerging Threats signature sets (the free ET Open and the commercial ET Pro versions). Furthermore, it exports events in very useful formats (fast.log, JSON in eve.json) for integration with SIEMs, ELK, Splunk, and other systems.
Suricata as an IPS in Linux: capture modes and NFQUEUE
On GNU/Linux, Suricata can operate in different modes depending on how traffic is intercepted: NFQUEUE, AF_PACKET, PF_RING, libpcap, NFLOG, IPFW, DAG, Napatech … Each has its advantages and requirements. At the pure IPS level, the two most important are NFQUEUE and AF_PACKET.
In NFQ (NFQUEUE) mode , the flow is similar to the one described earlier: a set of iptables rules sends packets to a queue; Suricata, running in user space, reads from that queue, inspects the contents according to its rules, and returns a verdict to the kernel: NF_ACCEPT, NF_DROP, or NF_REPEAT. The third can be used to reinject the packet into the same iptables table after applying additional marks or modifications.
This mode is very flexible and easy to implement in existing infrastructures , because it only requires modifying the rules at specific points (for example, FORWARD, INPUT, OUTPUT) and leaving everything else as is. The cost is the added overhead of passing packets up and down through NFQUEUE, with the aforementioned impact if the volume is very high or the rules are resource-intensive.
In AF_PACKET mode , Suricata operates closer to the network interface, copying packets across AF_PACKET sockets. This is a much faster zero-copy approach , but it requires the system to function as a gateway with two interfaces and that traffic blocking be performed at the forwarding level between NICs: the packet to be blocked is simply not passed from the input interface to the output interface.
In both modes, Suricata can be combined with Netfilter, but NFQUEUE fits especially well in scenarios where we want to reuse all the iptables logic (policies, ranges, previous rules) and only send to Suricata the traffic that we are interested in inspecting in depth.
Basic installation of Suricata from source code
For those who prefer to compile Suricata instead of using packages, the process on Debian/Ubuntu-type distributions involves first installing the compilation dependencies (build-essential, libpcre, libpcap-dev, libnet-dev, libyaml-dev, zlib, libcap-ng-dev, libjansson-dev, etc.), downloading the tarball from the official website, and running the classic ./configure, make, make install.
During the configure phase, the script will indicate which support features have been enabled: AF_PACKET yes/no, PF_RING, NFQUEUE yes/no, NFLOG, IPFW, support for libnss, libjansson, Prelude, PCRE JIT, Lua, GeoIP, etc. It is important to verify that NFQUEUE is enabled if we want to work in that mode , and that the capture library we are interested in has been located.
After installing the binary, you can run `make install-conf` to deploy a default configuration to `/etc/suricata` and `make install-rules` to download and place a set of Emerging Threats rules in `/etc/suricata/rules`. These sets can then be updated using tools like `suricata-update`.
On Red Hat/CentOS systems, the logic is similar, using yum or dnf for dependencies (libpcap-devel, pcre-devel, libnet-devel, libyaml-devel, jansson-devel, etc.) and then compiling with the same steps. For performance reasons, it's also advisable to disable LRO/GRO in the capture interface using ethtool, as these offload functions can affect the visibility of packages at the IDS level.
Suricata configuration: YAML, variables, and threading
Suricata's main configuration resides in /etc/suricata/suricata.yaml . It is a fairly readable and heavily commented YAML file, where everything from log paths and rule sets to target operating system policies and threading parameters are defined.
One of the basic fields is `default-log-dir` , which specifies where the log files will be stored (by default, `/var/log/suricata`). Under the `vars` section are variables such as `HOME_NET`, `EXTERNAL_NET`, `HTTP_PORTS`, `SHELLCODE_PORTS`, and `SSH_PORTS`, which serve as abbreviations in the rules. `HOME_NET` is usually configured with the local network range we want to protect, while `EXTERNAL_NET` is typically defined as `!HOME_NET`.
Another important part is the host-os-policy , which tells Suricata which operating system is supposed to run certain IP ranges. This allows it to adjust how it reassembles TCP or interprets certain network stack behaviors , making it harder to evade protocols based on differences between stacks (Windows vs. Linux, etc.). Specific ranges can be assigned to categories such as Windows, Linux, BSD, Vista, Windows 2003, etc.
Regarding threading, the threading section allows you to fine-tune CPU affinity and the number of detection threads. By default, `set-cpu-affinity` is usually disabled, which lets the system scheduler distribute threads across the cores. The `detect-thread-ratio` parameter indicates how many detection threads are created per available core; with `detect-thread-ratio: 1.5` on an 8-core machine, Suricata will generate 12 detection threads, plus capture and management threads.
This entire model is then reflected in the output when the daemon starts: a capture thread (for example, pcap) and multiple detector threads are visible, in addition to flow managers and statistics managers. This multi-process architecture is what allows Suricata to scale much better than single-threaded engines when faced with 10/40 Gbit/s links.
Rules and signature updates in Suricata
Suricata relies on rule sets to detect attack patterns, anomalous behavior, and protocol misuse. In addition to accepting rules in Snort format , the most common ecosystem is that of Emerging Threats: ET Open (free) and ET Pro (commercial), with rules geared toward current threats.
Many modern distributions include the `suricata-update` tool , which simplifies rule management: it updates sources, enables or disables specific providers, and downloads the latest versions of signature sets. A typical workflow would be to install `suricata-update` (for example, via pip), run the first `suricata-update` to download ET Open, list sources with `suricata-update list-sources`, enable additional sources such as `ptresearch/attackdetection`, `oisf/trafficid`, or `sslbl/ssl-fp-blacklist`, and run `suricata-update` again to regenerate the rules file.
The suricata.yaml file is adjusted to point to the correct rules path, and from there Suricata will begin raising alert events that will be logged in fast.log (fast, readable text) and eve.json (structured JSON with very complete information) . This latter format is especially useful for feeding dashboards, correlation systems, or custom scripts.
In addition to signatures, Suricata incorporates decoders and parsers for multiple protocols , allowing it to be less dependent on ports: it can identify HTTP traffic even if it goes through non-standard ports, detect SSH, TLS, DNS, etc. over different ports and encapsulation levels (including mixed IPv4/IPv6 tunnels).
Practical use: from detecting web exploits to automatic blocking
One of the most desired use cases in hosting environments or data centers is to detect in real time attempts to exploit vulnerabilities in web applications (e.g., WordPress and its plugins) and react automatically, usually by blocking or blacklisting the source IP in the firewall.
Suricata, powered by updated rules, is capable of recognizing specific attack patterns against URLs, parameters, HTTP payloads , and even request sequences that match known exploits. The IDS can operate in passive mode, receiving traffic via mirroring from a switch port (SPAN), but to act as an IPS and block attacks, it needs to be integrated with the forwarding plane.
There are two common approaches: setting up the IDS as an online bridge, so that traffic physically passes through the machine (using iptables, AF_PACKET, or PF, depending on the platform), or leaving the topology as is but combining mirroring with actions on the central firewall via API, scripts, or NFQUEUE . The first approach minimizes latency between detection and blocking, at the cost of adding another element "in the middle" of the network; the second offers greater flexibility and resilience, but introduces more complexity to the orchestration.
It's perfectly feasible for an Intrusion Detection System (IDS) to detect an attempt to exploit a vulnerable WordPress plugin and then, either directly or through an associated component, add the attacker's IP address to an iptables blacklist. This can be done via Suricata's JSON output and scripts that call iptables/nftables , or by delegating some of the logic to NFQUEUE, where the engine itself or an associated process makes the decision on the fly without waiting for an external list to be updated.
This allows you to focus on threats that really matter (exploits, escalation attempts, very aggressive scans), ignoring or simply logging background noise such as basic port scans that, in many contexts, are not worrisome in themselves.
Suricata on Pfsense: opensource firewall with integrated IDS/IPS
Not everyone can or wants to afford a proprietary high-end firewall like Palo Alto. In many environments, it's more attractive to set up an open-source solution with pfSense and Suricata , which covers both advanced firewall needs (multi-WAN, VLAN, VPN, NAT, etc.) and IDS/IPS.
Pfsense, based on FreeBSD and Packet Filter, works particularly well with virtualized environments (Proxmox, KVM, etc.), with the exception that it is recommended to use E1000 cards instead of Virtio in KVM machines if you want to avoid performance problems and crashes under load, unless you apply Netgate's recommendations (disable hardware checksum offload in System > Advanced > Networking and restart, knowing that this may not be enough with very high loads).
The minimum hardware requirements for a lab with Suricata on Pfsense can be modest (1 CPU 500 MHz, 1 GB RAM, 4 GB disk), but for serious use, at least 2 CPUs, 4 GB of RAM and 16 GB of storage are recommended , not forgetting to have several network interfaces (one for WAN, another for LAN, more if you want multiple WANs or complex VLANs).
Installing pfSense itself is very quick: you boot from the ISO, accept the license, choose to install, select your language and keyboard layout, leave the partitioning on automatic (Auto UFS if you're going to use the entire disk), and in a few minutes the system is ready for its first boot. The console offers a menu for assigning interfaces, restarting, launching the shell, etc.
In the lab, for example in VirtualBox, it is common to temporarily disable the Pfsense firewall from the console with pfctl -d in order to access the web interface via the WAN (username admin, password pfsense) and complete the initial wizard: general data, NTP servers, WAN configuration (DHCP is usually sufficient in the lab), LAN, change of admin password and application of the configuration.
Once access is stabilized, you can create a rule in the WAN firewall that allows HTTPS from any source to the pfsense IP address, adding descriptive separators to visually organize the rules (for example, "Firewall Access"). It's also advisable to disable the option to block private networks on the WAN if you're in a test environment with RFC1918 addresses, to avoid having to constantly use `pfctl -d`.
Suricata installation on pfSense and overview
With the pfSense base up and running, installing Suricata is as simple as going to System > Package Manager > Available Packages , searching for Suricata, and installing the package. The process downloads several files and may take a while depending on your hardware, but it's fully assisted through the web interface.
Once installed, a Suricata entry appears in the Services tab, where you can configure instances by interface (WAN, LAN, VLANs, etc.), choose which rule sets to use, activate IDS or IPS mode , and adjust performance and logging parameters. The range of options is extensive (enough for entire articles just on configuration), but the advantage is that many tasks that in Linux require manual YAML editing are handled here with forms and checkboxes.
Important note: While it might be tempting to open the pfSense administration directly to the internet in a lab environment, in production it's crucial to restrict access to static IP addresses, use VPNs for remote management, and avoid leaving the web console exposed at all costs . pfSense is very flexible, but it must also be treated as the critical element it is.
With Suricata enabled on pfSense, you get an environment where traffic passes through pfSense for firewalling and NAT, and Suricata inspects it according to its rules and can block it in IPS mode . This combination, managed from a single web interface, greatly simplifies deploying DPI protection in small and medium-sized networks.
In many deployments, this is complemented by a connection of Pfsense/Suricata to a SIEM or centralized log platform, taking advantage of structured output formats to correlate events and detect broader campaigns.
Event monitoring and example logs in Suricata
Once Suricata is running, events are logged to the path defined by default-log-dir, usually /var/log/suricata . The fast.log file uses a compact text format with timestamps, rule IDs, classifications, and priority, suitable for quick inspection from the terminal (tail -f).
For example, when encountering traffic with incorrect TCP checksums, we might see lines like these: timestamps with date and time, followed by the rule identifier (e.g., 1:2200074:1), the message "SURICATA TCPv4 invalid checksum", classification, priority, and the source-destination IP/port pair. These types of alerts allow for the rapid identification of packet integrity issues or evasion attempts.
The eve.json file contains the same events in JSON format, with fields such as timestamp, event_type, src_ip, dest_ip, src_port, dest_port, proto, and an alert subfile with action, gid, signature_id, rev, signature, category, and severity. This format can be easily ingested with Logstash, Fluentd, Filebeat, or any other log agent , allowing for much richer analytics than simply using plain text.
When deploying Suricata on a multi-core server (e.g., 8 cores), the thread compression is readily apparent in tools like htop in thread mode, showing one or more capture threads (pcap, AF_PACKET, or NFQ) and a large number of detection threads distributed across the cores. Adjusting the detect-thread-ratio and CPU affinity can significantly impact throughput and latency when traffic volume approaches the platform's limits.
Before deploying it to production, it's advisable to spend some time fine- tuning which rule sets are activated to avoid a flood of false positives that could block legitimate traffic or clutter the logs. Suricata-update allows you to disable entire categories or individual rules to find a reasonable balance between sensitivity and usability.
Special applications: VoIP, audio analytics, and creative NFQUEUE
Beyond classic uses (web service protection, malware detection, DDoS analysis), the Netfilter+NFQUEUE duo allows for quite creative solutions in areas such as VoIP. For example, it's possible to set up an anti-SPIT (spam over IP telephony) filter or a system for censoring profanity in RTP streams.
The idea would be: to identify RTP traffic by ports or by protocol recognition and send it to NFQUEUE; from the user application, reconstruct the RTP stream using a library like librtp , extract the audio in WAV format and pass it to a keyword recognition engine (wordspotting), such as a synthesis or recognition library offered by a third party.
Based on the detected words, the NFQUEUE process could decide to allow, block, or even alter playback by inserting a beep into the stream, although the latter requires very fine-tuned control of RTCP, packet sequences, and timings—almost a man-in-the-middle approach. It's not trivial, but theoretically it's perfectly achievable by leveraging the same queue and verdict system.
It's true that some of this could be done with a simple sniffer feeding data to an external processor, and then acting on the SIP signaling or through an SBC (Asterisk, Kamailio, etc.). The difference with using NFQUEUE is that the action on the RTP flow can be immediate and direct , without needing to coordinate multiple components or wait for the signaling layer to complete the call.
These scenarios clearly illustrate the potential of the GNU/Linux + Netfilter + Suricata + third-party libraries combination: it's not just about blocking ports and IP addresses, but about orchestrating complex traffic decisions in real time using a 100% free software ecosystem.
Looking at the entire journey, from the small C program that always accepts packets to a multiprocess Suricata deployment integrated with NFQUEUE, Pfsense, databases and caches, one can appreciate the flexibility that this technology stack offers to build everything from simple dynamic firewalls to data center-scale IDS/IPS architectures, with real deep inspection capabilities and automated response to increasingly complex attacks.