构建 Linux 防火墙

作者:Chris Kostick

互联网的增长促使许多组织开始关注安全。有记录和未记录的安全违规事件、关于安全问题的扩展研究,甚至媒体炒作,都带来了为网络环境提供至少部分解决方案的可能性——而不会将网络与外部世界完全隔离。防火墙是解决方案中的佼佼者。几乎每个人都定义了什么是防火墙,所以我也不例外。防火墙是一种设备或设备集合,用于限制“外部”网络对“内部”网络的访问。毫不奇怪,Linux 可以在这个领域发挥作用。

目前有三种模型用于对防火墙进行分类。从根本上说,当前的行业分类是应用代理网关、电路级中继和数据包过滤器。

应用代理网关是大多数人在谈论防火墙时想到的。它也称为堡垒主机,用于完全切断外部网络和内部网络之间的连接。连接通过代理进程连接到堡垒主机。堡垒主机反过来将建立到实际目的地的连接,并处理两个连接之间的通信。

代理网关有几个优点。首先,由于代理位于应用层,它们可以利用应用协议。例如,提供身份验证的协议(如 TELNET、FTP 和 HTTP)可以在代理处被拦截,并应用更强的身份验证机制(如 S/Key),而不会对远程客户端产生不利影响。此外,代理可以应用特定于协议的规则。可以建立一条规则,允许 FTP GET 通过网关,但不允许 FTP PUT。另一个优点是在应用层可以提供广泛的日志记录。重要的是要注意,堡垒主机不执行 IP 路由功能。所有通信都通过代理进程进行。防火墙工具包 FWTK,可从 TIS 免费获得,是防火墙应用级网关的一个例子。

电路级中继的功能类似于应用代理网关,不同之处在于电路中继使用的代理通常不感知应用程序。因此,您会失去应用代理网关中拥有的许多详细的日志记录功能和精确的规则定义。重要的概念仍然相同,即连接是通过代理建立的,并且 IP 数据包不会通过防火墙转发。SOCKS 是基于电路级中继的防火墙的实现。

数据包过滤是最常见的防火墙类型。它基于规则转发数据包的概念工作。这些规则通常会考虑源 IP 地址和目标 IP 地址、源端口号和目标端口号、正在传输的协议、TCP 标志、IP 标志或选项以及其他信息,例如数据包到达的接口。数据包过滤防火墙与其他防火墙的主要区别在于 IP 转发。数据包过滤防火墙通常是路由器,其生命中的功能是转发数据包。这意味着,虽然您可以控制外部的哪些机器可以与内部的某些机器(以及哪些应用程序)通信,但您现在依赖于应用程序来保护自己免受伤害。对于某些应用程序,这不是一个特别明智的决定。尽管如此,数据包过滤可能非常有用,并且广泛可用且通常价格低廉。

Linux 机器可以用作这三种防火墙类型中的任何一种,也可以用作全部三种(即作为混合型)。然而,在没有附加组件的情况下,Linux 内核具有充当数据包过滤路由设备的能力,使用由 Daniel Boulet 和 Ugen J.S. Antsilevich 编写的 ipfirewall 代码。对于大多数 1.2.x 和 1.3.x 内核,防火墙代码 (ip_fw.c) 基于 Alan Cox 和 Jos Vos 的端口。Boulet 已经发布了他的 ipfirewall 代码的 2.0e 版本(截至本文撰写之时),作为共享软件。我还没有安装新版本,因此我的任何讨论都基于 ip_fw.c 代码——特别是内核 1.2.13。

注意

为了使用这种内置的防火墙功能,您需要了解一些关于 TCP/IP 工作原理的知识。在不了解网络的情况下从头开始设置防火墙肯定会是一场灾难。如果您想要一个用于 Linux 的“即插即用”防火墙解决方案,本文末尾会提到一个。要了解更多关于 TCP 和 IP 的信息,推荐阅读 W. Richard Stevens 的 TCP/IP Illustrated, Volume 1。此外,Douglas Comer 的 Internetworking with TCP/IP 第 3 版也是极好的睡前读物。

如何完成

防火墙代码包括三个工具——记帐、阻止和转发。记帐 规则用于维护所选 IP 流量的数据包和字节计数统计信息。阻止 规则指定接受和拒绝往返防火墙自身的数据包的规则。转发 规则指定哪些数据包将由防火墙转发;这意味着源地址和目标地址不是防火墙本身。您可以基于源和/或目标 IP 地址;TCP 或 UDP 端口;协议(如 TCP、UDP 或 ICMP);或三者的组合来指定任何类型的规则。

