Firewall Exploration Lab: 2.1 Container Setup and Commands
Firewall Exploration Lab: 2.1 Container Setup and Commands
Firewall Exploration Lab: 2.1 Container Setup and Commands
1 Overview
The learning objective of this lab is two-fold: learning how firewalls work, and setting up a simple firewall
for a network. Students will first implement a simple stateless packet-filtering firewall, which inspects pack-
ets, and decides whether to drop or forward a packet based on firewall rules. Through this implementation
task, students can get the basic ideas on how firewall works.
Actually, Linux already has a built-in firewall, also based on netfilter. This firewall is called
iptables. Students will be given a simple network topology, and are asked to use iptables to set up
firewall rules to protect the network. Students will also be exposed to several other interesting applications
of iptables. This lab covers the following topics:
• Firewall
• Netfilter
• Loadable kernel module
• Using iptables to set up firewall rules
• Various applications of iptables
Readings and videos. Detailed coverage of firewalls can be found in the following:
• Chapter 17 of the SEED Book, Computer & Internet Security: A Hands-on Approach, 2nd Edition,
by Wenliang Du. See details at https://www.handsonsecurity.net.
• Section 9 of the SEED Lecture, Internet Security: A Hands-on Approach, by Wenliang Du. See details
at https://www.handsonsecurity.net/video.html.
Lab environment. This lab has been tested on the SEED Ubuntu 20.04 VM. You can download a pre-built
image from the SEED website, and run the SEED VM on your own computer. However, most of the SEED
labs can be conducted on the cloud, and you can follow our instruction to create a SEED VM on the cloud.
10.9.0.0/24 10.9.0.11
Router
` `
Attacker
10.9.0.1 10.9.0.5 192.168.60.11
192.168.60.0/24
` ` `
192.168.60.5 192.168.60.6 192.168.60.7
to the website of this lab. If this is the first time you set up a SEED lab environment using containers, it is
very important that you read the user manual.
In the following, we list some of the commonly used commands related to Docker and Compose. Since
we are going to use these commands very frequently, we have created aliases for them in the .bashrc file
(in our provided SEEDUbuntu 20.04 VM).
$ docker-compose build # Build the container image
$ docker-compose up # Start the container
$ docker-compose down # Shut down the container
All the containers will be running in the background. To run commands on a container, we often need
to get a shell on that container. We first need to use the "docker ps" command to find out the ID of
the container, and then use "docker exec" to start a shell on that container. We have created aliases for
them in the .bashrc file.
$ dockps // Alias for: docker ps --format "{{.ID}} {{.Names}}"
$ docksh <id> // Alias for: docker exec -it <id> /bin/bash
$ docksh 96
root@9652715c8e0a:/#
// type the entire ID string. Typing the first few characters will
// be sufficient, as long as they are unique among all the containers.
If you encounter problems when setting up the lab environment, please read the “Common Problems”
section of the manual for potential solutions.
Notes about containers. Since all the containers share the same kernel, kernel modules are global. There-
fore, if we set a kernel model from a container, it affects all the containers and the host. For this reason, it
does not matter where you set the kernel module. In this lab, we will just set the kernel module from the
host VM.
Another thing to keep in mind is that containers’ IP addresses are virtual. Packets going to these virtual
IP addresses may not traverse the same path as what is described in the Netfilter document. Therefore, in
this task, to avoid confusion, we will try to avoid using those virtual addresses. We do most tasks on the
host VM. The containers are mainly for the other tasks.
int initialization(void)
{
printk(KERN_INFO "Hello World!\n");
return 0;
}
void cleanup(void)
{
printk(KERN_INFO "Bye-bye World!.\n");
}
SEED Labs – Firewall Exploration Lab 4
module_init(initialization);
module_exit(cleanup);
We now need to create Makefile, which includes the following contents (the file is included in the lab
setup files). Just type make, and the above program will be compiled into a loadable kernel module (if you
copy and paste the following into Makefile, make sure replace the spaces before the make commands
with a tab).
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
The generated kernel module is in hello.ko. You can use the following commands to load the module,
list all modules, and remove the module. Also, you can use "modinfo hello.ko" to show information
about a Linux Kernel module.
$ sudo insmod hello.ko (inserting a module)
$ lsmod | grep hello (list modules)
$ sudo rmmod hello (remove the module)
$ dmesg (check the messages)
Task. Please compile this simple kernel module on your VM, and run it on the VM. For this task, we will
not use containers. Please show your running results in the lab report.
Hooking to Netfilter. Using netfilter is quite straightforward. All we need to do is to hook our
functions (in the kernel module) to the corresponding netfilter hooks. Here we show an example (the
code is in Labsetup/packet_filter, but it may not be exactly the same as this example).
SEED Labs – Firewall Exploration Lab 5
The structure of the code follows the structure of the kernel module implemented earlier. When the
kernel module is added to the kernel, the registerFilter() function in the code will be invoked.
Inside this function, we register two hooks to netfilter.
To register a hook, you need to prepare a hook data structure, and set all the needed parameters, the
most important of which are a function name (Line Ê) and a hook number (Line Ë). The hook number is
one of the 5 hooks in netfilter, and the specified function will be invoked when a packet has reached
this hook. In this example, when a packet gets to the LOCAL IN hook, the function printInfo() will
be invoked (this function will be given later). Once the hook data structure is prepared, we attach the hook
to netfilter in Line Ì).
int registerFilter(void) {
printk(KERN_INFO "Registering filters.\n");
// Hook 1
hook1.hook = printInfo; Ê
hook1.hooknum = NF_INET_LOCAL_IN; Ë
hook1.pf = PF_INET;
hook1.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &hook1); Ì
// Hook 2
hook2.hook = blockUDP;
hook2.hooknum = NF_INET_POST_ROUTING;
hook2.pf = PF_INET;
hook2.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &hook2);
return 0;
}
void removeFilter(void) {
printk(KERN_INFO "The filters are being removed.\n");
nf_unregister_net_hook(&init_net, &hook1);
nf_unregister_net_hook(&init_net, &hook2);
}
module_init(registerFilter);
module_exit(removeFilter);
Note for Ubuntu 20.04 VM: The code in the SEED book was developed in Ubuntu 16.04. It needs to be
changed slightly to work in Ubuntu 20.04. The change is in the hook registration and un-registration APIs.
See the difference in the following:
// Hook registration:
nf_register_hook(&nfho); // For Ubuntu 16.04 VM
nf_register_net_hook(&init_net, &nfho); // For Ubuntu 20.04 VM
// Hook unregistration:
SEED Labs – Firewall Exploration Lab 6
Hook functions. We give an example of hook function below. It only prints out the packet information.
When netfilter invokes a hook function, it passes three arguments to the function, including a pointer
to the actual packet (skb). In the following code, Line Ê shows how to retrieve the hook number from the
state argument. In Line Ë, we use ip hdr() function to get the pointer for the IP header, and then use
the %pI4 format string specifier to print out the source and destination IP addresses in Line Ì.
switch (state->hook){ Ê
case NF_INET_LOCAL_IN:
printk("*** LOCAL_IN"); break;
.. (code omitted) ...
}
iph = ip_hdr(skb); Ë
printk(" %pI4 --> %pI4\n", &(iph->saddr), &(iph->daddr)); Ì
return NF_ACCEPT;
}
If you need to get the headers for other protocols, you can use the following functions defined in various
header files. The structure definition of these headers can be found inside the /lib/modules/5.4.
0-54-generic/build/include/uapi/linux folder, where the version number in the path is the
result of "uname -r", so it may be different if the kernel version is different.
struct iphdr *iph = ip_hdr(skb) // (need to include <linux/ip.h>)
struct tcphdr *tcph = tcp_hdr(skb) // (need to include <linux/tcp.h>)
struct udphdr *udph = udp_hdr(skb) // (need to include <linux/udp.h>)
struct icmphdr *icmph = icmp_hdr(skb) // (need to include <linux/icmp.h>)
Blocking packets. We also provide a hook function example to show how to block a packet, if it satisfies
the specified condition. The following example blocks the UDP packets if their destination IP is 8.8.8.8
and the destination port is 53. This means blocking the DNS query to the nameserver 8.8.8.8.
iph = ip_hdr(skb);
if (iph->protocol == IPPROTO_UDP) {
udph = udp_hdr(skb);
if (iph->daddr == ip_addr && ntohs(udph->dest) == 53){ Ë
printk(KERN_DEBUG "****Dropping %pI4 (UDP), port %d\n",
&(iph->daddr), port);
return NF_DROP; Ì
}
}
return NF_ACCEPT; Í
}
In the code above, Line Ê shows, inside the kernel, how to convert an IP address in the dotted decimal
format (i.e., a string, such as 1.2.3.4) to a 32-bit binary (0x01020304), so it can be compared with the
binary number stored inside packets. Line Ë compares the destination IP address and port number with the
values in our specified rule. If they match the rule, the NF DROP will be returned to netfilter, which
will drop the packet. Otherwise, the NF ACCEPT will be returned, and netfilter will let the packet
continue its journey (NF ACCEPT only means that the packet is accepted by this hook function; it may still
be dropped by other hook functions).
Tasks. The complete sample code is called seedFilter.c, which is included in the lab setup files
(inside the Files/packet_filter folder). Please do the following tasks (do each of them separately):
1. Compile the sample code using the provided Makefile. Load it into the kernel, and demonstrate
that the firewall is working as expected. You can use the following command to generate UDP packets
to 8.8.8.8, which is Google’s DNS server. If your firewall works, your request will be blocked;
otherwise, you will get a response.
dig @8.8.8.8 www.example.com
2. Hook the printInfo function to all of the netfilter hooks. Here are the macros of the hook
numbers. Using your experiment results to help explain at what condition will each of the hook
function be invoked.
NF_INET_PRE_ROUTING
NF_INET_LOCAL_IN
NF_INET_FORWARD
NF_INET_LOCAL_OUT
NF_INET_POST_ROUTING
3. Implement two more hooks to achieve the following: (1) preventing other computers to ping the
VM, and (2) preventing other computers to telnet into the VM. Please implement two different hook
functions, but register them to the same netfilter hook. You should decide what hook to use.
Telnet’s default port is TCP port 23. To test it, you can start the containers, go to 10.9.0.5, run the
following commands (10.9.0.1 is the IP address assigned to the VM; for the sake of simplicity,
you can hardcode this IP address in your firewall rules):
SEED Labs – Firewall Exploration Lab 8
ping 10.9.0.1
telnet 10.9.0.1
Important note: Since we make changes to the kernel, there is a high chance that you would crash the
kernel. Make sure you back up your files frequently, so you don’t lose them. One of the common reasons
for system crash is that you forget to unregister hooks. When a module is removed, these hooks will still
be triggered, but the module is no longer present in the kernel. That will cause system crash. To avoid this,
make sure for each hook you add to your module, add a line in removeFilter to unregister it, so when
the module is removed, those hooks are also removed.
The rule is the most complicated part of the iptables command. We will provide additional informa-
tion later when we use specific rules. In the following, we list some commonly used commands:
// List all the rules in a table (without line number)
iptables -t nat -L -n
Note. Docker relies on iptables to manage the networks it creates, so it adds many rules to the nat
table. When we manipulate iptables rules, we should be careful not to remove Docker rules. For
example, it will be quite dangerous to run the "iptables -t nat -F" command, because it removes
all the rules in the nat table, including many of the Docker rules. That will cause trouble to Docker
containers. Doing this for the filter table is fine, because Docker does not touch this table.
Cleanup. Before moving on to the next task, please restore the filter table to its original state by
running the following commands:
iptables -F
iptables -P OUTPUT ACCEPT
iptables -P INPUT ACCEPT
Another way to restore the states of all the tables is to restart the container. You can do it using the
following command (you need to find the container’s ID first):
$ docker restart <Container ID>
You will need to use the "-p icmp" options to specify the match options related to the ICMP protocol.
You can run "iptables -p icmp -h" to find out all the ICMP match options. The following example
drops the ICMP echo request.
iptables -A FORWARD -p icmp --icmp-type echo-request -j DROP
In your lab report, please include your rules and screenshots to demonstrate that your firewall works
as expected. When you are done with this task, please remember to clean the table or restart the container
before moving on to the next task.
1. All the internal hosts run a telnet server (listening to port 23). Outside hosts can only access the telnet
server on 192.168.60.5, not the other internal hosts.
2. Outside hosts cannot access other internal servers.
3. Internal hosts can access all the internal servers.
4. Internal hosts cannot access external servers.
5. In this task, the connection tracking mechanism is not allowed. It will be used in a later task.
You will need to use the "-p tcp" options to specify the match options related to the TCP protocol.
You can run "iptables -p tcp -h" to find out all the TCP match options. The following example
allows the TCP packets coming from the interface eth0 if their source port is 5000.
iptables -A FORWARD -i eth0 -p tcp --sport 5000 -j ACCEPT
When you are done with this task, please remember to clean the table or restart the container before
moving on to the next task.
The goal of the task is to use a series of experiments to help students understand the connection concept
in this tracking mechanism, especially for the ICMP and UDP protocols, because unlike TCP, they do not
have connections. Please conduct the following experiments. For each experiment, please describe your
observation, along with your explanation.
• ICMP experiment: Run the following command and check the connection tracking information on
the router. Describe your observation. How long is the ICMP connection state be kept?
// On 10.9.0.5, send out ICMP packets
# ping 192.168.60.5
• UDP experiment: Run the following command and check the connection tracking information on the
router. Describe your observation. How long is the UDP connection state be kept?
SEED Labs – Firewall Exploration Lab 12
• TCP experiment: Run the following command and check the connection tracking information on the
router. Describe your observation. How long is the TCP connection state be kept?
// On 192.168.60.5, start a netcat TCP server
# nc -l 9090
The rule above does not cover the SYN packets, which do not belong to any established connection.
Without it, we will not be able to create a connection in the first place. Therefore, we need to add a rule to
accept incoming SYN packet:
iptables -A FORWARD -p tcp -i eth0 --dport 8080 --syn \
-m conntrack --ctstate NEW -j ACCEPT
Finally, we will set the default policy on FORWARD to drop everything. This way, if a packet is not
accepted by the two rules above, they will be dropped.
iptables -P FORWARD DROP
Please rewrite the firewall rules in Task 2.C, but this time, we will add a rule allowing internal hosts to
visit any external server (this was not allowed in Task 2.C). After you write the rules using the connection
tracking mechanism, think about how to do it without using the connection tracking mechanism (you do not
need to actually implement them). Based on these two sets of rules, compare these two different approaches,
and explain the advantage and disadvantage of each approach. When you are done with this task, remember
to clear all the rules.
SEED Labs – Firewall Exploration Lab 13
Please run the following commands on router, and then ping 192.168.60.5 from 10.9.0.5. De-
scribe your observation. Please conduct the experiment with and without the second rule, and then explain
whether the second rule is needed or not, and why.
iptables -A FORWARD -s 10.9.0.5 -m limit \
--limit 10/minute --limit-burst 5 -j ACCEPT
We can use the statistic module to achieve load balancing. You can type the following command
to get its manual. You can see there are two modes: random and nth. We will conduct experiments using
both of them.
$ iptables -m statistic -h
statistic match options:
--mode mode Match mode (random, nth)
random mode:
[!] --probability p Probability
nth mode:
[!] --every n Match every nth packet
--packet p Initial counter value (0 <= p <= n-1, default 0)
Using the nth mode (round-robin). On the router container, we set the following rule, which applies to
all the UDP packets going to port 8080. The nth mode of the statistic module is used; it implements
a round-robin load balancing policy: for every three packets, pick the packet 0 (i.e., the first one), change its
SEED Labs – Firewall Exploration Lab 14
destination IP address and port number to 192.168.60.5 and 8080, respectively. The modified packets
will continue on its journey.
iptables -t nat -A PREROUTING -p udp --dport 8080 \
-m statistic --mode nth --every 3 --packet 0 \
-j DNAT --to-destination 192.168.60.5:8080
It should be noted that those packets that do not match the rule will continue on their journeys; they will
not be modified or blocked. With this rule in place, if you send a UDP packet to the router’s 8080 port, you
will see that one out of three packets gets to 192.168.60.5.
// On 10.9.0.5
echo hello | nc -u 10.9.0.11 8080
<hit Ctrl-C>
Please add more rules to the router container, so all the three internal hosts get the equal number of
packets. Please provide some explanation for the rules.
Using the random mode. Let’s use a different mode to achieve the load balancing. The following rule
will select a matching packet with the probability P. You need to replace P with a probability number.
iptables -t nat -A PREROUTING -p udp --dport 8080 \
-m statistic --mode random --probability P \
-j DNAT --to-destination 192.168.60.5:8080
Please use this mode to implement your load balancing rules, so each internal server get roughly the
same amount of traffic (it may not be exactly the same, but should be close when the total number of packets
is large). Please provide some explanation for the rules.