Linux防火墙实验

SeedLab网络安全防火墙实验过程记录整理,对防火墙的理解还是比较浅层,在做的过程中有些仍没把握可能存在些许问题,欢迎指正和探讨!

实验来源:seed-labs

实验环境:SEEDUbuntu 20.04 VM

Environment Setup Using Containers

命令:docker-compose up -d

image-20230208000916665

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//Labsetup/Files/kernel_module/hello.c
#include <linux/module.h>
#include <linux/kernel.h>

int initialization(void){
    printk(KERN_INFO "Hello World!\n");
    return 0;
}

void cleanup(void){
    printk(KERN_INFO "Bye-bye World!.\n");
}

module_init(initialization);
module_exit(cleanup);

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.

1
2
3
4
5
6
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

编译内核模块: image-20230211094607549

生成的内核模块为hello.ko,可以使用下述命令进行模块加载、展示、移除等。

1
2
3
4
5
$ sudo insmod hello.ko (inserting a module)
$ lsmod | grep hello (list modules)
$ sudo rmmod hello (remove the module)
$ dmesg (check the messages)
$ modinfo hello.ko (show information about a Linux Kernel module)

在VM上运行该模块:

  1. 往运行时内核插入该模块: sudo insmod hello.ko

    image-20230211095257088

  2. 展示: lsmod | grep hello

    image-20230211095422286

  3. 从内核中移除特定模块: sudo rmmod hello

    image-20230211095626424

  4. 查看模块运行时输出信息: dmesg

    image-20230211095657475

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. (样例)包过滤模块函数代码解析

     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");
    
  2. 利用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
    

    编译:

    image-20230211223848862

    进行加载:sudo insmod seedFilter.ko

    image-20230211224230152

  3. 验证防火墙起作用

    利用 dig @8.8.8.8 www.example.com 生成UDP包至8.8.8.8(Google DNS服务器)

    image-20230211224719005

    通过上图可看出防火墙正常工作,请求已经被阻止了,超时不能到达。(若未工作,将会得到响应)

    查看系统日志也可以发现LOCAL_OUT钩子已经起作用了,过滤模块对匹配规则的数据包进行了过滤:

    image-20230211235326346

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.

钩子号的宏:

1
2
3
4
5
NF_INET_PRE_ROUTING
NF_INET_LOCAL_IN
NF_INET_FORWARD
NF_INET_LOCAL_OUT
NF_INET_POST_ROUTING

源码设计:

 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
//Files/task1B2/test.c
#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, hook3, hook4, hook5; 

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);
   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_OUT;
   hook1.pf = PF_INET;
   hook1.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook1);

   hook2.hook = printInfo;
   hook2.hooknum = NF_INET_POST_ROUTING;
   hook2.pf = PF_INET;
   hook2.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook2);
   
   hook3.hook = printInfo;
   hook3.hooknum = NF_INET_PRE_ROUTING;
   hook3.pf = PF_INET;
   hook3.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook3);
   
   hook4.hook = printInfo;
   hook4.hooknum = NF_INET_LOCAL_IN;
   hook4.pf = PF_INET;
   hook4.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook4);
   
   hook5.hook = printInfo;
   hook5.hooknum = NF_INET_FORWARD;
   hook5.pf = PF_INET;
   hook5.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook5);

   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);
   nf_unregister_net_hook(&init_net, &hook3);
   nf_unregister_net_hook(&init_net, &hook4);
   nf_unregister_net_hook(&init_net, &hook5);
}

module_init(registerFilter);
module_exit(removeFilter);

MODULE_LICENSE("GPL");

Makefile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
obj-m += test.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 test.ko

rm:
	sudo rmmod test

编译:

image-20230212102804088

加载:sudo insmod test.ko

image-20230212102855163

验证每个钩子函数被调用的条件:

image-20230212104135663

通过分析上图系统日志,当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个钩子被触发。

image-20230212111916230

注:对于触发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

参考:深入理解netfilter的核心原理与实现

image-20230224080503134

img

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).

1
2
ping 10.9.0.1
telnet 10.9.0.1

防火墙代码实现:

 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
//Files/task1B3/filter.c
//注释说明为什么挂载在LOCAL_IN:因为这两个需求的数据包均会从其它主机发往本机,因此必然会经过LOCAL_IN hook
#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 hook_func1(void *priv, struct sk_buff *skb,
                       const struct nf_hook_state *state)
{
   struct iphdr *iph;
   char ip[16] = "10.9.0.1";//simplicity
   u32  ip_addr;
   if (!skb) return NF_ACCEPT;
   iph = ip_hdr(skb);
   in4_pton(ip, -1, (u8 *)&ip_addr, '\0', NULL);//字符串转32位二进制地址

   if (iph->protocol == IPPROTO_ICMP){//filter ICMP Packet(ping)
           printk(KERN_WARNING "*** Dropping %pI4 (ICMP/ping)\n", &(iph->saddr));
           return NF_DROP;
   }
   printk(KERN_INFO "Not ICMP\n");
   return NF_ACCEPT;
}