这些服务在内核启动时激活,规则通过 setsockopt(2) 系统调用设置和修改。可以通过查看 /proc/net 目录中的 ip_acct、ip_block 和 ip_forward 文件来查看当前的记帐统计信息和防火墙规则。ip_acct 的内容如下所示

# cat /proc/net/ip_acct
IP accounting rules
C0A80101/FFFFFFFF->00000000/00000000 00000000
 0 0 0 386 392946 0 0 0 0 0 0 0 0 0 0

在这个例子中,存在一个规则,基本上是说要保留从 192.168.1.1 到任何地方的所有端口和所有协议的连接的统计信息。

更改记帐和防火墙工具必须通过 C 程序、Perl 脚本或支持 setsockopt(2) 系统调用的其他语言来完成。这是一个示例程序,它将更改转发规则的默认策略

# cat set_policy.c
#include <stdio.h>
#include <string.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/ip_fw.h>

main(int argc, char **argv)
{
  int    p, sfd;
  struct ip_fw fw;

  fw.fw_flg = 0;

  if (strcmp(argv[1], "accept") == 0) {
    p = IP_FW_F_ACCEPT;
  }
  else if (strcmp(argv[1], "reject") == 0) {
    p = IP_FW_F_ICMPRPL;
  }
  else if (strcmp(argv[1], "deny") == 0) {
    p = 0;
  }

  sfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
  setsockopt(sfd, IPPROTO_IP, IP_FW_POLICY_FWD,
             &p, sizeof(int));
}

# cat /proc/net/ip_forward
IP firewall forward rules, default 4

# set_policy deny

# cat /proc/net/ip_forward
IP firewall forward rules, default 0

正如您所看到的,这只是打开一个原始 IP 套接字并使用 setsockopt() 来更改环境的问题。setsockopt 调用正在设置转发规则的默认策略 (IP_FW_POLICY_FWD)。转发策略命令的值保存在 p 中,并由命令行参数 acceptrejectdeny 确定。这些词等同于 IP_FW_F_ACCEPTIP_FW_F_ICMPRPL0 的定义值。deny 和 reject 之间的区别在于,deny 策略只会丢弃数据包,而 reject 策略会丢弃数据包,并向发起主机响应“ICMP 端口不可达”消息。

不要重复发明轮子

编写 C 或 Perl 代码来操作防火墙规则听起来可能很有趣,但一些管理员可能没有时间来开发自己的防火墙界面。ipfw 是 net-tools(版本 1.3.6)包中的一个实用程序,它允许 root 用户添加、删除或列出与记帐和防火墙规则相关的信息。图 1 显示了当前主机中阻止规则的输出。

显然,这比查看 /proc/net/ip_block 文件的直接内容要好得多。-n 选项只是告诉 ipfw 不要将地址解析为名称。

添加规则非常容易。假设我们想要接受来自远程管理站的 SNMP 查询(请注意,作者并不真正提倡这种行为——这只是一个例子)。我们可以添加规则

# ipfw add b accept udp from 0.0.0.0/0 \
  to 20.2.51.105 161

为我们提供 图 2 中显示的新列表。

在我们继续之前,让我解释一下上面给出的 ipfw 命令中的一些符号。这些参数告诉我们我们正在添加一个用于 UDP 协议的阻止 (b) 规则。该规则接受从任何主机 (0.0.0.0/0) 到 20.2.51.105 的 UDP 数据报,但仅限于目标端口为 161 的数据报。20.2.51.105 是防火墙上一个接口的地址。

假设上面的 SNMP 规则不是我们想要的。假设我们只想允许来自一个特定网络(例如 20.2.61.0(当然是子网))的 SNMP 查询。我们可以删除我们刚刚添加的规则,然后输入我们的新规则。

# ipfw del b accept udp from 0.0.0.0/0 \
  to 20.2.51.105 161
# ipfw add b accept udp from 20.2.61.0/24 \
  to 20.2.51.105 161

图 3 显示了我们的新规则集。语法 20.2.61.0/24 允许您使用地址指定网络掩码。/24 表示有 24 位网络掩码或 255.255.255.0。

此时,更敏锐的读者可能会问:“那些规则到底是什么意思?” 我会讲到这一点,但首先,我想谈谈一个我认为比 ipfw 更好的实用程序。

