网络篇-深入理解防火墙工作原理

image.png

背景

防火墙这个名词相信我们都不陌生,在现实生活中我们在地下车库经常会看到下面这种防火卷帘门,这就是我们房子的防火墙(门),其作用是当发生火灾时隔绝火的蔓延,降低损失。
image.png
现实中的防火墙了解了,下面我们来了解了解计算机里的防火墙

计算机防火墙的概述

计算机防火墙用于保护计算机和网络免受来自互联网或局域网内部的未经授权的访问、攻击、病毒和恶意软件的侵害。防火墙通过监控网络流量,识别和过滤掉非法或有害的流量,从而防止未经授权的访问和攻击。它通常使用规则集来指定哪些流量应该允许通过,哪些流量应该被阻止或拒绝,以及如何响应不同类型的攻击和安全事件。

防火墙的主要功能包括:

  1. 包过滤:基于IP地址、端口号、协议类型等特定信息,过滤不符合规则的数据包。
  2. 状态检测:对进出网络的连接状态进行检测,以防止恶意攻击。
  3. NAT(网络地址转换):将私有IP地址转换为公网IP地址,保护内网不受外部攻击。
  4. VPN(虚拟专用网络)支持:为远程用户提供安全的访问方式。
  5. 审计日志:记录网络流量和安全事件,以便后续分析和调查。

为什么需要防火墙

  1. 保护网络安全:随着互联网的发展和普及,网络攻击和病毒传播的风险也越来越高。防火墙可以帮助企业和个人用户保护网络安全,避免遭受网络攻击和病毒感染,从而确保业务和个人信息的安全性。

  2. 控制网络访问:防火墙可以对网络流量进行监控和控制,根据特定的规则来允许或拒绝不同来源的流量,防止未经授权的访问和攻击。

  3. 网络隔离:防火墙可以将网络分割为不同的安全区域,从而限制内部和外部网络之间的数据交换。这有助于防止内部网络的机密数据被未经授权的用户访问和泄露。

  4. 提高网络性能:防火墙可以过滤掉无用的流量和恶意流量,从而减少网络拥塞和传输延迟,提高网络性能和稳定性。

防火墙的分类

防火墙可以分为软件防火墙硬件防火墙。软件防火墙运行在操作系统内部,通常作为软件程序安装在计算机上。硬件防火墙是一种独立设备,通常放置在网络边缘或网关位置,用于过滤网络流量和提供更高级别的安全保护。

根据不同的分类标准,防火墙可以分为以下几种类型:

  1. 包过滤型防火墙:包过滤型防火墙是最早的防火墙类型,它基于网络层(OSI模型中的第三层)对数据包进行过滤和控制,通过过滤不合法的数据包来阻止未授权的访问和攻击。包过滤型防火墙可以基于源地址、目标地址、端口号、协议类型等特定信息来判断是否允许数据包通过,但它无法检测应用层的攻击和恶意软件。

  2. 应用层防火墙:应用层防火墙是基于应用层(OSI模型中的第七层)对数据流进行过滤和控制的防火墙类型。它可以识别和过滤掉应用层的攻击和恶意流量,如SQL注入、跨站点脚本(XSS)等,从而提高网络的安全性。

  3. 状态检测型防火墙:状态检测型防火墙是基于连接状态对数据流进行检测和控制的防火墙类型。它可以识别并防止来自已建立连接的恶意流量和攻击,从而提高网络的安全性和性能。

  4. 混合型防火墙:混合型防火墙将包过滤型防火墙、应用层防火墙和状态检测型防火墙相结合,综合了它们的优点,可以提供更全面的安全保护和网络性能优化。

  5. 下一代防火墙:下一代防火墙(NGFW)是基于深度包检测技术的新一代防火墙,可以识别和过滤掉更复杂的攻击和恶意流量,如嵌入式恶意软件、高级持续性威胁(APTs)等,从而提供更全面和高效的网络安全保护。同时,NGFW还支持网络入侵检测和防御(NIDS)和虚拟专用网络(VPN)等功能。

linux下的防火墙

Linux下的防火墙有很多种,些常见的有:

  1. iptables:iptables是Linux中最常用的防火墙程序之一,它基于内核层面实现,可以对数据包进行过滤、转发和修改,同时支持多种网络协议和表达式,具有灵活性和高效性。
  2. nftables:nftables是iptables的继任者,它提供了更强大的规则语言和数据结构,同时支持多种网络协议和表达式,可以更精确地控制和管理网络流量。
  3. firewalld:firewalld是Red Hat和CentOS中预安装的防火墙程序,它基于iptables和nftables,提供了基于服务、区域和接口的防火墙规则管理,可以更加简单和安全地配置和管理防火墙。
  4. ufw:ufw是Ubuntu中预安装的简单防火墙程序,它基于iptables,提供了易于使用的命令行界面,可以轻松地配置和管理基本的防火墙规则。
  5. Shorewall:Shorewall是一个高级的防火墙管理程序,它基于iptables和nftables,提供了多种规则和策略,可以实现灵活的网络流量控制和安全保护。

