How to build Firewall with C++

This blog explores the implementation of a user-space firewall using Netfilter Queue and Conntrack, mimicking Cisco-style Access Control Lists (ACLs).

Programming
Cybersecurity
Network
Code
Banner image

Why should i care?

Before I learn anything, I always ask myself: why? Understanding how firewalls work isn’t just for network engineers—it’s for anyone who wants to protect their system or even learn how attackers think.

Take this example: imagine you're trying to scan an internal network. If you don’t understand how the firewall works, you might run a basic scan and instantly trigger alerts. But if you do understand it, you’ll know how to be stealthy—maybe avoid certain ports, limit your requests, or mimic normal traffic.

Basically, knowing how the firewall behaves lets you move smart, whether you're defending or attacking.

and the best way to learn is to create your own firewall and in this blog i will explain how to create firewall the can block or deny cretin packets, and in future add scan the packets or you might do it?

What is Firewall?

A firewall is basically a digital gatekeeper between your trusted internal network (like your home or office) and untrusted external networks (like the internet). It inspects every packet of data entering or leaving your system, comparing them to a set of predefined security rules—only allowing traffic that matches and blocking the rest.

One common way firewalls make decisions is through something called Access Control Lists (ACLs). These are simple rule sets that define what kind of traffic is allowed or denied based on things like IP address, port number, or protocol. For example:
Allow TCP traffic from 192.168.1.10 to port 80, but block everything else.

This kind of logic is used in enterprise firewalls like Cisco’s, and in this blog, we’ll implement a similar system to help you understand how these filters work at a deeper level.

System Architecture

1. Linux Kernel (Netfilter):

  • Netfilter is a hook-based packet filtering system inside the Linux kernel.
  • Packets traverse through five main hooks:
    • PREROUTING
    • INPUT
    • FORWARD
    • OUTPUT
    • POSTROUTING
  • You can insert rules into these hooks using iptables to forward packets to a user-space queue via NFQUEUE.

2. NFQUEUE (Queueing packets):

  • When a packet matches a rule in iptables, it gets queued to a specific queue number using the NFQUEUE target.
  • These packets are frozen in the kernel and handed to a user-space application via libnetfilter_queue.

3. User-Space Firewall (C++):

  • Your C++ application reads from this queue.
  • Parses the packet using raw socket headers (IP, TCP/UDP/ICMP).
  • Evaluates it against a custom set of ACL rules (like "deny all from 192.168.1.0/24 to port 22").
  • Takes an action:
    • NF_ACCEPT: Let the packet through.
    • NF_DROP: Drop the packet.
    • (Optionally) Log or alert.

Let's dive into the action

Before diving into the implementation, it's a good idea to open the project in another tab so you can follow along more easily. Walking through the code while reading will help you understand how everything connects—from packet interception to rule handling.

Now that we understand what the Linux kernel is and how Netfilter Queue works, we can start programming our own firewall—this time using the power of C++. With Netfilter Queue, we can intercept packets in user space, inspect them, and decide whether to accept or drop them based on our own rules—just like a real firewall.

Project structure

run.bash – Compiles the project and sets up iptables to redirect traffic to the firewall via Netfilter Queue.

firewall.cpp – Core logic: receives packets, inspects them, and decides to allow or drop based on ACL rules.

aclManager.cpp – Handles ACL rule definitions and checks if a packet matches any rule.

log.cpp – Logs allowed/blocked packets using spdlog.

interface.cpp (optional) – CLI tool to manage ACLs (add, remove, list rules).

run.bash file

First we need load the kernal modules, and setting iptables rules, this allow the user space to access the NFQUEUE in the kernal and allow us to scan or deny it or allow it.

1. Load kernel modules

  • sudo modprobe nf_conntrack
  • sudo modprobe nfnetlink
  • sudo modprobe nfnetlink_queue

Needed for Netfilter Queue to work and allow packet inspection in user space.

2. Set iptables rules

