实验环境
我使用的是 Ubuntu 24.04.4,基本环境配置见ARP攻击原理学习
网络结构如下:
| 节点 |
IP 地址 |
作用 |
| 外部主机 |
10.9.0.5 |
用来模拟外部访问者 |
| 路由器 |
10.9.0.1(外网口eth0 )/ 192.168.60.1(内网口eth1) |
作为防火墙 |
| 内部服务器 |
192.168.60.5 |
受防火墙保护 |
启动实验前先清理容器环境,防止被之前的网络配置干扰
1
2
|
sudo docker stop $(sudo docker ps -q) 2>/dev/null
sudo docker system prune -f
|
进入实验目录并启动
1
2
|
cd seed-labs/category-network/Firewall/Labsetup
sudo docker compose up -d
|
打开三个终端分别进入对应的容器
外部主机:
1
|
docker exec -it hostA-10.9.0.5 /bin/bash
|
路由器:
1
|
docker exec -it seed-router /bin/bash
|
内部服务器:
1
|
docker exec -it host1-192.168.60.5 /bin/bash
|
Netfilter 内核级防火墙
防火墙的拦截程序在 seedFilter.c
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
91
92
93
94
95
96
97
98
99
100
101
|
#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;
unsigned int blockTCPICMP(void *priv, struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph;
struct udphdr *udph;
u16 port = 23;
char ip[16] = "10.9.0.1";
u32 ip_addr;
if (!skb) return NF_ACCEPT;
iph = ip_hdr(skb);
// Convert the IPv4 address from dotted decimal to 32-bit binary
in4_pton(ip, -1, (u8 *)&ip_addr, '\0', NULL);
if (iph->protocol == IPPROTO_UDP) {
udph = udp_hdr(skb);
if (iph->daddr == ip_addr && ntohs(udph->dest) == port){
printk(KERN_WARNING "*** Dropping %pI4 (UDP), port %d\n", &(iph->daddr), port);
return NF_DROP;
}
}
return NF_ACCEPT;
}
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); // Print out the hook info
iph = ip_hdr(skb);
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;
}
// Print out the IP addresses and protocol
printk(KERN_INFO " %pI4 --> %pI4 (%s)\n",
&(iph->saddr), &(iph->daddr), protocol);
return NF_ACCEPT;
}
int registerFilter(void) {
printk(KERN_INFO "Registering filters.\n");
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);
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);
MODULE_LICENSE("GPL");
|
由于容器中没有安装make,新开一个终端,在宿主机进入源码目录,这里面包含 Makefile 和 seedFilter.c。在这里对内核进行编译
1
2
|
cd /home/th4uma/seed-labs/category-network/Firewall/Labsetup/volumes/packet_filter
make
|
在路由器容器中进入源码目录
1
|
cd /volumes/packet_filter
|
为了方便观察新的日志,加载模块前先清空 dmesg
1
2
|
dmesg -C
insmod seedFilter.ko
|
用 lsmod 查看模块是否加载成功:
1
|
lsmod | grep seedFilter
|
模块加载后,在外部主机中访问路由器:
1
2
|
ping 10.9.0.1
telnet 10.9.0.1
|
可以看到外部主机无法收到响应,ping 出现 100% 丢包,telnet 也一直卡在连接阶段。
回到路由器中查看内核日志:
日志中出现了 Dropping 10.9.0.1 (ICMP) 和 Dropping 10.9.0.1 (TCP), port 23,说明内核模块已经在底层拦截了发往路由器的 ICMP 和 Telnet 流量。
这个任务和普通应用层过滤不同,拦截动作发生在内核网络协议栈中。只要数据包匹配模块中写好的条件,就会直接返回 NF_DROP,后面的网络栈不会继续处理这个包,所以外部主机表现为收不到任何响应。
无状态防火墙规则
前面的 Netfilter 模块是直接写内核代码进行过滤,这一部分改用 iptables 配置无状态防火墙规则。目标是让外部主机不能主动 Ping 内部服务器,但内部服务器仍然可以正常访问外部主机。
为了避免前面加载的内核模块影响后续实验,可以先在路由器中卸载模块并清理旧规则:
1
2
|
rmmod seedFilter
iptables -F
|
接着在路由器中配置 FORWARD 链。FORWARD 链负责处理经过路由器转发的数据包,外部主机访问内部服务器、内部服务器访问外部主机,都属于转发流量。
1
2
3
4
|
iptables -A FORWARD -i eth0 -p icmp --icmp-type echo-request -j DROP
iptables -A FORWARD -i eth1 -p icmp --icmp-type echo-request -j ACCEPT
iptables -A FORWARD -p icmp --icmp-type echo-reply -j ACCEPT
iptables -P FORWARD DROP
|
这几条规则的含义是:
- 从外网口
eth0 进入的 ICMP echo-request 直接丢弃,也就是阻止外部主机主动 Ping 内部服务器;
- 从内网口
eth1 进入的 ICMP echo-request 允许通过,也就是允许内部服务器主动 Ping 外部主机;
- ICMP echo-reply 允许通过,保证内部主动发起的 Ping 能收到响应;
- 默认策略设置为
DROP,没有匹配到规则的转发流量全部丢弃。
在外部主机中尝试 Ping 内部服务器:
可以看到外部主机访问内部服务器失败,出现 100% 丢包。再从内部服务器向外部方向发起 Ping:
内部方向的访问可以正常收到响应,说明规则实现了简单的单向访问控制。这个规则集本身没有记录连接状态,只是根据入口网卡、协议类型和 ICMP 类型进行匹配,所以属于无状态过滤。
网络流量限速
除了直接允许或拒绝数据包,防火墙还可以限制某类流量的速率。这里使用 iptables 的 limit 模块限制来自 10.9.0.5 的 ICMP 流量,用来模拟对 ICMP 泛洪的防护。
先在路由器中清空之前的 FORWARD 规则:
然后添加限速规则:
1
2
3
|
iptables -A FORWARD -s 10.9.0.5 -p icmp -m limit --limit 10/minute --limit-burst 5 -j ACCEPT
iptables -A FORWARD -s 10.9.0.5 -p icmp -j DROP
iptables -A FORWARD -j ACCEPT
|
第一条规则表示允许源地址为 10.9.0.5 的 ICMP 流量,但速率限制为每分钟 10 个包,并允许最开始有 5 个包的突发流量。第二条规则会丢弃同一源地址剩余的 ICMP 包。第三条规则放行其他流量,防止实验过程中把无关通信一起阻断。
在外部主机中连续 Ping 内部服务器:
从结果可以看到,前 5 个包连续通过,这是 --limit-burst 5 允许的突发流量。之后返回包的序号变成 7、13、19、25、31,中间的包被防火墙丢弃,基本符合每分钟 10 个包,也就是大约 6 秒通过 1 个包的限制。
实验总结
这次实验主要对防火墙的三种实现方式进行了验证。
Netfilter 模块是在内核层直接拦截数据包,灵活性高,但需要编写和加载内核模块,操作风险也更高。iptables 则是在已有框架上配置规则,更适合日常管理和实验验证。无状态规则可以根据源地址、入口网卡、协议类型等字段快速决定是否放行,但它不理解连接上下文,只能做比较简单的访问控制。limit 模块进一步说明防火墙不仅能阻断流量,也可以对流量进行速率控制,从而减轻简单的泛洪攻击影响。
从结果看,外部主机无法主动访问内部服务器,内部服务器仍可以访问外部主机;限速规则也成功把连续 ICMP 流量控制在指定速率内,说明实验中的防火墙规则生效。