防火墙的工作原理

防火墙的主要工作原理是通过策略和规则来过滤和控制网络流量,从而防止未授权的访问和攻击。其具体工作过程如下:

  1. 流量过滤:防火墙会监视网络流量,并将数据包与预定义的安全策略进行比较,根据规则对数据包进行过滤和分类,如允许、拒绝或转发。
  2. 策略管理:防火墙管理员可以定义和管理防火墙策略,包括源和目标IP地址、端口号、协议类型等,以及允许或拒绝流量的规则。
  3. 状态管理:防火墙会维护一个状态表来跟踪已建立的连接状态,并根据规则检测和拦截恶意流量和攻击。
  4. NAT和端口转发:防火墙还可以支持网络地址转换(NAT)和端口转发等功能,以实现内部和外部网络之间的通信。
  5. 日志和报告:防火墙还可以生成日志和报告,记录网络流量和事件,以便管理员对网络安全进行跟踪和分析。

Iptables

简单介绍

iptables是Linux操作系统上的一种软件工具,用于管理网络中的数据包流动,通过检查每个数据包并根据定义的规则执行相应的操作来控制网络流量。

基本用法

查看当前iptables规则:可以使用以下命令来查看当前iptables规则:

1
iptables -L

清除当前iptables规则:可以使用以下命令来清除当前iptables规则:

1
iptables -F

设置iptables规则:可以使用以下命令来设置iptables规则,其中-A表示添加规则,-p表示协议,--dport表示目标端口,-j表示动作,ACCEPT表示接受,DROP表示丢弃:

1
2
3
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -j DROP

保存iptables规则:可以使用以下命令将当前iptables规则保存到文件中:

1
iptables-save > /etc/sysconfig/iptables

加载iptables规则:可以使用以下命令将保存的iptables规则加载到系统中:

1
iptables-restore < /etc/sysconfig/iptables

删除iptables规则:可以使用以下命令删除指定的iptables规则,其中-D表示删除规则,INPUT表示链名,1表示规则序号:

1
iptables -D INPUT 1

修改iptables规则:可以使用以下命令修改指定的iptables规则,其中-R表示替换规则,INPUT表示链名,1表示规则序号,-p表示协议,--dport表示目标端口,-j表示动作,ACCEPT表示接受:

1
iptables -R INPUT 1 -p tcp --dport 80 -j ACCEPT

暂停iptables规则:可以使用以下命令暂停iptables规则,其中-F表示清空所有规则,-X表示删除所有用户自定义链,-Z表示重置所有计数器:

1
2
3
4
r
iptables -F
iptables -X
iptables -Z

禁用iptables规则:可以使用以下命令禁用iptables规则,其中-P表示设置默认策略,INPUT表示链名,DROP表示默认动作:

1
iptables -P INPUT DROP

允许回环网络:可以使用以下命令允许回环网络,其中-A表示添加规则,INPUT表示链名,-i表示输入接口,lo表示回环接口,-j表示动作,ACCEPT表示接受:

1
iptables -A INPUT -i lo -j ACCEPT

限制IP地址范围:可以使用以下命令限制IP地址范围,其中-A表示添加规则,INPUT表示链名,-s表示源地址,192.168.0.0/24表示IP地址范围,-j表示动作,DROP表示丢弃:

1
iptables -A INPUT -s 192.168.0.0/24 -j DROP

限制连接数:可以使用以下命令限制连接数,其中-A表示添加规则,INPUT表示链名,-p表示协议,--syn表示只匹配SYN数据包,-m connlimit表示连接数限制模块,--connlimit-above表示连接数超过指定值,3表示连接数上限,-j表示动作,DROP表示丢弃:

1
iptables -A INPUT -p tcp --syn -m connlimit --connlimit-above 3 -j DROP

限制连接速率:可以使用以下命令限制连接速率,其中-A表示添加规则,INPUT表示链名,-p表示协议,--dport表示目标端口,-m limit表示速率限制模块,--limit表示速率上限,10/s表示每秒最多允许10个连接,-j表示动作,ACCEPT表示接受:

1
iptables -A INPUT -p tcp --dport 80 -m limit --limit 10/s -j ACCEPT

使用用户自定义链:可以使用以下命令创建用户自定义链,其中-N表示创建链,LOGDROP表示链名:

1
iptables -N LOGDROP

然后可以在该链中添加规则,最后将该链添加到INPUT链中:

1
2
3
iptables -A LOGDROP -j LOG --log-prefix "Dropped by LOGDROP: "
iptables -A LOGDROP -j DROP
iptables -I INPUT -j LOGDROP

这样,当INPUT链中的规则匹配到该链时,将跳转到LOGDROP链中执行相应的规则,从而实现用户自定义的功能。

使用iptables防DDoS攻击:可以使用以下命令限制每个IP地址的连接数和速率,从而防止DDoS攻击:

1
2
3
4
iptables -N DDoS
iptables -A DDoS -m recent --name DDoS --set
iptables -A DDoS -m recent --name DDoS --update --seconds 60 --hitcount 50 -j DROP
iptables -I INPUT -p tcp --dport 80 -m state --state NEW -j DDoS

以上命令将创建一个名为DDoS的用户自定义链,用于限制每个IP地址在60秒内最多只能发起50个新连接。如果超过了这个限制,将被DROP动作丢弃。最后将该链添加到INPUT链中。

  1. 保存和恢复规则:可以使用以下命令保存和恢复iptables规则,其中-L表示列出规则,-n表示不解析IP地址和端口号,> rules表示将规则保存到文件中,< rules表示从文件中恢复规则:

保存规则:

1
iptables -L -n > rules

恢复规则:

1
iptables-restore < rules

清除规则:可以使用以下命令清除iptables规则,其中-F表示清空所有规则,-X表示删除所有用户自定义链,-Z表示将所有计数器归零:

1
2
3
iptables -F
iptables -X
iptables -Z

永久保存规则:可以使用以下命令将当前规则保存到永久规则中,以便重启后仍然有效,其中iptables-save命令用于保存规则,iptables-restore命令用于恢复规则:

1
2
iptables-save > /etc/sysconfig/iptables
iptables-restore < /etc/sysconfig/iptables

配置端口转发:可以使用以下命令配置端口转发,其中-t nat表示使用NAT表,-A PREROUTING表示添加PREROUTING链规则,-i eth0表示进入网络接口,--dport 80表示目标端口,-j DNAT表示目标地址转换,--to-destination 192.168.0.2:80表示目标IP地址和端口:

1
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.0.2:80

配置源地址转换:可以使用以下命令配置源地址转换,其中-t nat表示使用NAT表,-A POSTROUTING表示添加POSTROUTING链规则,-s 192.168.0.0/24表示源地址,-o eth0表示出去网络接口,-j MASQUERADE表示地址伪装:

1
iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o eth0 -j MASQUERADE

配置多网卡负载均衡:可以使用以下命令配置多网卡负载均衡,其中-t mangle表示使用MANGLE表,-A PREROUTING表示添加PREROUTING链规则,-i eth0表示进入网络接口,-j MARK表示标记数据包,--set-mark 1表示标记为1,-A PREROUTING表示添加PREROUTING链规则,-i eth1表示进入网络接口,-j MARK表示标记数据包,--set-mark 2表示标记为2,-t nat表示使用NAT表,-A POSTROUTING表示添加POSTROUTING链规则,-o eth0表示出去网络接口,-j SNAT表示源地址转换,--to-source 192.168.0.2表示目标IP地址:

1
2
3
4
iptables -t mangle -A PREROUTING -i eth0 -j MARK --set-mark 1
iptables -t mangle -A PREROUTING -i eth1 -j MARK --set-mark 2
iptables -t nat -A POSTROUTING -o eth0 -m mark --mark 1 -j SNAT --to-source 192.168.0.2
iptables -t nat -A POSTROUTING -o eth0 -m mark --mark 2 -j SNAT --to-source 192.168.0.3

配置DDoS防护:可以使用以下命令配置DDoS防护,其中-A INPUT表示添加INPUT链规则,-p tcp表示传输层协议为TCP,-m connlimit --connlimit-above 20表示连接数超过20个,--connlimit-mask 32表示对所有源地址生效,-j DROP表示丢弃数据包:

1
iptables -A INPUT -p tcp -m connlimit --connlimit-above 20 --connlimit-mask 32 -j DROP

配置SSH远程访问:可以使用以下命令配置SSH远程访问,其中-A INPUT表示添加INPUT链规则,-p tcp表示传输层协议为TCP,--dport 22表示目标端口为22,-j ACCEPT表示允许数据包通过:

1
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

配置HTTP访问控制:可以使用以下命令配置HTTP访问控制,其中-A INPUT表示添加INPUT链规则,-p tcp表示传输层协议为TCP,--dport 80表示目标端口为80,-m state --state NEW表示只允许新连接,-m limit --limit 100/minute --limit-burst 100表示限制每分钟连接数为100个,-j ACCEPT表示允许数据包通过:

1
iptables -A INPUT -p tcp --dport 80 -m state --state NEW -m limit --limit 100/minute --limit-burst 100 -j ACCEPT

配置反向代理:可以使用以下命令配置反向代理,其中-t nat表示使用NAT表,-A PREROUTING表示添加PREROUTING链规则,-p tcp表示传输层协议为TCP,--dport 80表示目标端口为80,-j REDIRECT --to-port 8080表示将数据包重定向到端口8080:

1
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080

工作原理

工作原理如下:

  1. 首先,iptables检查接收到的每个数据包,以查看其目标地址和端口号是否与已定义的规则匹配。
  2. 如果数据包匹配了规则,则iptables将根据规则定义的操作执行相应的动作,例如允许或阻止数据包。
  3. 如果数据包未匹配任何规则,则iptables将采取默认操作。默认操作通常是允许或拒绝数据包。
  4. 在iptables中,规则由规则链组成,每个规则链都是一系列规则的集合。规则链可以是系统预定义的,也可以是用户定义的。
  5. 当数据包到达系统时,它将通过规则链顺序进行匹配,直到找到匹配的规则。如果没有匹配的规则,则使用默认规则。
  6. iptables还支持网络地址转换(NAT)和端口地址转换(PAT)。NAT和PAT可以在不更改数据包内容的情况下修改数据包的源地址和目标地址。

Netfilter框架

一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。Netfilter 是一个由Linux 内核提供的框架,可以进行多种网络相关的自定义操作。

Netfilter 负责在内核中执行各种挂接的规则,运行在内核模式中;而 iptables 是在用户模式下运行的进程,负责协助和维护内核中 Netfilter 的各种规则表 。二者相互配合来实现整个 Linux 网络协议栈中灵活的数据包处理机制。

调用链路

iptables 的钩子分为五个阶段:PREROUTING、INPUT、FORWARD、OUTPUT 和 POSTROUTING

1
2
3
4
5
6
7
static const char *const hooknames[] = {
[NF_INET_PRE_ROUTING] = "PREROUTING",
[NF_INET_LOCAL_IN] = "INPUT",
[NF_INET_FORWARD] = "FORWARD",
[NF_INET_LOCAL_OUT] = "OUTPUT",
[NF_INET_POST_ROUTING] = "POSTROUTING",
};
  1. PREROUTING 链:在数据包到达本机之前,内核会先进入 PREROUTING 链,可以对数据包的目的地地址进行修改或重定向。在这个阶段可以做 DNAT(目标地址转换),例如将外网访问本机的 IP 地址转换为内网机器的 IP 地址,实现端口映射等功能。
  2. INPUT 链:在数据包到达本机之后,内核会进入 INPUT 链,可以对数据包进行进一步的处理和过滤。在这个阶段可以进行数据包的过滤、连接追踪、一些协议处理等。
  3. FORWARD 链:当数据包不是本机的目的地地址时,内核会进入 FORWARD 链,可以对数据包进行转发。在这个阶段可以对数据包进行路由、过滤等处理。
  4. OUTPUT 链:在数据包从本机出去之前,内核会进入 OUTPUT 链,可以对数据包进行修改或重定向。在这个阶段可以做 SNAT(源地址转换),例如将本机的 IP 地址转换为另一个 IP 地址,实现 IP 地址伪装等功能。
  5. POSTROUTING 链:在数据包从本机出去之后,内核会进入 POSTROUTING 链,可以对数据包的源地址进行修改或重定向。在这个阶段可以做 SNAT(源地址转换)、MASQUERADE(伪装网关地址)、一些连接追踪等。

从ip_rcv()函数开始分析iptables处理数据包的过程:

  1. ip_rcv()函数:接收数据包,然后交给网络层进行处理。
  2. ip_local_deliver_finish()函数:处理数据包的协议头,根据目标IP地址找到对应的网络设备,并将数据包发送到数据链路层进行发送。
  3. nf_hook_slow()函数:iptables将hook函数注册到内核的网络层中,当数据包经过相应的处理阶段时,hook函数将被调用。
  4. nf_iterate()函数:当数据包经过hook函数时,内核将调用nf_iterate()函数来依次执行所有注册到该hook的iptables规则。
  5. do_builtin_nf_hook()函数:nf_iterate()函数会根据iptables规则中的匹配条件和动作来执行相应的操作,其中一部分规则是内置的iptables规则,这些规则由do_builtin_nf_hook()函数来执行。
  6. ip_local_out()函数:当数据包需要从本地计算机发送到网络上时,iptables将通过OUTPUT链来处理数据包。在OUTPUT链中,数据包会经过ip_local_out()函数进行处理,该函数会将数据包发送到数据链路层进行发送。
  7. ip_forward()函数:当数据包需要转发到其他计算机时,iptables将通过FORWARD链来处理数据包。在FORWARD链中,数据包会经过ip_forward()函数进行处理,该函数会进行路由选择,并将数据包转发到目标网络设备的数据链路层进行发送。
  8. ip_output()函数:在数据包经过iptables的处理后,会进入内核的网络层进行进一步的处理,ip_output()函数会负责将数据包发送到数据链路层进行发送。