unsigned int hook_func2(void *priv, struct sk_buff *skb,
                       const struct nf_hook_state *state)
{
   struct iphdr *iph;
   struct tcphdr *tcph;
   char ip[16] = "10.9.0.1";//simplicity
   u32  ip_addr;

   if (!skb) return NF_ACCEPT;
   iph = ip_hdr(skb);
   in4_pton(ip, -1, (u8 *)&ip_addr, '\0', NULL);
   tcph = tcp_hdr(skb);

   if (iph->protocol == IPPROTO_TCP && tcph->dest == htons(23)){
     			 //过滤telnet连接,tcp且端口号23
           printk(KERN_WARNING "*** Dropping %pI4 (TCP/telnet)\n", &(iph->saddr));
           return NF_DROP;
   }
   printk(KERN_INFO "Not TCP\n");
   return NF_ACCEPT;
}


int registerFilter(void) {
   printk(KERN_INFO "Registering filters.\n");

  //两个钩子函数挂在同一个挂载点NF_INET_LOCAL_IN
   hook1.hook = hook_func1;
   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 = hook_func2;
   hook2.hooknum = NF_INET_LOCAL_IN;
   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");

编译成LKM:make

image-20230212133153744

测试:

  1. 启动容器,docker-compose up -d

    image-20230212133350903

  2. 进入hostA,docksh 40

    image-20230212133540716

  3. 在未装载LKM时,从hostA ping VM以及从hostA telnet VM

    image-20230212133642299

    均正常。

  4. 加载: sudo insmod filter.ko

    image-20230212133818651

  5. 再次重复步骤3,结果如下:

    image-20230212134103774

    ping已经没有响应。

    image-20230212134609018

    telnet连接也是超时状态。已经反映了防火墙策略应该生效。

  6. 进一步查看系统日志:

    image-20230212134311617

    可以发现防火墙策略确实已经工作,根据输出的日志信息已经将ping自己的数据包均过滤掉了。

    image-20230212134739186

    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.

实验:

  1. 启动容器:docker-compose up -d

    image-20230222232245463

  2. 进入router container:docksh 84

    image-20230222232318005

  3. 执行如下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
    

    image-20230222232708840

  4. 进入10.9.0.5container:docksh 40

    image-20230222232849112

  5. 从10.9.0.5 ping router:ping 10.9.0.11能够ping通。

    image-20230222233028131

  6. 从10.9.0.5 telnet router:telnet 10.9.0.11

    image-20230222233424293

    发现连接超时,telnet未成功。

  7. 解释每条规则的含义

    • 第一条对filter表(默认)的INPUT链追加一条规则:对发往本地的ICMP包的echo-request类型均accept放行。
    • 第二条对filter表(默认)的OUTPUT链追加一条规则:对从本地发出的ICMP包的echo-reply类型均accept放行。
    • 第三条设置输出策略为DROP,也就是拒绝所有的输出流量即系统不能发送任何数据包的到外部。默认策略。
    • 第四条设置输入策略为DROP,也就是拒绝所有的输入流量即系统不能接受任何外部数据包。默认策略。

    上述ping能成功,icmp输入输出流量均正常是因为前两条规则覆盖了默认策略。

  8. 清空前述策略,恢复至初始状态进入下一个任务:

    1
    2
    3
    
    iptables -F
    iptables -P OUTPUT ACCEPT
    iptables -P INPUT ACCEPT
    

    image-20230223001030135

    当然也可以直接重启容器: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流量实施以下限制:

  1. 外部主机无法ping通内部主机。

  2. 外部主机可以ping通路由器。

  3. 内部主机可以ping外部主机。

  4. 内部网络和外部网络之间的所有其他数据包都应该被阻止

实验:

  1. 启动容器并进入router

    image-20230223080437547

  2. 构建防火墙

    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
    

    image-20230223084300068

  3. 测试

    • 外部主机无法ping通内部主机:进入hostA ping内网主机,可以发现长时间无响应。

    image-20230223082305768

    • 外部主机可以ping通路由器:仍然利用hostA,ping路由器,可以发现能够ping通。

    image-20230223082348708

    • 内部主机可以ping外部主机:进入host1,ping hostA,可以ping通。

    image-20230223084357316

    • 内部网络和外部网络之间的所有其他数据包都应该被阻止:从host1 telnet hostA以及从hostA telnet host1,均超时无法成功。

    image-20230223084758147

    image-20230223085154591

  4. 清空规则,开始下一个任务。

C: Protecting Internal Servers

目标:

  1. 所有内部主机都运行一个telnet服务器(监听端口23)。外部主机只能访问位于192.168.60.5的telnet服务器,而不是其他内部主机。

  2. 外部主机无法访问其他内部服务器。

  3. 内部主机可以访问所有内部服务器。

  4. 内部主机无法访问外部服务器。

  5. 在此任务中,不允许使用连接跟踪机制。它将在后面的任务中使用。

实验:

  1. 启动容器

    image-20230223194949125

  2. 构建防火墙规则,施加在路由器上

    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
    

    image-20230223202255875

  3. 测试

    • 外部主机只能访问位于192.168.60.5的telnet服务器: 从hostA访问host1

    image-20230223202346893

    • 外部主机无法访问其他内部服务器:从hostA访问host2,host3

    image-20230223202703037

    image-20230223203022106

    • 内部主机可以访问所有内部服务器:

    image-20230223203115864

    • 内部主机无法访问外部服务器:host1访问hostA

    image-20230223203416236

  4. 清空规则,开始下一个任务。

Task 3: Connection Tracking and Stateful Firewall

A: Experiment with the Connection Tracking

Tracking connections is achieved by the conntrack mechanism inside the kernel.

实验:

  1. ICMP实验

    在hostA ping host1:ping 192.168.60.5

    在Router上检查连接追踪信息:conntrack -L

    image-20230223204400587

    ICMP连接状态大约会被保留30秒左右。

  2. UDP实验

    在192.168.60.5建立一个UDP服务器:nc -lu 9090

    image-20230223204827566

    在10.9.0.5发送UDP包:

    1
    2
    
    nc -u 192.168.60.5 9090
    <type something, then hit return>
    

    image-20230223205100442

    在服务器主机上可以看到发送的UDP包内容:

    image-20230223205211059

    在Router上检查连接追踪信息:conntrack -L

    image-20230223205422400

    UDP连接状态大约也会被保留30秒左右。

  3. 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>
    

    image-20230223210115514

    在服务器主机上可以看到发送的TCP包内容:

    image-20230223210140522

    在Router上检查连接追踪信息:conntrack -L

    image-20230223210347319

    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. 构建防火墙

    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
    

    image-20230224002139582

  3. 测试

    • 外部主机只能访问位于192.168.60.5的telnet服务器: 从hostA访问host1

    image-20230223221516524

    • 外部主机无法访问其他内部服务器:从hostA访问host2,host3

    image-20230223222253985

    image-20230223222605555

    • 内部主机可以访问所有内部服务器:

    image-20230223222636398

    • 内部主机可以访问外部服务器:

    image-20230224002230530

  4. 清空规则,开始下一个任务。

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. 在路由器上配置防火墙

    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
    

    image-20230223224701337

  3. 从10.9.0.5 ping 192.168.60.5

    image-20230223225057940

  4. 去除第二条规则,再次ping

    image-20230223225152031

  5. 分析

    第一个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模块:

  1. 在host1,host2,host3中开启UDP服务器:nc -luk 8080

  2. 在路由器配置如下规则

    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
    

    image-20230223230807814

  3. 在10.9.0.5发包观察:

    1
    2
    
    echo hello | nc -u 10.9.0.11 8080
    <hit Ctrl-C>
    

    image-20230223231355649即该规则是配置了当发送一个UDP包到路由器的8080端口时,会看到每三个包中第一个会到达192.168.60.5。

  4. 继续配置路由器规则,增加如下两条:

    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
    

    image-20230223235642439

  5. 发包观察三个服务器收到包的情况

    连续发送三次,可以观察到第一次的包被分发到了192.168.60.5,第二次的包被分发到了192.168.60.6,第三次的包被分发到了192.168.60.7,往后随着次数增加,每三个包中第一个都是到host1,每2个的第一个是host2,最后一个是host3,实现负载均衡。

    image-20230224000330750

    image-20230224000350139

    image-20230224000359808

    image-20230224000410048

即三个服务器流量相同,符合实验目的,负载均衡。

使用random模块:

  1. 清空前述规则

  2. 路由器上配置规则

    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
    
  3. 在10.9.0.5发包观察:

    1
    2
    
    echo hello | nc -u 10.9.0.11 8080
    <hit Ctrl-C>
    

    image-20230224000958076

    可以看到该包被分配到了192.168.60.5,实际上是有1/3的概率。

  4. 继续配置路由器规则,增加如下两条:

    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
    

    image-20230224001213613

  5. 发包观察三个服务器收到包的情况

    同样连续发送三次,可以观察到每个服务器都被分发到了一个,因为当前一共有3个服务器,每个被分发的概率是0.3333,因此每个服务器被分发的概率基本是相同的,总体上分发的流量也相同,实现负载均衡。

    image-20230224001519423

    image-20230224001529666

    image-20230224001538967

    (清空规则)

即三个服务器流量基本相同,符合实验目的,负载均衡。

0%