Linux防火墙实验
SeedLab网络安全防火墙实验过程记录整理,对防火墙的理解还是比较浅层,在做的过程中有些仍没把握可能存在些许问题,欢迎指正和探讨!
实验来源:seed-labs
实验环境:SEEDUbuntu 20.04 VM
Environment Setup Using Containers
命令:docker-compose up -d
Task 1: Implementing a Simple Firewall
A: Implement a Simple Kernel Module
LKM allows us to add a new module to the kernel at the runtime. This new module enables us to extend the functionalities of the kernel, without rebuilding the kernel or even rebooting the computer. The packet filtering part of a firewall can be implemented as an LKM. In this task, we will get familiar with LKM. The following is a simple loadable kernel module. It prints out “Hello World!” when the module is loaded; when the module is removed from the kernel, it prints out “Bye-bye World!”. The messages are not printed out on the screen; they are actually printed into the /var/log/syslog file. You can use “dmesg” to view the messages.
|
|
We now need to create Makefile, which includes the following contents. Just type make, and the above program will be compiled into a loadable kernel module.
|
|
编译内核模块:
生成的内核模块为hello.ko,可以使用下述命令进行模块加载、展示、移除等。
|
|
在VM上运行该模块:
-
往运行时内核插入该模块:
sudo insmod hello.ko
-
展示:
lsmod | grep hello
-
从内核中移除特定模块:
sudo rmmod hello
-
查看模块运行时输出信息:
dmesg
B: Implement a Simple Firewall Using Netfilter
Theory: Netfilter is designed to facilitate the manipulation of packets by authorized users. It achieves this goal by implementing a number of hooks in the Linux kernel. These hooks are inserted into various places, including the packet incoming and outgoing paths. If we want to manipulate the incoming packets, we simply need to connect our own programs (within LKM) to the corresponding hooks. Once an incoming packet arrives, our program will be invoked. Our program can decide whether this packet should be blocked or not; moreover, we can also modify the packets in the program.
Task:Use LKM and Netfilter to implement a packet filtering module: This module will fetch the firewall policies from a data structure, and use the policies to decide whether packets should be blocked or not.
实验核心:将我们的函数(在内核模块中)挂接到相应的netfilter钩子上。,可拆解成如下3部分进行理解:
- Hooking to Netfilter
- Hook functions
- Blocking packets
简要说明可参考:实验手册
Task1
task: Compile the sample code using the provided Makefile. Load it into the kernel, and demonstrate that the firewall is working as expected.
-
(样例)包过滤模块函数代码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
#include <linux/kernel.h> #include <linux/module.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/udp.h> #include <linux/if_ether.h> #include <linux/inet.h> static struct nf_hook_ops hook1, hook2;//定义两个netfilter类型钩子,准备好hook data structure //阻止符合条件的udp数据包(钩子函数2) unsigned int blockUDP(void *priv, struct sk_buff *skb, const struct nf_hook_state *state){ struct iphdr *iph; struct udphdr *udph; u16 port = 53; char ip[16] = "8.8.8.8"; u32 ip_addr; if (!skb) return NF_ACCEPT; iph = ip_hdr(skb); //转换IP地址,从点分十进制格式(1.2.3.4)到32位二进制(0x01020304) //它可以与存储在数据包中的二进制数进行比较。 in4_pton(ip, -1, (u8 *)&ip_addr, '\0', NULL); if (iph->protocol == IPPROTO_UDP) { udph = udp_hdr(skb); //将目的IP地址和端口号与指定规则中的值进行比较 if (iph->daddr == ip_addr && ntohs(udph->dest) == port){ printk(KERN_WARNING "*** Dropping %pI4 (UDP), port %d\n", &(iph->daddr), port); return NF_DROP; //若匹配规则,则返回NF_DROP给netfilter, netfilter将丢弃数据包 //否则,不匹配过滤规则,返回NF_ACCEPT,netfilter将放行数据包 } } return NF_ACCEPT; } //钩子1的钩子函数:打印包信息 //当netfilter调用钩子函数时,传入三个参数,其中skb是真实包指针 unsigned int printInfo(void *priv, struct sk_buff *skb, const struct nf_hook_state *state){ struct iphdr *iph; char *hook; char *protocol; //如何从状态参数中检索钩子号。 switch (state->hook){ case NF_INET_LOCAL_IN: hook = "LOCAL_IN"; break; case NF_INET_LOCAL_OUT: hook = "LOCAL_OUT"; break; case NF_INET_PRE_ROUTING: hook = "PRE_ROUTING"; break; case NF_INET_POST_ROUTING: hook = "POST_ROUTING"; break; case NF_INET_FORWARD: hook = "FORWARD"; break; default: hook = "IMPOSSIBLE"; break; } printk(KERN_INFO "*** %s\n", hook); //打印钩子信息 iph = ip_hdr(skb);//获得IP头指针 switch (iph->protocol){ case IPPROTO_UDP: protocol = "UDP"; break; case IPPROTO_TCP: protocol = "TCP"; break; case IPPROTO_ICMP: protocol = "ICMP"; break; default: protocol = "OTHER"; break; } //输出源和目的IP地址及协议 printk(KERN_INFO " %pI4 --> %pI4 (%s)\n", &(iph->saddr), &(iph->daddr), protocol); return NF_ACCEPT; } //在该模块被加载后,调用此函数,注册两个hooks至netfilter int registerFilter(void){ printk(KERN_INFO "Registering filters.\n"); //设置hook data structure所需参数 hook1.hook = printInfo;//钩子函数名 hook1.hooknum = NF_INET_LOCAL_OUT;//hook number (LOCAL_OUT hook);钩子号是netfilter中5个钩子之一 hook1.pf = PF_INET; hook1.priority = NF_IP_PRI_FIRST; nf_register_net_hook(&init_net, &hook1);//准备好钩子的参数后,绑定至netfilter //同理 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; } //移除内核模块时从netfilter注销过滤模块 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); MODULE_LICENSE("GPL");
-
利用Makefile编译代码成LKM,并将它加载进内核中。
MakeFile如下:
1 2 3 4 5 6 7 8 9 10
obj-m += seedFilter.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 ins: sudo dmesg -C sudo insmod seedFilter.ko rm: sudo rmmod seedFilter
编译:
进行加载:
sudo insmod seedFilter.ko
-
验证防火墙起作用
利用
dig @8.8.8.8 www.example.com
生成UDP包至8.8.8.8(Google DNS服务器)通过上图可看出防火墙正常工作,请求已经被阻止了,超时不能到达。(若未工作,将会得到响应)
查看系统日志也可以发现LOCAL_OUT钩子已经起作用了,过滤模块对匹配规则的数据包进行了过滤:
Task2
Hook the printInfo function to all of the netfilter hooks. Using your experiment results to help explain at what condition will each of the hook function be invoked.
钩子号的宏:
|
|
源码设计:
|
|
Makefile:
|
|
编译:
加载:sudo insmod test.ko
验证每个钩子函数被调用的条件:
通过分析上图系统日志,当LKM被加载后各个钩子函数调用情况:
首先本机地址是10.0.2.15,因此[17493.785998]当从本地想要发包至10.10.0.119时,LOCAL_OUT钩子被调用;从日志能看出,这个从本地产生的发往其他机器的包在经过LOCAL_OUT钩子后,随后还会[17493.786006]由POST_ROUTING钩子进行处理;[17493.783003]能分析出从外机(10.10.0.19)发往某个机器,途径本机在进入协议栈后立即触发PRE_ROUTING钩子(在任何路由判断之前)进行校验(区分LOCAL_IN);最后[17493.793014]可以分析接收到的包经过路由判断,如果目的是本机,将触发此hook。
可以用dig @8.8.8.8 www.example.com构造,同理可以观察到上述4个钩子被触发。
注:对于触发FORWARD钩子,目前想到的方法应该是在路由器上进行设置,由于当时做的时候没用Container,因此暂未复现,有待补充。
总结:
- NF_IP_PRE_ROUTING: 接收到的包进入协议栈立即触发此个hook
- NF_IP_LOCAL_IN: 接收到的包经过路由判断,如果目的是本机,将触发此hook
- NF_IP_FORWARD: 接收到的包经过路由判断,如果目的是其他机器,将触发此hook
- NF_IP_LOCAL_OUT:本机产生的准备发送的包,在进入协议栈后立即触发此hook
- NF_IP_POST_ROUTING: 本机产生的准备发送的包或者转发的包,在经过路由的判断之后,将触发此hook
Task3
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).
|
|
防火墙代码实现:
|
|
编译成LKM:make
测试:
-
启动容器,
docker-compose up -d
-
进入hostA,
docksh 40
-
在未装载LKM时,从hostA ping VM以及从hostA telnet VM
均正常。
-
加载:
sudo insmod filter.ko
-
再次重复步骤3,结果如下:
ping已经没有响应。
telnet连接也是超时状态。已经反映了防火墙策略应该生效。
-
进一步查看系统日志:
可以发现防火墙策略确实已经工作,根据输出的日志信息已经将ping自己的数据包均过滤掉了。
telnet同理。
对比日志输出信息,可以发现,由于上述两个钩子函数是挂同一个hook point上的(LOCAL_IN),二者执行顺序的策略是NF_IP_PRI_FIRST,在此例中是过滤telnet连接数据包优先执行。
netfilter允许在同一个hook上注册多个回调函数,并且可以指定不同的优先级。如果第一个函数接受了数据包,那么数据包会被传递给下一个优先级低的函数。如果数据包被一个回调函数丢弃了,那么后面的函数(如果存在)就不会被执行了。
Task 2: Experimenting with Stateless Firewall Rules
A: Protecting the Router
目标:Set up rules to prevent outside machines from accessing the router machine, except ping.
实验:
-
启动容器:
docker-compose up -d
-
进入router container:
docksh 84
-
执行如下iptables命令建立防火墙规则:
1 2 3 4
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT iptables -P OUTPUT DROP ➙Set default rule for OUTPUT iptables -P INPUT DROP ➙Set default rule for INPUT
-
进入10.9.0.5container:
docksh 40
-
从10.9.0.5 ping router:
ping 10.9.0.11
能够ping通。 -
从10.9.0.5 telnet router:
telnet 10.9.0.11
发现连接超时,telnet未成功。
-
解释每条规则的含义
- 第一条对filter表(默认)的INPUT链追加一条规则:对发往本地的ICMP包的echo-request类型均accept放行。
- 第二条对filter表(默认)的OUTPUT链追加一条规则:对从本地发出的ICMP包的echo-reply类型均accept放行。
- 第三条设置输出策略为DROP,也就是拒绝所有的输出流量即系统不能发送任何数据包的到外部。默认策略。
- 第四条设置输入策略为DROP,也就是拒绝所有的输入流量即系统不能接受任何外部数据包。默认策略。
上述ping能成功,icmp输入输出流量均正常是因为前两条规则覆盖了默认策略。
-
清空前述策略,恢复至初始状态进入下一个任务:
1 2 3
iptables -F iptables -P OUTPUT ACCEPT iptables -P INPUT ACCEPT
当然也可以直接重启容器:
docker restart <Container ID>
补充:
-P是iptables的一个选项,它的全称是–policy。它用来设置iptables的默认策略,也就是当没有规则匹配一个数据包时,应该采取什么动作。你可以为每个链(chain)设置不同的默认策略,例如INPUT、OUTPUT或FORWARD。默认策略通常有两种选择:ACCEPT或DROP。ACCEPT表示允许数据包通过,DROP表示拒绝数据包通过。
-F是iptables的一个选项,它的全称是–flush。它用来删除所有的规则,也就是清空iptables的表(table)。这样的话,你的iptables就会恢复到初始状态,只有默认策略起作用。如果你想删除某个特定的链(chain)的规则,你可以指定链的名字,例如INPUT、OUTPUT或FORWARD。
B: Protecting the Internal Network
目标:Set up firewall rules on the router to protect the internal network 192.168.60.0/24
使用FORWARD链
主要是对ICMP流量实施以下限制:
-
外部主机无法ping通内部主机。
-
外部主机可以ping通路由器。
-
内部主机可以ping外部主机。
-
内部网络和外部网络之间的所有其他数据包都应该被阻止
实验:
-
启动容器并进入router
-
构建防火墙
1 2 3 4
iptables -A FORWARD -s 192.168.60.0/24 -p icmp --icmp-type echo-request -j ACCEPT iptables -A FORWARD -d 192.168.60.0/24 -p icmp --icmp-type echo-reply -j ACCEPT iptables -A FORWARD -p icmp --icmp-type echo-request -j DROP iptables -P FORWARD DROP
-
测试
- 外部主机无法ping通内部主机:进入hostA ping内网主机,可以发现长时间无响应。
- 外部主机可以ping通路由器:仍然利用hostA,ping路由器,可以发现能够ping通。
- 内部主机可以ping外部主机:进入host1,ping hostA,可以ping通。
- 内部网络和外部网络之间的所有其他数据包都应该被阻止:从host1 telnet hostA以及从hostA telnet host1,均超时无法成功。
-
清空规则,开始下一个任务。
C: Protecting Internal Servers
目标:
-
所有内部主机都运行一个telnet服务器(监听端口23)。外部主机只能访问位于192.168.60.5的telnet服务器,而不是其他内部主机。
-
外部主机无法访问其他内部服务器。
-
内部主机可以访问所有内部服务器。
-
内部主机无法访问外部服务器。
-
在此任务中,不允许使用连接跟踪机制。它将在后面的任务中使用。
实验:
-
启动容器
-
构建防火墙规则,施加在路由器上
1 2 3
iptables -A FORWARD -i eth0 -p tcp --dport 23 -d 192.168.60.5 -j ACCEPT iptables -A FORWARD -i eth0 -p tcp --dport 23 -j DROP iptables -A FORWARD -i eth1 -p tcp --dport 23 -j DROP
-
测试
- 外部主机只能访问位于192.168.60.5的telnet服务器: 从hostA访问host1
- 外部主机无法访问其他内部服务器:从hostA访问host2,host3
- 内部主机可以访问所有内部服务器:
- 内部主机无法访问外部服务器:host1访问hostA
-
清空规则,开始下一个任务。
Task 3: Connection Tracking and Stateful Firewall
A: Experiment with the Connection Tracking
Tracking connections is achieved by the conntrack mechanism inside the kernel.
实验:
-
ICMP实验
在hostA ping host1:
ping 192.168.60.5
在Router上检查连接追踪信息:
conntrack -L
ICMP连接状态大约会被保留30秒左右。
-
UDP实验
在192.168.60.5建立一个UDP服务器:
nc -lu 9090
在10.9.0.5发送UDP包:
1 2
nc -u 192.168.60.5 9090 <type something, then hit return>
在服务器主机上可以看到发送的UDP包内容:
在Router上检查连接追踪信息:
conntrack -L
UDP连接状态大约也会被保留30秒左右。
-
TCP实验
在192.168.60.5建立一个TCP服务器:
nc -l 9090
在10.9.0.5发送TCP包:
1 2
nc 192.168.60.5 9090 <type something, then hit return>
在服务器主机上可以看到发送的TCP包内容:
在Router上检查连接追踪信息:
conntrack -L
TCP连接自身无时间限制,直到有一方关闭连接。
B: Setting Up a Stateful Firewall
任务: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).
实验:
-
启动容器
-
构建防火墙
1 2 3 4 5 6 7 8
iptables -A FORWARD -i eth0 -p tcp --dport 23 --syn -d 192.168.60.5 -m conntrack --ctstate NEW -j ACCEPT iptables -A FORWARD -i eth1 -p tcp --syn -m conntrack --ctstate NEW -j ACCEPT iptables -A FORWARD -p tcp -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A FORWARD -p tcp -j DROP iptables -P FORWARD ACCEPT
-
测试
- 外部主机只能访问位于192.168.60.5的telnet服务器: 从hostA访问host1
- 外部主机无法访问其他内部服务器:从hostA访问host2,host3
- 内部主机可以访问所有内部服务器:
- 内部主机可以访问外部服务器:
-
清空规则,开始下一个任务。
Task 4: Limiting Network Traffic
In this task, we will use limit module to limit how many packets from 10.9.0.5 are allowed to get into the internal network.
实验:
-
启动容器
-
在路由器上配置防火墙
1 2 3
iptables -A FORWARD -s 10.9.0.5 -m limit --limit 10/minute --limit-burst 5 -j ACCEPT iptables -A FORWARD -s 10.9.0.5 -j DROP
-
从10.9.0.5 ping 192.168.60.5
-
去除第二条规则,再次ping
-
分析
第一个iptables规则意味着来自10.9.0.5的数据包将被FORWARD链以每分钟10个的速率限制接受,突发流量限制为5个。第二条规则意味着来自10.9.0.5的任何其他数据包将被FORWARD链丢弃。
在有第二条规则时,观察上图,从10.9.0.5 ping 192.168.60.5,会发现一些数据包传输成功,一些数据包丢失,这取决于发送它们的速度。
没有第二条规则时,那来自10.9.0.5的所有数据包将被FORWARD链接受,没有任何速率限制或丢弃操作。
因此如果想对来自10.9.0.5的数据包实施严格的速率限制,并防止任何多余的流量通过FORWARD链,则需要第二个规则。
清空规则
Task 5: Load Balancing
In this task, we will use it to load balance three UDP servers running in the internal network.
实验:
使用nth模块:
-
在host1,host2,host3中开启UDP服务器:
nc -luk 8080
-
在路由器配置如下规则
1
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
-
在10.9.0.5发包观察:
1 2
echo hello | nc -u 10.9.0.11 8080 <hit Ctrl-C>
即该规则是配置了当发送一个UDP包到路由器的8080端口时,会看到每三个包中第一个会到达192.168.60.5。
-
继续配置路由器规则,增加如下两条:
1 2 3
iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode nth --every 2 --packet 0 -j DNAT --to-destination 192.168.60.6:8080 iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode nth --every 1 --packet 0 -j DNAT --to-destination 192.168.60.7:8080
-
发包观察三个服务器收到包的情况
连续发送三次,可以观察到第一次的包被分发到了192.168.60.5,第二次的包被分发到了192.168.60.6,第三次的包被分发到了192.168.60.7,往后随着次数增加,每三个包中第一个都是到host1,每2个的第一个是host2,最后一个是host3,实现负载均衡。
即三个服务器流量相同,符合实验目的,负载均衡。
使用random模块:
-
清空前述规则
-
路由器上配置规则
1
iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode random --probability 0.3333 -j DNAT --to-destination 192.168.60.5:8080
-
在10.9.0.5发包观察:
1 2
echo hello | nc -u 10.9.0.11 8080 <hit Ctrl-C>
可以看到该包被分配到了192.168.60.5,实际上是有1/3的概率。
-
继续配置路由器规则,增加如下两条:
1 2 3
iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode random --probability 0.3333 -j DNAT --to-destination 192.168.60.6:8080 iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode random --probability 0.3333 -j DNAT --to-destination 192.168.60.7:8080
-
发包观察三个服务器收到包的情况
同样连续发送三次,可以观察到每个服务器都被分发到了一个,因为当前一共有3个服务器,每个被分发的概率是0.3333,因此每个服务器被分发的概率基本是相同的,总体上分发的流量也相同,实现负载均衡。
(清空规则)
即三个服务器流量基本相同,符合实验目的,负载均衡。