ipfwadm(版本 1.2)程序由 Jos Vos 开发(可从 www.xos.nl/linux/ipfwadm 获得),是一个用于 IP 防火墙和记帐的管理实用程序,类似于 ipfw。我认为,它提供了稍微更直观的界面、更好的输出和更好的手册页(并非每个人都阅读源代码来获取文档)。

要列出 图 3 中显示的规则,请发出此命令

# ipfwadm -B -l -n
IP firewall block rules, default policy: accept
typ prot source         destination   ports
den tcp  0.0.0.0/0      20.2.51.105   * -> *
den tcp  0.0.0.0/0      192.168.1.1   * -> *
acc udp  20.2.61.0/24   20.2.51.105   * -> 161
rej udp  0.0.0.0/0      192.168.1.1   * -> *
acc udp  0.0.0.0/0      20.2.51.105   53 -> *
rej udp  0.0.0.0/0      20.2.51.105   * -> *

请注意几件事。首先,ipfwadm 始终显示默认策略;我喜欢看到这一点。其次,这些规则的类型字段

rej udp  0.0.0.0/0      192.168.1.1   * -> *
rej udp  0.0.0.0/0      20.2.51.105   * -> *

设置为 reject,而不是 deny,如 图 3 中的 ipfw 输出所示。嗯,它们实际上设置为 reject。ipfw 仅支持 deny 和 accept 策略,不支持 reject。

鉴于我们在设置和列出规则方面的丰富知识,让我们再次重建表(除了 SNMP 规则),同时添加一些更多细节。但首先,我们将定义我们的网络外观,如 图 4 所示。

我们将 20.2.51.0 网络称为“外部”网络,将 192.168.1.0 网络称为“内部”网络。由于阻止规则适用于防火墙本身,因此规则将在 deathstar 上设置。首先,我们将清除我们拥有的任何规则,并将默认策略设置为 accept

# ipfwadm -B -f
# ipfwadm -B -p accept

现在我们将定义我们需要阻止的内容。您可以选择阻止的协议是 TCP、UDP 和 ICMP。我们希望允许 ICMP 消息到达防火墙,因此我们不能只是阻止一切。我们可以通过添加规则来阻止 TCP

# ipfwadm -B -a deny -P tcp -S 0.0.0.0/0 \
  -D 20.2.51.105

但这将是不充分的。该规则最终会阻止所有 来自 防火墙的流量,以及 防火墙的流量。因此,管理员无法从机器上 telnet 或 ftp。这可能是理想的,但假设我们将允许来自内部的 TCP 流量。我们希望阻止所有到防火墙的连接尝试,同时让连接出去。我们可以使用 -y 选项修改规则。这将通过阻止来自任何主机到防火墙的任何设置了 SYN 位的 TCP 段来完成我们想要的操作。

# ipfwadm -B -y -a deny -P tcp -S 0.0.0.0/0 \
  -D 20.2.51.105

记住防火墙有两个接口,我们也阻止第二个接口

# ipfwadm -B -y -a deny -P tcp -S 0.0.0.0/0 \
  -D 192.168.1.1

这是过于严格的,因为来自内部网络到防火墙的连接也将被阻止。我们可以细化规则,仅当 TCP 连接请求通过外部接口 (20.2.51.105) 进入时才阻止它们。

# ipfwadm -B -y -a deny -P tcp -S 0.0.0.0/0 \
  -D 20.2.51.105 -I 20.2.51.105
# ipfwadm -B -y -a deny -P tcp -S 0.0.0.0/0 \
  -D 192.168.1.1 -I 20.2.51.105

好了,这还不错。

现在我们已经处理了 TCP 流量,让我们为 UDP 编写一些规则。由于 UDP 有许多我们在此处不会详细讨论的问题,我们将阻止所有 UDP。UDP 的规则将与 TCP 的规则相同,只是由于没有 SYN 位,因此不需要 -y,因此我们将拒绝数据包而不是拒绝它。我们拒绝它的原因是 ICMP 端口不可达消息对基于 UDP 的应用程序有意义,但 TCP 应用程序会忽略它们。如果 ip_fw 代码在规则是针对 TCP 并且标记为拒绝时发送 TCP 重置,那就太好了,但它不会。因此,我们的 UDP 规则将是

# ipfwadm -B -a reject -P udp -S 0.0.0.0/0 \
  -D 192.168.1.1 -I 20.2.51.105
# ipfwadm -B -a reject -P udp -S 0.0.0.0/0 \
  -D 20.2.51.105 -I 20.2.51.105