ip_rcv

  1. ip_rcv接收数据函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /*
    * IP receive entry point
    */
    int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,struct net_device *orig_dev){
    struct net *net = dev_net(dev);// 获取网络设备所在的命名空间;
    skb = ip_rcv_core(skb, net);//IP数据包主处理函数,数据包合法性检测等
    if (skb == NULL)return NET_RX_DROP;
    //数据包转发之前,先进行netfilter检测,触发NF_INET_PRE_ROUTING,处理完成进入 ip_rcv_finish 交付数据给上层协议
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,net, NULL, skb, dev, NULL,ip_rcv_finish);
    }
  2. 1 处理IP 数据包
    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
    102
    103
    /*
    * Main IP Receive routine.
    */
    static struct sk_buff *ip_rcv_core(struct sk_buff *skb, struct net *net){
    const struct iphdr *iph;
    int drop_reason;
    u32 len;

    /* When the interface is in promisc. mode, drop all the crap
    * that it receives, do not try to analyse it.
    */
    /**
    * 判断数据包的类型是否为PACKET_OTHERHOST,如果是则增加一个与网络设备相关的统计值,
    * 并且设置drop_reason为SKB_DROP_REASON_OTHERHOST,跳转到drop标签执行相关的清理工作。
    */
    if (skb->pkt_type == PACKET_OTHERHOST) {
    dev_core_stats_rx_otherhost_dropped_inc(skb->dev);
    drop_reason = SKB_DROP_REASON_OTHERHOST;
    goto drop;
    }
    //更新IP层的统计信息,其中net指向网络命名空间,IPSTATS_MIB_IN表示接收到的IP数据包数量,skb->len表示接收到的数据包的长度。
    __IP_UPD_PO_STATS(net, IPSTATS_MIB_IN, skb->len);
    //检查数据包是否可共享,如果不能共享则丢弃数据包并更新统计信息,跳转到out标签。
    skb = skb_share_check(skb, GFP_ATOMIC);
    if (!skb) {
    __IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);
    goto out;
    }
    /**
    * 将drop_reason设置为SKB_DROP_REASON_NOT_SPECIFIED,调用一个内核函数pskb_may_pull检查数据包是否能够正确地提取出IP头部,
    * 如果不能,则跳转到inhdr_error标签。
    */
    drop_reason = SKB_DROP_REASON_NOT_SPECIFIED;
    if (!pskb_may_pull(skb, sizeof(struct iphdr)))
    goto inhdr_error;

    /**
    * 获取指向IP头部的指针,判断IP头部中的版本号是否为IPv4且头部长度是否大于等于5个32位字(20个字节),如果不符合条件,则跳转到inhdr_error标签。
    */
    iph = ip_hdr(skb);
    if (iph->ihl < 5 || iph->version != 4)
    goto inhdr_error;

    BUILD_BUG_ON(IPSTATS_MIB_ECT1PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_1);
    BUILD_BUG_ON(IPSTATS_MIB_ECT0PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_0);
    BUILD_BUG_ON(IPSTATS_MIB_CEPKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_CE);
    __IP_ADD_STATS(net,
    IPSTATS_MIB_NOECTPKTS + (iph->tos & INET_ECN_MASK),
    max_t(unsigned short, 1, skb_shinfo(skb)->gso_segs));

    if (!pskb_may_pull(skb, iph->ihl*4))
    goto inhdr_error;

    /**
    * iph 是一个指向 struct iphdr 结构体的指针,用于指向当前传输层数据包中的 IP 头部。
    */
    iph = ip_hdr(skb);

    if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
    goto csum_error;
    //长度
    len = iph_totlen(skb, iph);
    if (skb->len < len) {
    drop_reason = SKB_DROP_REASON_PKT_TOO_SMALL;
    __IP_INC_STATS(net, IPSTATS_MIB_INTRUNCATEDPKTS);
    goto drop;
    } else if (len < (iph->ihl*4))
    goto inhdr_error;
    /* Our transport medium may have padded the buffer out. Now we know it
    * is IP we can trim to the true length of the frame.
    * Note this now means skb->len holds ntohs(iph->tot_len).
    */
    //获取真实长度
    if (pskb_trim_rcsum(skb, len)) {
    __IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);
    goto drop;
    }

    iph = ip_hdr(skb);
    skb->transport_header = skb->network_header + iph->ihl*4;

    /* Remove any debris in the socket control block */
    memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
    IPCB(skb)->iif = skb->skb_iif;

    /* Must drop socket now because of tproxy. */
    if (!skb_sk_is_prefetched(skb))
    skb_orphan(skb);

    return skb;

    csum_error:
    drop_reason = SKB_DROP_REASON_IP_CSUM;
    __IP_INC_STATS(net, IPSTATS_MIB_CSUMERRORS);
    inhdr_error:
    if (drop_reason == SKB_DROP_REASON_NOT_SPECIFIED)
    drop_reason = SKB_DROP_REASON_IP_INHDR;
    __IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
    drop:
    kfree_skb_reason(skb, drop_reason);
    out:
    return NULL;
    }
  3. 2 触发 NF_INET_PRE_ROUTING 钩子通过后 执行 ip_rcv_finish
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /**
    * (主线1.2:交付数据) IP 数据包接收完成的函数,它会将接收到的数据包交给上层协议进行处理
    * @param net 当前网络命名空间
    * @param sk 套接字
    * @param skb 数据包缓冲区
    * @return
    */
    static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
    {
    struct net_device *dev = skb->dev;//获取网络驱动
    int ret;
    /**
    * 如果 ingress 设备已被绑定到 L3 主设备,则会将 skb 传递到其处理程序进行处理。
    * 如果 skb 为 NULL,则表示此数据包已经处理完成,直接返回 NET_RX_SUCCESS。
    */
    skb = l3mdev_ip_rcv(skb);
    if (!skb)return NET_RX_SUCCESS;
    //内核函数,实现了 IP 数据包的核心处理逻辑。
    ret = ip_rcv_finish_core(net, sk, skb, dev, NULL);
    //如果返回值不是 NET_RX_DROP,则将 skb 传递给目的地址输入路径的处理函数 dst_input() 进行处理。最后返回处理结果。
    if (ret != NET_RX_DROP)
    ret = dst_input(skb);//dst_input() 函数是由路由子系统实现的,主要用于将数据包传递给相应的上层协议处理函数。
    return ret;
    }
  4. 3 调用 ip_rcv_finish_core 处理数据报同时解析路由
    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
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119

    /**
    * 主线: 完成 IP 数据包的处理并将其交付给上层协议进行处理
    * @param net 网络命名空间指针
    * @param sk 指向套接字缓冲区的指针
    * @param skb 指向 sk_buff 结构体的指针,表示接收到的 IP 数据包。
    * @param dev 指向网络接口设备的指针。
    * @param hint 指向一个用于提示 IP 路由器查找路由信息的数据包
    * @return 处理结果
    */
    static int ip_rcv_finish_core(struct net *net, struct sock *sk,struct sk_buff *skb, struct net_device *dev,const struct sk_buff *hint)
    {
    const struct iphdr *iph = ip_hdr(skb);
    int err, drop_reason;
    struct rtable *rt;

    drop_reason = SKB_DROP_REASON_NOT_SPECIFIED;

    //判断hint 是否可以被使用,如果可用则使用 ip_route_use_hint() 函数来获取到一个匹配的路由表项。
    if (ip_can_use_hint(skb, iph, hint)) {
    err = ip_route_use_hint(skb, iph->daddr, iph->saddr, iph->tos,dev, hint);
    if (unlikely(err))goto drop_error;
    }
    //判断了内核是否启用了 ip_early_demux 功能,它的作用是对TCP和UDP报文进行早期的协议解析,以便提高网络性能。
    if (READ_ONCE(net->ipv4.sysctl_ip_early_demux) &&
    !skb_dst(skb) &&
    !skb->sk &&
    !ip_is_fragment(iph)) {
    //根据协议类型进行早期解析
    switch (iph->protocol) {
    case IPPROTO_TCP:
    if (READ_ONCE(net->ipv4.sysctl_tcp_early_demux)) {
    tcp_v4_early_demux(skb);

    /* must reload iph, skb->head might have changed */
    iph = ip_hdr(skb);
    }
    break;
    case IPPROTO_UDP:
    if (READ_ONCE(net->ipv4.sysctl_udp_early_demux)) {
    err = udp_v4_early_demux(skb);
    if (unlikely(err))
    goto drop_error;

    /* must reload iph, skb->head might have changed */
    iph = ip_hdr(skb);
    }
    break;
    }
    }
    /*
    * Initialise the virtual path cache for the packet. It describes
    * how the packet travels inside Linux networking.
    */
    //如果当前 skb 没有解析出来的路由信息,就调用 ip_route_input_noref() 函数初始化虚拟路径缓存,并且读取目的
    if (!skb_valid_dst(skb)) {
    //会根据不同的类型进行给 skb_dst(skb)->input 赋值
    err = ip_route_input_noref(skb, iph->daddr, iph->saddr,iph->tos, dev);
    if (unlikely(err))
    goto drop_error;
    } else {//否则,表示数据包已经被路由过,那么就直接从缓存中获取相应的路由信息。
    struct in_device *in_dev = __in_dev_get_rcu(dev);
    if (in_dev && IN_DEV_ORCONF(in_dev, NOPOLICY))
    IPCB(skb)->flags |= IPSKB_NOPOLICY;
    }

    /**
    * 这段代码是一个可选的路由分类器(route classifier),仅当内核编译配置了 CONFIG_IP_ROUTE_CLASSID 选项时才会被编译。
    */
    #ifdef CONFIG_IP_ROUTE_CLASSID
    if (unlikely(skb_dst(skb)->tclassid)) {
    struct ip_rt_acct *st = this_cpu_ptr(ip_rt_acct);
    u32 idx = skb_dst(skb)->tclassid;
    st[idx&0xFF].o_packets++;
    st[idx&0xFF].o_bytes += skb->len;
    st[(idx>>16)&0xFF].i_packets++;
    st[(idx>>16)&0xFF].i_bytes += skb->len;
    }
    #endif

    //处理 IP 头部的选项。如果 IP 头部的选项长度大于 5,并且选项处理函数 ip_rcv_options() 返回一个非零值,
    // 则说明该数据包包含无法处理的选项,将该数据包丢弃。
    if (iph->ihl > 5 && ip_rcv_options(skb, dev))
    goto drop;
    //获取路由表
    rt = skb_rtable(skb);
    /**
    * 这段代码用于处理广播和组播数据包,根据数据包的路由类型和数据链路类型来执行相应的处理操作。
    * 如果数据包是组播数据包,则更新组播接收统计数据;
    * 如果数据包是广播数据包,则更新广播接收统计数据;
    * 如果数据包是数据链路层广播或组播
    */
    if (rt->rt_type == RTN_MULTICAST) {
    __IP_UPD_PO_STATS(net, IPSTATS_MIB_INMCAST, skb->len);
    } else if (rt->rt_type == RTN_BROADCAST) {
    __IP_UPD_PO_STATS(net, IPSTATS_MIB_INBCAST, skb->len);
    } else if (skb->pkt_type == PACKET_BROADCAST ||
    skb->pkt_type == PACKET_MULTICAST) {
    struct in_device *in_dev = __in_dev_get_rcu(dev);
    if (in_dev &&
    IN_DEV_ORCONF(in_dev, DROP_UNICAST_IN_L2_MULTICAST)) {
    drop_reason = SKB_DROP_REASON_UNICAST_IN_L2_MULTICAST;
    goto drop;
    }
    }
    //返回处理成功
    return NET_RX_SUCCESS;

    drop:
    kfree_skb_reason(skb, drop_reason);
    return NET_RX_DROP;

    drop_error:
    if (err == -EXDEV) {
    drop_reason = SKB_DROP_REASON_IP_RPFILTER;
    __NET_INC_STATS(net, LINUX_MIB_IPRPFILTER);
    }
    goto drop;
    }
    通过查询路由表,查找目标 IP 地址的下一跳地址,并返回对应的输入接口。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int ip_route_input_noref(struct sk_buff *skb, __be32 daddr, __be32 saddr,u8 tos, struct net_device *dev)
    {
    struct fib_result res;
    int err;
    tos &= IPTOS_RT_MASK;
    rcu_read_lock();
    //在查找过程中使用了 RCU(Read-Copy-Update)机制,以保证在路由表更新时不会影响正在进行路由查找的数据包,而不需要使用锁机制。
    err = ip_route_input_rcu(skb, daddr, saddr, tos, dev, &res);
    rcu_read_unlock();
    return err;
    }
    EXPORT_SYMBOL(ip_route_input_noref);
    调用链路
    1
    2
    3
    4
    5
    6
    ip_route_input_noref
    ->
    ip_route_input_mc
    ->
    skb_dst_set(skb, &rth->dst);//设置skb的目标路由信息。在执行路由查找后,内核会将路由信息存储在skb的dst字段中。skb_dst_set()将skb->dst->input设置为指向ip_rcv()函数,以便在数据包到达主机后进行进一步的处理。

