Background
In the previous article, we examined how to write a Kernel module and load it dynamically into a running Linux system. Based on this understanding, let’s continue our journey to write a Netfilter
module as our mini-firewall.
Netfilter architecture.
Basics of Netfilter hooks
The Netfilter
framework provides a bunch of hooks
in the Linux kernel. As network packets pass through the protocol stack in the kernel, they will traverse these hooks as well. And Netfilter allows you to write modules and register callback functions with these hooks. When the hooks are triggered, the callback functions will be called. This is the basic idea behind Netfilter architecture. Not difficult to understand, right?
Currently, Netfilter provides the following 5 hooks for IPv4
:
- NF_INET_PRE_ROUTING: is triggered right after the packet has been received on a network card. This hook is triggered before the
routing decision
was made. Then the kernel determines whether this packet is destined for the current host or not. Based on the condition, the following two hooks will be triggered. - NF_INET_LOCAL_IN: is triggered for network packets that are destined for the current host.
- NF_INET_FORWARD: is triggered for network packets that should be forwarded.
- NF_INET_POST_ROUTING: is triggered for network packets that have been routed and before being sent out to the network card.
- NF_INET_LOCAL_OUT: is triggered for network packets generated by the processes on the current host.
The hook function you defined in the module can mangle or filter the packets, but it eventually must return a status code to Netfilter. There are several possible values for the code, but for now, you only need to understand two of them:
- NF_ACCEPT: this means the hook function accepts the packet and it can go on the network stack trip.
- NF_DROP: this means the packet is dropped and no further parts of the network stack will be traversed.
Netfilter allows you to register multiple callback functions to the same hook with different priorities. If the first hook function accepts the packet, then the packet will be passed to the next functions with low priority. If the packet is dropped by one callback function, then the next functions(if existing) will not be traversed.
As you see, Netfilter
has a big scope and I can’t cover every detail in the articles. So the mini-firewall developed here will work on the hook NF_INET_PRE_ROUTING
, which means it works by controlling the inbound network traffic. But the way of registering the hook and handling the packet can be applied to all other hooks.
Note: there is another remarkable question: what’s the difference between Netfilter
and eBPF
? If you don’t know eBPF, please refer to my previous article. Both of them are important network features in the Linux kernel. The important thing is Netfilter
and eBPF
hooks are located in different layers of the Kernel. As I drew in the above diagram, eBPF
is located in a lower layer.
Kernel code of Netfilter hooks
To have a clear understanding of how the Netfilter
framework is implemented inside the protocol stack, let’s dig a little bit deeper and take a look at the kernel source code (Don’t worry, only shows several simple functions). Let’s use the hook NF_INET_PRE_ROUTING
as an example; since the mini-firewall will be written based on it.
When an IPv4 packet is received, its handler function ip_rcv
will be called as follows:
1 | //In source code file /kernel-src/net/ipv4/ip_input.c |
In this handler function, you can see the hook is passed to the function NF_HOOK
. Based on the name NF_HOOK
, you can guess that it is for triggering the Netfilter hooks. Right? Let’s continue to examine how NF_HOOK
is implemented as follows:
1 | //In source code file /kernel-src/include/linux/netfilter.h |
The function NF_HOOK
contains two steps:
- First, runs the hook’s callback functions by calling the underlying function
nf_hook
. - Second, invokes the function
okfn
(passed to NF_HOOK as the argument), if the packet passes through the hook functions and doesn’t drop.
For the hook NF_INET_LOCAL_IN, the function ip_rcv_finish
will be invoked after the hook functions pass. Its job is to pass the packet on to the next protocol handler(TCP or UDP) in the protocol stack to continue its journey!
The other 4 hooks all use the same function NF_HOOK
to trigger the callback functions. The following table shows where the hooks are embedded in the kernel, I leave them to the readers.
Hook | File | Function |
---|---|---|
NF_INET_PRE_ROUTING | /kernel-src/net/ipv4/ip_input.c | ip_rcv() |
NF_INET_LOCAL_IN | /kernel-src/net/ipv4/ip_input.c | ip_local_deliver() |
NF_INET_FORWARD | /kernel-src/net/ipv4/ip_forward.c | ip_forward() |
NF_INET_POST_ROUTING | /kernel-src/net/ipv4/ip_output.c | ip_build_and_send_pkt() |
NF_INET_LOCAL_OUT | /kernel-src/net/ipv4/ip_output.c | ip_output() |
Next, Let’s review the Netfilter’s APIs to create and register the hook function.
Netfilter API
It’s straightforward to create a Netfilter module, which involves three steps:
- Define the hook function.
- Register the hook function in the kernel module initialization process.
- Unregister the hook function in the kernel module clean-up process.
Let’s go through them quickly one by one.
Define a hook function
The hook function name can be whatever you want, but it must follow the signature below:
1 | //In source code file /kernel-src/include/linux/netfilter.h |
The hook function can mangle or filter the packet whose data is stored in the sk_buff
structure (we can ignore the other two parameters; since we don’t use them in our mini-firewall). As we mentioned above, the callback function must return a Netfilter status code which is an integer. For instance, the accepted
and dropped
status is defined as follows:
1 | // In source code file /kernel-src/include/uapi/linux/netfilter.h |
Register and unregister a hook function
To register a hook function, we should wrap the defined hook function with related information, such as which hook you want to bind to, the protocol family and the priority of the hook function, into a structure struct nf_hook_ops
and pass it to the function nf_register_net_hook
.
1 | //In source code file /kernel-src/include/linux/netfilter.h |
Most of the fields are very straightforward to understand. The one need to emphasize is the field hooknum
, which is just the Netfilter hooks discussed above. They are defined as enumerators as follows:
1 | // In source code file /kernel-src/include/uapi/linux/netfilter.h |
Next, let’s take a look at the functions to register and unregister hook functions goes as follows:
1 | //In source code file /kernel-src/include/linux/netfilter.h |
The first parameter struct net
is related to the network namespace, we can ignore it for now and use a default value.
Next, let’s implement our mini-firewall based on these APIs. All right?
Implement mini-firewall
First, we need to clarify the requirements for our mini-firewall. We’ll implement two network traffic control rules in the mini-firewall as follows:
- Network protocol rule: drops the ICMP protocol packets.
- IP address rule: drops the packets from one specific IP address.
The completed code implementation is in this Github repo.
Drop ICMP protocol packets
ICMP
is a network protocol widely used in the real world. The popular diagnostic tools like ping
and traceroute
run the ICMP protocol. We can filter out the ICMP packets based on the protocol type in the IP headers with the following hook function:
1 | // In mini-firewall.c |
The logic in the above hook function is easy to understand. First, we retrieve the IP headers from the network packet. And then according to the protocol
type field in the headers, we decided to accept TCP and UDP packets but drop the ICMP packets. The only technique we need to pay attention to is the function ip_hdr
, which is the kernel function defined as follows:
1 | //In source code file /kernel-src/include/linux/ip.h |
The function ip_hdr
delegates the task to the function skb_network_header
. It gets IP headers based on the following two data:
- head: is the pointer to the packet;
- network_header: is the offset between the pointer to the packet and the pointer to the network layer protocol header. In detail, you can refer to this document.
Next, we can register the above hook function as follows:
1 | // In mini-firewall.c |
The above logic is self-explaining. I will not spend too much time here.
Next, it’s time to demo how our mini-firewall works.
Demo time
Before we load the mini-firewall module, the ping
command can work as expected:
1 | chrisbao@CN0005DOU18129:~$ lsmod | grep mini_firewall |
In contrast, after the mini-firewall module is built and loaded (based on the commands we discussed previously):
1 | chrisbao@CN0005DOU18129:~$ lsmod | grep mini_firewall |
You can see all the packets are lost; because it is dropped by our mini-firewall. We can verify this by running the command dmesg
:
1 | chrisbao@CN0005DOU18129:~$ dmesg | tail -n 5 |
But other protocol packets can still run through the firewall. For instance, the command wget 142.250.4.103
can return normally as follows:
1 | chrisbao@CN0005DOU18129:~$ wget 142.250.4.103 |
Next, let’s try to ban the traffic from this IP address.
Drop packets source from one specific IP address
As we mentioned above, multiple callback functions are allowed to be registered on the same Netfilter hook. So we will define the second hook function with a different priority. The logic of this hook function goes like this: we can get the source IP address from the IP headers and make the drop or accept decision according to it. The code goes as follows
1 | // In mini-firewall.c |
This hook function uses two interesting techniques:
ntohl
: is a kernel function, which is used to convert the value fromnetwork byte order
tohost byte order
.Byte order
is related to the computer science concept ofEndianness
. Endianness defines the order or sequence of bytes of a word of digital data in computer memory. Abig-endian
system stores the most significant byte of a word at the smallest memory address. Alittle-endian
system, in contrast, stores the least-significant byte at the smallest address. Network protocol uses thebig-endian
system. But different OS and platforms run various Endianness system. So it may need such conversion based on the host machine.IPADDRESS
: is a macro, which generates the standard IP address format(four 8-bit fields separated by periods) from a 32-bit integer. It uses the technique ofthe equivalence of arrays and pointers in C
. I will write another article to examine what it is and how it works. Please keep watching my updates!
Next, we can register this hook function in the same way discussed above. The only remarkable point is this callback function should have a different priority as follows:
1 | static int __init nf_minifirewall_init(void) { |
Let’s see how it works with a demo.
Demo time
After re-build and re-load the module, we can get:
1 | chrisbao@CN0005DOU18129:~$ wget 142.250.4.103 |
The wget 142.250.4.103
can’t return response. Because it is dropped by our mini-firewall. Great!
1 | chrisbao@CN0005DOU18129:~$ dmesg | tail -n 5 |
More space to expand
You can find the full code implementation here. But I have to say, our mini-firewall only touches the surface of what Netfilter can provide. You can keep expanding the functionalities. For example, currently, the rules are hardcoded, why not make it possible to config the rules dynamically. There are many cool ideas worth trying. I leave it for the readers.
Summary
In this article, we implement the mini-firewall step by step and examined many detailed techniques. Not only code; but we also verify the behavior of the mini-firewall by running real demos.