设置这些规则后,我们发现阻止所有 UDP 流量毕竟不是一个好主意。如果我们有 telnet,那么我们可能希望能够解析主机名。因此,我们打开 DNS。不过,在我们这样做之前,让我们看一下 DNS 查询的流量模式,以便我们可以衡量我们需要编写什么。图 5 包含从 deathstar 到 mccoy(一个“外部”DNS 服务器)的 DNS 查询的 tcpdump 输出。

我们可以看到我们将必须有两个规则——一个用于发出的查询(发送到远程计算机上的端口 53),另一个用于响应(从远程计算机发送到防火墙上的端口 53)。该规则可以写成

# ipfwadm -B -a accept -P udp -S 20.2.51.105  \
  -D 0.0.0.0/0 53
# ipfwadm -B -a accept -P udp -S 0.0.0.0/0 53 \
  -D 20.2.51.105

由于这种双向流量在许多协议中很常见,因此您可以设置一个选项来将两个规则压缩为一个。-b 选项设置双向模式,这将安装一个规则,该规则匹配两个方向的流量。然后我们可以使用

# ipfwadm -B -a accept -b -P udp -S 0.0.0.0/0 53 \
  -D 20.2.51.105

现在我们已经重新创建了我们的表,我们可以使用扩展 (-e) 输出获取规则列表。这在 图 6 中显示。请注意,扩展输出包含阻止规则的数据包和字节计数。防火墙代码自动维护转发和阻止规则的记帐信息。

到目前为止,我们所完成的工作是保护机器 deathstar 以适应我们的环境。为了保护内部网络,我们将需要制定正确的转发规则。在我开始输入规则集之前,我喜欢构建一个允许和不允许的内容表。图 7 显示了我放在一起建立阻止规则的表。请注意,规则表中的星号表示任何主机地址或任何端口号。

我们可以为我们的转发规则集构建相同类型的表。如果我们有安全策略,那么构建我们的表应该很简单。假设我们的安全策略中讨论防火墙的部分将允许 TELNET 和 FTP 出去,以及电子邮件 (SMTP) 双向进入。此外,电子邮件仅允许发送到邮件中心(因为内部网络上只有一台机器,它将是邮件中心——请理解我)。图 8 显示了转发规则集的通用规则表。

防火墙的立场是“拒绝一切”。这是防火墙非常常见的策略,因为唯一会被转发的数据包是那些明确允许的数据包。

这些规则非常简单,除了编号为 1024 或更大的源端口和目标端口。造成这种情况的原因基本上是历史性的。大多数 Unix 客户端程序(如 TELNET)将其临时端口范围分配在 1024 到 5000 之间。端口 1 到 1023 被称为“保留端口”。这些端口用于服务器应用程序,如 telnetd、ftpd 等。几乎所有的 TCP/IP 协议栈都遵循这个约定,因此我们可以在我们的规则集中利用它——以帮助保证进入网络的仅限客户端的通信。我不使用 1024-5000 范围的原因是并非所有设备都遵守这个 Unix 传统。例如,附件终端服务器将其临时端口范围从 10000 开始。

以下是建立我们的转发规则的命令

# ipfwadm -F -f
# ipfwadm -F -p deny
# ipfwadm -F -a accept -b -P tcp -S 0.0.0.0/0 23 \
  -D 192.168.1.0/24 1024:65535
# ipfwadm -F -a accept -b -P tcp -S 0.0.0.0/0 21 \
  -D 192.168.1.0/24 1024:65535
# ipfwadm -F -a accept -b -P tcp -S >0.0.0.0/0 20 \
  -D 192.168.1.0/24 1024:65535
# ipfwadm -F -a accept -b -P tcp -S >0.0.0.0/0 25 \
  -D 192.168.1.2 1024:65535
# ipfwadm -F -a accept -b -P tcp -S >192.168.1.2 25 \
  -D 0.0.0.0/0 1024:65535

要将 deathstar 设置为防火墙机器,ipfwadm 命令将被放入文件中并作为 shell 脚本执行。Deathstar 使用文件 /usr/local/etc/set-rules.sh。为了正确启动机器,明智的做法是在网络接口启动之前在内核中建立规则。deathstar 上的 /etc/rc.d/rc.inet1 包含以下行

# set firewall rules
/bin/bash /usr/local/etc/set-rules.sh
# bring up ethernet
ifconfig eth0 192.168.1.1 192.168.1.255 up
# bring up ppp link
/usr/lib/ppp/ppp-on