run.bash
1iptables -t mangle -A PREROUTING ... 2iptables -t mangle -A OUTPUT ...

Redirects packets to NFQUEUE for both incoming and outgoing traffic.
Then runs your compiled firewall binary.

Cleanup is automatic on exit using trap.

2. Set Clean up

iptables -t mangle -F: Flushes (removes) all rules in the mangle table.iptables -t mangle -X: Deletes all user-defined chains in the mangle table.

Firewall.cpp file

since firewall.cpp is the core logic file, it should handle the following:

  1. Initializing NFQUEUE
  2. Registering the packet handling callback
  3. Parsing and inspecting packets (IP, TCP, UDP, etc.)
  4. Evaluating packets against ACL rules
  5. Returning verdicts (ACCEPT, DROP, etc.)

First, we have the constructor and destructor of the Firewall class, which handle the setup of the entire firewall system. In the constructor, the first thing done is storing a reference to the ACLManager. This class holds all the Access Control Entries (ACEs), which are a structured set of rules used to match and control traffic. Each ACE defines conditions such as source/destination IP, ports, protocols, TCP flags, and actions (either allow or deny), enabling fine-grained packet filtering.

After storing the ACLManager reference, the constructor proceeds to initialize a thread pool with 4 threads. Each thread is responsible for handling its own NFQUEUE queue, which allows the firewall to process multiple packets concurrently, improving performance. Inside each thread, a separate nfq_handle *h is created using nfq_open(). This handle is the main interface to the Netfilter Queue subsystem in the Linux kernel, enabling the firewall to receive packets redirected from iptables via NFQUEUE rules. By having a unique nfq_handle per thread, the system ensures isolation between threads and avoids shared-state contention, which is important for stability and efficiency under high traffic conditions. Each thread then configures its handle, binds it to the IPv4 protocol, sets up queue handlers, and enters a packet-receiving loop where it evaluates packets against the ACEs and returns a verdict (accept or drop) accordingly.

The line nfq_q_handle *qh = nfq_create_queue(h, i, &Firewall::callback, reinterpret_cast<void *>(i)); sets up the callback function for each queue. Here, the nfq_create_queue() function creates a new queue for each thread and binds it to the appropriate Netfilter handler.

The start function runs in the background used for handling packet processing in different threads. It takes the queue number as an argument and uses it to access the corresponding Netfilter queue handler (nfq_handle *h), which allows us to capture network traffic from the kernel. The function then enters an infinite loop where it listens for incoming packets from the Netfilter queue using recv(). It reads raw packet data into a buffer and checks if the reception was successful. If successful, the function passes the packet to the nfq_handle_packet() function, which processes the packet and invokes the user-defined callback which we already configure it in the destructor

The Firewall::callback function is the core packet handler that is executed every time a packet is received by the Netfilter queue. It is registered via nfq_create_queue, and it is invoked for each packet passed from the kernel to user space. This function's purpose is to parse the packet data, determine the protocol, and apply appropriate logic to decide whether to allow or drop the packet based on predefined rules.

First, the function extracts the packet metadata and payload. It uses nfq_get_msg_packet_hdr(nfa) to retrieve the packet header, from which it extracts the packet ID—this is crucial because the verdict must be explicitly set for each packet using this ID. The actual payload data (raw packet bytes) is then retrieved using nfq_get_payload(nfa, &packetData). The data argument passed to the callback (from nfq_create_queue) contains the thread-specific queue index, which is cast back into an integer and used to reference the appropriate Queue structure from the thread pool.

After acquiring the payload, the function interprets it as an IP packet by casting it to an iphdr structure. Additionally, it casts the same data to an ether_header to retrieve Ethernet-level information if needed. Next, the function determines the direction of the packet—whether it’s inbound—by checking the Netfilter hook (e.g., NF_INET_PRE_ROUTING or NF_INET_LOCAL_IN). This information is used later during rule matching.