ip_local_deliver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* Deliver IP Packets to the higher protocol layers.
* 将IP数据包传送到更高的协议层
*/
int ip_local_deliver(struct sk_buff *skb){
/*
* Reassemble IP fragments.
*/
struct net *net = dev_net(skb->dev);

if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
//同样,先执行NF_INET_LOCAL_IN的hook,才进行执行ip_local_deliver_finish交付给上层
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,net, NULL, skb, skb->dev, NULL,ip_local_deliver_finish);
}

进行加锁读取ip_protocol_deliver_rcu

1
2
3
4
5
6
7
8
9
10
11
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
skb_clear_delivery_time(skb);
__skb_pull(skb, skb_network_header_len(skb));

rcu_read_lock();
ip_protocol_deliver_rcu(net, skb, ip_hdr(skb)->protocol);
rcu_read_unlock();

return 0;
}

主要是负责将收到的 IP 数据包提交给对应的传输层协议处理,以便进行进一步的分发和处理。该函数会首先调用 raw_local_deliver 函数进行本地处理,然后根据 IP 协议号找到对应的协议处理函数,并使用 INDIRECT_CALL_2 宏调用对应的处理函数处理数据包。处理完成后,函数会更新 IP 统计信息。如果没有找到对应的协议处理函数,则会将数据包丢弃,并发送 ICMP 错误消息。

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
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
const struct net_protocol *ipprot;
int raw, ret;