Deathstar 实际上是我的桌面机器。我在上面加载了几乎所有东西,所以它不是一个很好的防火墙。正如我们所描述的那样,防火墙应该运行最少的软件才能运行。通常这意味着编译器、X、游戏或任何与内核或通信无关的东西都从系统中删除。

检查您的工作

即使有通用表可以使用,您也可能无法始终按照您想要的方式获得规则。能够检查您的工作会很好。ipfwadm 实用程序提供 -c 选项来针对您的规则检查数据包。例如,要检查来自某些主机的数据包是否可以将邮件发送到 warbird 以外的内部主机,我们可以运行

# ipfwadm -F -c -P tcp -S >195.1.1.1 1024 \
  -D 192.168.1.5 25 -I 20.2.51.105

这将产生响应 packet denied。当使用 -c 检查规则时,您需要非常具体,并提供源地址和端口、目标地址和端口以及接口地址。

测试您的环境的另一种方法是使用实时流量。如果您怀疑流量由于您的规则集而未被转发,您可以使用 tcpdump 监视进入和离开防火墙的流量。如果防火墙不允许合法流量通过,则会变得非常明显。例如,当我设置规则以允许邮件通过时,我注意到发送邮件花费了异常长的时间。tcpdump 显示接收者(在本例中为 mccoy)正在向源发送 IDENT 消息,但它们被防火墙阻止了。通过添加一条允许 IDENT 消息的规则,邮件发送速度更快。创建此规则留给读者作为练习。

对于基本的日志记录,可以使用 -k 选项设置规则,这将导致内核通过 syslog 为所有匹配的数据包打印消息。但是,设置内核以理解 -k 选项并非易事。内核需要使用定义的 CONFIG_IP_FIREWALL_VERBOSE 进行编译。为此,只需将定义添加到内核源代码目录中 net/inet 目录中的 Makefile 中。不幸的是,在 1.2.x 发行版中,ip_fw.c 的 CONFIG_IP_FIREWALL_VERBOSE 部分中定义的代码无法干净地编译。修复很简单,并在最新的 1.3.x 版本的内核中实现。

如果您设置内核以支持 -k 选项,您将在 /var/adm/messages 文件中收到类似于 图 9 中显示的输出。

总结性评论

我们刚刚构建的防火墙可以被您从供应商处购买的几乎任何路由器所取代。然而,将 Linux 机器变成数据包过滤路由器是一种廉价且非常有效的替代方案。

防火墙代码有一些局限性。其不足的日志记录功能是一个很大的缺陷;文档缺乏;并且无法根据 IP 选项进行过滤,这使得过滤路由器不如它可能的那样灵活。

对于许多环境,Linux 内核的防火墙功能可能绰绰有余,但对于那些需要用于 Linux 的商业级防火墙软件或可以在 Linux 下运行的软件的人来说,有一些解决方案。本文前面提到的 Daniel Boulet 的共享软件 ipfirewall 代码解决了刚才提到的几个问题。此外,Mazama Software Labs 的商业 Mazama Packet Filter 是一款真正的“铃铛和哨子”产品。它配备了完善的文档、用于定义规则集的过滤器“语言”(这是一个亮点)、用于非常简单的管理的 GUI 以及技术问题的修复(例如 IP 选项和 TCP SYN/ACK 过滤)。

本文中未提及的最后一个概念是 IP 伪装。非常敏锐的读者会注意到 warbird 所在的网络 (192.168.1.0) 是一个私有 IP 地址。也就是说,它不是 InterNIC 分配的地址,但可以用于未连接到互联网的本地或私有基于 IP 的网络。我可以放心地使用此寻址方案,因为名为 relay 的机器是一个执行伪装(也称为地址隐藏)的商业防火墙。从远程计算机的角度来看,所有离开 20.2.51 或 192.168.1 网络的连接的源地址都是 relay。正如您可能从其名称中猜到的那样,relay 也是一个应用代理网关。Linux 也具有隐藏地址的能力,但这将是另一篇文章的主题。

Chris Kostick (ckostick@csc.com) 是计算机科学公司网络安全部门的高级计算机科学家。他喜欢使用 Linux,但认为自己是一个后来者,因为他从内核版本 1.1.18 开始使用。就计算机而言,他不确定自己是调试 TCP/IP 问题更有趣还是玩 Doom 更有趣。

加载 Disqus 评论