The core of the function is a switch statement that handles packets based on their protocol. If the packet is TCP, it extracts the TCP header and wraps all header layers into a tcpFullHdr structure (which likely includes Ethernet, IP, and TCP headers plus direction). It then passes this to a TCP() function, which likely evaluates the packet against the TCP-specific ACEs. Based on the result, it either calls allowPacket() or dropPacket() with the packet ID, sending the verdict back to the kernel.

The same logic is applied to UDP and ICMP packets using udpFullHdr and icmpFullHdr structures and their corresponding handler functions UDP() and ICMP(). If the protocol does not match any of these (e.g., GRE, ESP, or others), the default case is to drop the packet, enforcing a deny-by-default policy.

Ultimately, this function returns the verdict to the Netfilter subsystem, instructing it to either accept or drop the packet. This is where the actual enforcement of firewall rules takes place in real time for each intercepted packet.

For each protocol—TCP, UDP, and ICMP—the firewall performs a series of validation checks beyond simple protocol matching. These include:

  • Traffic Direction: Determines whether the packet is inbound or outbound based on the source and destination IP addresses and interfaces.
  • TCP State Verification: For TCP, the firewall inspects control flags (e.g. SYN, ACK, FIN) to ensure the packet is part of a valid connection sequence, preventing spoofed or out-of-state packets.
  • IP and Port Matching: Packets are evaluated against ACL rules that define allowed source/destination IP ranges and ports.
  • Security Policy Compliance: Additional policies may be enforced, such as blocking traffic between specific subnets or enforcing rate limits.

If a packet meets all defined criteria, it is accepted and forwarded to its destination. Otherwise, it is dropped, and the event is logged for auditing and analysis.

what is established connection ?

An established connection means a TCP session where the three-way handshake (SYN, SYN-ACK, ACK) has completed and data transfer is allowed.

In ACLs, "established" typically matches packets with ACK or RST flags, showing the packet is part of an existing flow.

The firewall checks this using:

  • TCP flags (e.g. ACK/RST set)
  • Conntrack (via libnetfilter_conntrack) to verify the connection state is ESTABLISHED or RELATED

Here is it in details:When a packet is processed, if the matching ACE rule has the established flag set to true, the firewall inspects the packet's TCP flags. If the packet has the ACK or RST flag set, it may be part of an ongoing or recently terminated connection. To confirm this, the firewall calls the query() method, which uses Conntrack to look up the connection in the kernel’s connection tracking table. It does so by matching the packet's source and destination IP addresses, ports, and protocol. If Conntrack finds a matching connection entry, the firewall confirms it as established and allows the packet. If no such entry is found, the packet is rejected because it's pretending to be part of an established connection without evidence in the state table.

On the other hand, if the TCP flags do not include ACK or RST, the firewall assumes this is either a new connection (e.g., a SYN packet) or some other non-established traffic, and it bypasses the Conntrack check.

ACLmanager.cpp file

This file handles Access Control Entries (ACEs), including operations such as adding new ACEs, deleting existing ones, and updating rules. It also manages these ACE rules through a Command-Line Interface (CLI). Additionally, the file is responsible for parsing ACE inputs provided via the CLI.

The implementation is straightforward and self-explanatory, so there is no need for in-depth explanation. Simply take a look at the code and its structure—everything should be clear.

END

while this code provides a functional starting point for managing ACLs and ACEs through a CLI, it does have its pros and cons. On the positive side, it's a clean and simple implementation that covers the essential features—adding, updating, deleting, and parsing ACEs—making it easy to follow and extend. However, there are areas for improvement, such as enhancing error handling, adding support for persistent storage, refining the parsing logic, and improving the overall structure using better design patterns. This project is part of my learning journey—I'm not claiming to be an expert. I'm constantly exploring, building, and improving. Feedback is always welcome, and I'm open to learning from others who are passionate about low-level systems, networking, and access control.

Published Aug 21, 2025