resubmit:
raw = raw_local_deliver(skb, protocol);

ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
if (!ipprot->no_policy) {
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
kfree_skb_reason(skb,
SKB_DROP_REASON_XFRM_POLICY);
return;
}
nf_reset_ct(skb);
}
ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv,
skb);
if (ret < 0) {
protocol = -ret;
goto resubmit;
}
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
} else {
if (!raw) {
if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
__IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
icmp_send(skb, ICMP_DEST_UNREACH,
ICMP_PROT_UNREACH, 0);
}
kfree_skb_reason(skb, SKB_DROP_REASON_IP_NOPROTO);
} else {
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
consume_skb(skb);
}
}
}

ip_forward

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

int ip_forward(struct sk_buff *skb)
{
//.....省略部分代码

//主线,调用hook NF_INET_FORWARD 后执行 ip_forward_finish
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
net, NULL, skb, skb->dev, rt->dst.dev,
ip_forward_finish);

sr_failed:
/*
* Strict routing permits no gatewaying
*/
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
goto drop;

too_many_hops:
/* Tell the sender its packet died... */
__IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
SKB_DR_SET(reason, IP_INHDR);
drop:
kfree_skb_reason(skb, reason);
return NET_RX_DROP;
}

ip_forward_finish调用dst_output转发出去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int ip_forward_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct ip_options *opt = &(IPCB(skb)->opt);

__IP_INC_STATS(net, IPSTATS_MIB_OUTFORWDATAGRAMS);
__IP_ADD_STATS(net, IPSTATS_MIB_OUTOCTETS, skb->len);

#ifdef CONFIG_NET_SWITCHDEV
if (skb->offload_l3_fwd_mark) {
consume_skb(skb);
return 0;
}
#endif

if (unlikely(opt->optlen))
ip_forward_options(skb);

skb_clear_tstamp(skb);
return dst_output(net, sk, skb);
}

ip_local_out

在上层发送数据的时候都会经过ip_local_out,同时在__ip_local_out会触发NF_INET_LOCAL_OUT钩子函数进行检测

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
int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb){
struct iphdr *iph = ip_hdr(skb);

iph_set_totlen(iph, skb->len);
ip_send_check(iph);

/* if egress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
skb = l3mdev_ip_out(sk, skb);
if (unlikely(!skb))
return 0;

skb->protocol = htons(ETH_P_IP);

return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,net, sk, skb, NULL, skb_dst(skb)->dev,dst_output);
}
//从本地发出的数据包,会在构造了ip头之后,调用ip_local_out函数,该函数设置数据包的总长度和校验和,触发NF_INET_LOCAL_OUT钩子函数
int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
int err;

err = __ip_local_out(net, sk, skb);
if (likely(err == 1))
err = dst_output(net, sk, skb);

return err;
}
EXPORT_SYMBOL_GPL(ip_local_out);

firewalld

todo

相关资料

  1. https://www.cisco.com/c/en/us/products/security/firewalls/what-is-a-firewall.html

网络篇-深入理解防火墙工作原理
https://mikeygithub.github.io/2023/03/01/yuque/网络篇-深入理解防火墙工作原理/
作者
Mikey
发布于
2023年3月1日
许可协议