PF-之书第三版-全-
PF 之书第三版(全)
原文:
zh.annas-archive.org/md5/ef3ed7ed0927b0aa72e4d12f6e34610d译者:飞龙
前言

这是一本关于构建你所需网络的书。我们将从一些理论出发,探讨防火墙及相关功能的主题。你将看到许多关于过滤和其他网络流量引导方式的示例。我假设你已经具备基本到中级的 TCP/IP 网络概念和 Unix 管理知识。
本书中的所有信息都附带一个警告:正如许多工作一样,我们讨论的解决方案可以有多种实现方式。而且,软件世界总在变化,最好的做法可能已经发生了变化,本书的出版时间是在 2014 年 7 月,当时测试的是 OpenBSD 5.6 版、FreeBSD 10.0 版和 NetBSD 6.1 版以及当时可用的所有补丁。
这不是一本 HOWTO 手册
本书是我广受欢迎的 PF 教程的直接后代,也是该手稿的第三版。在这本书的编写过程中,我投入了大量精力,以确保它的实用性,我相信你会发现它很有用,并希望你也能享受阅读过程。但请记住,本书并不是一份可以直接复制粘贴的现成方案。
为了强调这一点,请跟我一起重复:
//The Pledge of the Network Admin//
This is my network.
It is mine,
or technically, my employer's.
It is my responsibility,
and I care for it with all my heart.
There are many other networks a lot like mine,
but none are just like it.
I solemnly swear
that I will not mindlessly paste from HOWTOs.
重点是,虽然我已经测试了本书中的所有配置,它们可能至少在某些方面不完全适用于你的网络。请记住,本书的目的是向你展示一些有用的技巧,并激励你做出更好的成果。
力求理解你的网络以及如何改进它,请不要盲目从本书或任何其他资料中复制粘贴。
本书内容
本书旨在成为一份独立的文档,使你能够在不需要频繁查阅手册页的情况下进行机器配置,并偶尔参考附录 A 中列出的在线和印刷资源。
你的系统可能会附带一个预写的pf.conf文件,其中包含了一些注释掉的有用配置建议,以及一些文档目录中的示例,比如/usr/share/pf/。这些示例作为参考是有用的,但我们在本书中不会直接使用它们。相反,你将学习如何从头开始一步步构建一个pf.conf文件。
这里是本书内容的简要概述:
-
第一章讲解了基本的网络概念,简要回顾了 PF 的历史,并为你提供了一些关于如何适应 BSD 操作系统的建议,如果你是这个操作系统家族的新手,建议先阅读这一章,以了解如何使用 BSD 系统。
-
第二章展示了如何在你的系统上启用 PF,并介绍了一个非常基础的单机规则集。该章相当重要,因为后续的所有配置都基于我们在这一章中构建的规则。
-
第三章 基于第二章中的单机配置,并引导你了解如何设置网关,作为不同网络之间的联络点。在第三章结束时,你将建立一个典型的家庭或小型办公室网络配置,并掌握一些技巧,便于简化网络管理。你还将提前体验如何处理具有特殊要求的服务,如 FTP,并获取一些关于如何通过支持一些常见但较少理解的互联网协议和服务来让网络更易于故障排除的小贴士。
-
第四章 引导你将无线网络添加到现有的设置中。无线环境带来了一些安全挑战,在本章结束时,你可能已经拥有了一个通过
authpf实现访问控制和身份验证的无线网络。一些信息在有线环境中同样可能有所帮助。 -
第五章 讲解了当你引入需要从外部访问的服务器和服务时的情况。在本章结束时,你可能已经建立了一个拥有一个或多个独立子网和 DMZ(非军事区)的网络,并且你将尝试通过重定向和
relayd来实现几种不同的负载均衡方案,以提高用户的服务质量。 -
第六章 介绍了 PF 工具箱中的一些工具,用于应对不良活动的尝试,并展示了如何有效地使用这些工具。我们处理了暴力破解密码尝试和其他网络洪水攻击,以及反垃圾邮件工具
spamd,OpenBSD 的垃圾邮件推迟守护进程。本章应该让你的网络对合法用户更加友好,对那些心怀不良企图的人则不再那么热情。 -
第七章 介绍了通过 OpenBSD 5.5 引入的优先级和队列系统进行流量整形。本章还包含如何将早期基于 ALTQ 的设置转换为新系统的技巧,以及在没有新队列系统的操作系统上设置和维护 ALTQ 的信息。本章将帮助你通过根据网络需求调整流量整形来更好地利用资源。
-
第八章 介绍了如何创建冗余配置,使用 CARP 配置进行故障转移和负载均衡。本章将帮助你理解如何创建并维护一个高度可用、冗余的、基于 CARP 的配置。
-
第九章,解释了 PF 日志。你将学会如何使用系统自带工具以及可选包,从 PF 配置中提取并处理日志和统计数据。我们还将讨论基于 NetFlow 和 SNMP 的工具。
-
第十章,讲解了多种有助于调整配置的选项。它将你从前几章获得的知识与规则集调试教程结合起来。
-
附录 A,是一本注释过的印刷和在线文献及其他资源的列表,当你扩展 PF 和网络相关知识时,可能会对你有所帮助。
-
附录 B,概述了将一流工具作为自由软件开发时所涉及的一些问题。
本书的每一章都在前一章的基础上展开。虽然作为自由的个体,你完全可以跳过某些章节,但按顺序阅读章节可能会更有帮助。
由于种种原因,OpenBSD 是我最喜欢的操作系统。我写这本书的主要环境是由运行最近快照、偶尔是稳定版系统以及时不时构建的本地-current 版本的 OpenBSD 系统主导的。这意味着本书的主要视角是基于 OpenBSD 5.6 的命令行环境。然而,我保留了足够的其他 BSD 系统,因此即使你的平台选择是 FreeBSD、NetBSD 或 DragonFly BSD,这本书仍然会对你有用。在一些网络配置和 PF 设置的领域,这些系统与 OpenBSD 的基础系统有明显的差异,在这些情况下,你会找到关于差异的说明以及如何为你的环境构建有用配置的具体平台建议。
第一章:构建你需要的网络

PF,OpenBSD 的数据包过滤子系统,在我看来是控制网络的最佳工具。在深入了解如何将你的网络打造成理想的精密机器之前,请先阅读本章。本章介绍了基本的网络术语和概念,提供了一些 PF 的历史,并概述了你可以在本书中找到的内容。
你的网络:高性能、低维护和安全
如果这个标题准确描述了你的网络,那么你很可能是为了纯粹的娱乐而阅读本书,希望你能享受接下来的内容。另一方面,如果你还在学习如何构建网络,或者还不太自信自己的技能,那么回顾一些基本的网络安全概念会很有帮助。
信息技术(IT)安全是一个庞大、复杂且有时令人困惑的话题。即使我们只将视角限定在网络安全上,也可能会觉得我们并没有大幅缩小领域范围,或消除足够多固有的困惑性术语。几年前,随着个人计算机加入联网世界,并配备了显然不适合网络环境的系统软件和应用程序,情况变得更为严重。
结果是可预测的。即使在小型计算机联网之前,它们也已经成为恶意软件的温床,例如病毒(半自动化软件,能够“感染”其他文件,以传递其有效载荷并制作更多副本)和木马(最初是特洛伊木马,包含嵌入式代码的软件或文档,一旦激活,会使受害者的计算机执行用户未曾预期的操作)。当小型计算机开始联网时,又引入了另一类恶意软件,称为蠕虫,这类软件利用网络传播其有效载荷^([1])。与此同时,网络化的各种诈骗方式也开始出现在网络安全的视野中,今天,计算机安全活动的一个重要部分(可能是行业中最大的部分)集中在威胁管理上,特别是打击和分类恶意软件,或称恶意软件(malware)。
枚举恶劣情况的无意义性已经在其他地方有力地讨论过(参考文献见附录 A,例如马库斯·拉努姆的精彩文章《计算机安全中最愚蠢的六个想法》)。OpenBSD 的方法是在一开始就正确地设计和编码。然而,即使是聪明的人偶尔也会犯错,产生漏洞,因此请确保设计系统时能够将任何此类失败的安全影响降到最低。然后,如果你后来发现错误且漏洞是可被利用的,请修复这些漏洞,无论它们出现在代码树中的哪个地方,即使这可能意味着彻底重做设计,最坏情况下会丧失向后兼容性。^([2])
在 PF 中,广义上在本书中,重点更狭窄,集中于网络层的网络流量。OpenBSD 4.7 中引入的 divert(4)套接字使得设置一个系统变得更加容易,在这个系统中,PF 有助于深度数据包检查,类似于一些经过激烈营销的产品。然而,这个接口在自由软件中还未广泛用于这个目的,尽管也有例外。因此,我们将重点讨论一些基于纯网络层行为的技术,这些技术在第六章的示例配置中最为显著。这些技术将在你已配置的内容检查产品中减轻负担。正如你在接下来的章节中所看到的,除了阻止或通过数据包之外,网络层还带来了许多乐趣和激动。
包过滤器的作用
包过滤器的主要功能正如其名,是通过匹配单个数据包的属性和由这些数据包构建的网络连接,按照其配置文件中定义的过滤标准来过滤网络数据包。包过滤器负责决定如何处理这些数据包。这可能意味着通过数据包或者拒绝它们,或者触发其他操作系统部分或外部应用程序预设的事件。
PF 允许你编写自定义过滤标准,以基于几乎任何数据包或连接属性来控制网络流量,包括地址族、源地址和目标地址、接口、协议、端口和方向。根据这些标准,包过滤器会执行你指定的操作。最简单和最常见的操作之一就是阻止流量。
包过滤器可以阻止不必要的流量进入你的网络。它还可以帮助将网络流量限制在你自己的网络内部。这两个功能对防火墙概念非常重要,但阻止流量远不是功能齐全的包过滤器唯一有用或有趣的功能。正如你在本书中将看到的,你可以使用过滤标准将某些类型的网络流量定向到特定的主机,将流量类别分配到队列中,执行流量整形,甚至将选定类型的流量交给其他软件进行特别处理。
所有这些处理都发生在网络层,基于数据包和连接属性。PF 是网络堆栈的一部分,深深嵌入操作系统内核。尽管也有在用户空间实现包过滤的例子,但在大多数操作系统中,过滤功能是在内核中执行的,因为这样做更快。
PF 的崛起
如果你对历史感兴趣,你可能已经知道 OpenBSD 和其他 BSD^([3])是 BSD 系统的直接后裔(有时称为BSD Unix),这是一个包含了 1980 年代早期 TCP/IP 互联网协议原始参考实现的操作系统。
随着 90 年代初期 BSD 开发背后的研究项目逐渐结束,代码被解放出来,供世界各地的小型开发者团队进一步开发。这些开发者中的一些人负责确保新兴互联网的关键基础设施稳定运行,BSD 开发在多个团队中平行进行。OpenBSD 团队成为了 BSD 家族中最注重安全的团队。为了满足包过滤的需求,它使用了一个名为IPFilter的子系统,主要由 Darren Reed 编写。在这些早期的岁月里,OpenBSD 迅速赢得了“防火墙操作系统”的美誉,至今仍然有人认为 OpenBSD 是专门为此目的开发的。
当 Reed 在 2001 年初宣布,已经与 OpenBSD 紧密集成的 IPFilter 并不受 BSD 许可证的保护时,OpenBSD 社区感到震惊。事实上,它使用的是几乎逐字复制的许可证,只是省略了对代码进行修改和分发结果的权利。问题在于,OpenBSD 版本的 IPFilter 包含了几项更改和定制,而这些更改显然不符合许可证要求。结果,IPFilter 于 2001 年 5 月 29 日被从 OpenBSD 源代码树中删除,几周内,OpenBSD 的开发版本(-current)中没有任何包过滤软件。
幸运的是,在此时,丹尼尔·哈特迈尔(Daniel Hartmeier)在瑞士进行了一些涉及内核黑客技术的有限实验,实验内容涉及网络代码。他首先将自己编写的一个小函数挂接到网络堆栈中,然后让数据包经过它。接着,他开始思考过滤问题。当许可证危机发生时,PF 已经在小规模开发中。PF 代码的第一次提交发生在 2001 年 6 月 24 日星期天,19:48:58 UTC。接下来是几个月的开发者密集活动,最终 PF 的版本作为 OpenBSD 3.0 基础系统的默认部分于 2001 年 12 月发布。^([4]) 该版本包含了数据包过滤的实现,包括网络地址转换(NAT),并且其配置语言与 IPFilter 相似,因此迁移到新的 OpenBSD 版本时没有遇到重大问题。^([5])
PF 证明是一个开发成熟的软件。2002 年,哈特迈尔在 USENIX 会议上发表了一篇论文,进行的性能测试表明,在压力测试下,OpenBSD 3.1 的 PF 与 OpenBSD 3.1 上的 IPFilter 或 Linux 上的 iptables 性能相当,甚至更好。此外,在 OpenBSD 3.0 的原始 PF 上进行的测试表明,从 3.0 版本到 3.1 版本,代码的效率得到了显著提升。^([6])
OpenBSD 的 PF 代码,由经验丰富且注重安全性的开发人员编写,配备了全新的数据包过滤引擎, naturally 引起了其他 BSD 系统的兴趣。FreeBSD 项目逐渐采纳了 PF,最初作为一个包管理器软件包,之后从 5.3 版本开始,作为三种数据包过滤系统之一,集成到了基础系统中。PF 还被包括在 NetBSD 和 DragonFly BSD 中。^([7])
本书聚焦于 OpenBSD 5.5 中可用的 PF 版本。根据需要,我会指出该版本与其他系统中集成的版本之间的显著差异。
如果你准备好深入 PF 配置,可以跳到第二章开始。如果你想花更多时间在不熟悉的 BSD 领域中摸索,可以继续阅读本章。
更新版的 PF 发布性能更佳
与计算机世界的其余部分一样,OpenBSD 和 PF 也受到了硬件和网络条件快速变化的影响。我最近没有看到与丹尼尔·哈特迈尔 USENIX 论文中进行的测试相当的测试,但 PF 用户发现其过滤开销适中。
作为一个例子(主要是为了说明即使是平凡的硬件配置也可以有用),我负责管理的小型办公室网络与外界之间的网关机器是一台 450MHz 的 Pentium III,配备 384MB 的 RAM。每当我检查时,从top(1)命令的输出来看,这台机器从未低于 96%的空闲状态。
还值得注意的是,当前的 PF 开发者主要是 Henning Brauer 和 Ryan McBride,他们在最近的发布版本中对 OpenBSD 的 PF 代码进行了大量重写,提升了性能,并将性能提升作为主要目标,使得从 4.4 到 5.6 的每个版本在性能上都有明显改善。
如果你来自其他系统
如果你正在阅读本节内容,是因为你正在考虑将你的设置从其他系统迁移到 PF,那么这一节是为你准备的。
如果你想使用 PF,你需要安装并运行一个 BSD 系统,例如 OpenBSD、FreeBSD、NetBSD 或 DragonFly BSD。这些都是很好的操作系统,但我个人最喜欢的是 OpenBSD,主要是因为几乎所有 PF 的开发工作都发生在这个操作系统中。我也觉得开发者的务实态度和系统的简洁性令人耳目一新。
偶尔,来自其他系统的 PF 实现会将一些小的改动和 bug 修复反馈到主 PF 代码库,但最新、最更新的 PF 代码始终可以在 OpenBSD 上找到。本书中描述的某些功能仅在 OpenBSD 的最新版本中可用。其他 BSD 系统通常会将 OpenBSD 最新发布的 PF 版本移植到它们的代码库中,以便在下次发布时使用,但同步更新远非保证,而且有时延迟会相当显著。
如果你计划在 FreeBSD、NetBSD、DragonFly BSD 或其他系统上运行 PF,你应该查看你系统的发布说明和其他文档,了解包含的 PF 版本。
Linux 用户指南
Linux 和 BSD 之间的差异和相似之处如果深入探讨,可能会成为一个庞大的话题,但如果你对基本知识有一定掌握,应该不会花太多时间就能适应 BSD 的操作方式。在本书的其余部分,我将假设你能够熟悉 BSD 网络配置的基本内容。所以,如果你对 Linux 或其他系统的配置更为熟悉而不是 BSD,值得注意一些关于 BSD 配置的要点:
-
Linux 和 BSD 在命名网络接口时使用不同的约定。Linux 的约定是按照顺序标记机器上的所有网络接口,依次为
eth0、eth1等(尽管在某些 Linux 版本和驱动组合中,你还会看到wlan0、wlan1等无线接口)。在 BSD 系统中,接口会被分配一个名称,该名称由驱动程序名和一个序列号组成。例如,使用 ep 驱动的旧版 3Com 卡会显示为
ep0、ep1,依此类推;Intel 千兆卡可能会显示为em0、em1,依此类推。某些 SMC 卡会列为sn0、sn1,依此类推。这种系统非常合乎逻辑,使得查找该接口的具体文档变得更容易。如果你的内核(在启动时或在ifconfig输出中)报告说你有一个名为em0的接口,你只需在 Shell 命令行中输入man em,即可查找该接口支持的速度——是否有任何特殊要求,需要下载固件等等。 -
你应该了解,在 BSD 系统中,配置是以/etc/rc.conf为中心的。一般来说,BSD 系统会从文件/etc/rc.conf读取配置,该文件在启动时由/etc/rc脚本读取。OpenBSD 建议使用/etc/rc.conf.local来进行本地自定义,因为rc.conf包含默认值。FreeBSD 使用/etc/defaults/rc.conf来存储默认设置,使得/etc/rc.conf成为进行更改的正确位置。此外,OpenBSD 使用每个接口的配置文件,名为hostname.
,其中 会被接口名替换。 -
为了学习 PF,你需要专注于一个/etc/pf.conf文件,这个文件大部分将是你自己的创作。
如果你需要对所选 BSD 操作系统有更广泛和更全面的了解,请查阅操作系统的文档,包括 FAQ 和指南,可以在项目的网站上找到。你还可以在附录 A 中找到一些进一步阅读的建议。
关于 PF 的常见问题解答
本节内容基于我在电子邮件、会议和研讨会上收到的提问,以及在邮件列表和其他讨论论坛中出现的一些问题。这里以 FAQ 样式^([8])的格式涵盖了一些更常见的问题。
我可以在我的 Linux 机器上运行 PF 吗?
总的来说,不能。在过去的几年里,PF 邮件列表上曾有某人宣称已开始将 PF 移植到 Linux,但截至目前为止,还没有人声称已完成这一任务。其主要原因可能是 PF 主要作为 OpenBSD 网络栈的一个深度集成部分进行开发。即使在经过十多年并行开发后,OpenBSD 代码与其他 BSD 系统仍然共享足够的基础,使得移植成为可能,但将 PF 移植到非 BSD 系统将需要重写 PF 的很大一部分代码,以及根据目标平台的需要进行集成。
对于 Linux 用户如何在 BSD 网络配置中找到方向的一些基本指导,请参阅 Linux 用户指南。
你能推荐一个 GUI 工具来管理我的 PF 规则集吗?
本书主要面向那些在自己喜欢的文本编辑器中编辑规则集的用户。本书中的示例规则集足够简单,你可能不会从各种 GUI 工具提供的可视化选项中获得显著的收益。
一个常见的说法是,PF 配置文件通常足够可读,以至于不需要图形可视化工具。然而,确实有几种可用的 GUI 工具可以编辑和/或生成 PF 配置,包括一个完整的、定制的 FreeBSD 版本,名为pfSense(* www.pfsense.org/*),它包含一个复杂的 GUI 规则编辑器。
我建议你根据本书中与你的情况相关的部分进行学习,然后决定是否需要使用 GUI 工具,以便更舒适地运行和维护你所构建的系统。
有没有工具可以将我的 OtherProduct^®设置转换为 PF 配置?
将网络设置(包括防火墙设置)从一种产品转换到另一种产品时,最佳策略是回到网络或防火墙配置的规范或政策,然后使用新工具实施这些政策。
其他产品不可避免地会有稍微不同的功能集,而你为 OtherProduct^®创建的现有配置可能会在某些特定问题的处理方式上有所不同,这些差异可能无法直接映射到 PF 及其相关工具中的功能。
拥有一份文档化的政策,并随着需求的变化及时更新,将使你的工作更加轻松。这份文档应包含一份完整的文字说明,解释你的设置旨在实现什么目标。(你可以通过在配置文件中添加注释来解释规则的目的,作为起点。)这使得可以验证你当前运行的配置是否真正实现了设计目标。在一些公司环境中,甚至可能有书面政策的正式要求。
寻找一种自动化转换的方式是完全可以理解的,尤其是对于系统管理员来说,这也许是预期的。我建议你克制这个冲动,在重新评估你的业务和技术需求之后,再进行转换操作,并(最好)在此过程中创建或更新正式的规范或政策。
一些作为管理前端的 GUI 工具声称能够输出多个防火墙产品的配置文件,并可能作为转换工具使用。然而,这样做的结果是将你与规则集之间增加了一层抽象层,并且你将受制于工具作者对 PF 规则集工作原理的理解。我建议你至少学习本书中相关的部分,然后再考虑花费大量时间进行自动化转换。
我听说 PF 基于 IPFilter,我曾在 Solaris 中使用过 IPFilter。我可以直接将我的 IPFilter 配置复制过去,并立即得到一个可用的配置吗?
如果有人声称 PF 是“基于”IPFilter 的,那是不准确的。PF 是从零开始编写的,目的是替代被新删除的 IPFilter 代码。在 PF 的第一个版本中,设计目标之一是尽量保持语法与旧软件兼容,这样从 OpenBSD 3.0 过渡时,能够尽量减少痛苦,并且不会过度破坏现有配置,或者以不可预测的方式破坏它们。
然而,在未来的一个或两个版本中,认为没有 OpenBSD 用户仍然可能从包含 IPFilter 的版本进行升级是合理的,因此保持与旧系统兼容不再是优先事项。即便经过了 25 个 OpenBSD 版本和 12 年以上的积极开发,仍然保持一些语法相似性。试图将一个系统的配置加载到另一个系统中——例如,将 IPFilter 的配置复制到 OpenBSD 系统并尝试加载,或者将现代 PF 配置复制到 Solaris 系统并尝试作为 IPFilter 配置加载——几乎在所有情况下都会失败,除非是一些特别精心制作但仍然相当简单且实际上非常无用的配置。
为什么 PF 的规则语法突然发生了变化?
世界发生了变化,PF 也随之变化。更具体地说,OpenBSD 开发者对他们的代码有着非常积极且务实的批判性关系,像 OpenBSD 的所有部分一样,PF 代码也在不断审查之中。
在超过十年的 PF 开发和使用过程中获得的经验教训,促使了代码内部的变化,最终让开发者们意识到稍微调整语法是有意义的。这些变化使 PF 语法更加一致,并且从长远来看,使用户的使用更加轻松,代价只是对配置文件进行一些轻微编辑。对你来说,用户,现在 PF 变得更加易用,而且比早期版本性能更好。如果你正在将系统升级到 OpenBSD 4.7 或更高版本,你将体验到真正的惊喜。
在 OpenBSD 5.5 中,你将找到另一个升级的好理由:新的流量整形队列系统,旨在替代久负盛名的 ALTQ 系统。虽然 ALTQ 仍然是 OpenBSD 5.5 的一部分,且略有修改,但它已经在 OpenBSD 5.6 版本中被移除。关于迁移到新流量整形系统的内容可以在第七章找到专门的部分。
我在哪里可以了解更多信息?
有几个很好的资源可以了解 PF 以及它运行的系统,你已经在本书中找到了一个。你可以在附录 A 中找到一些印刷版和在线资源的参考。
如果你有安装了 PF 的 BSD 系统,可以查阅在线手册页或 man 页,获取有关你所使用的软件版本的详细信息。除非另有说明,本书中的信息指的是从 OpenBSD 5.5 系统的命令行视角所看到的世界。
一点鼓励:PF 俳句
如果你还没有完全相信,或者即使你已经相信了,也许还是需要一点鼓励。多年来,很多人已经对 PF 发表了自己的看法——有时很奇怪,有时很精彩,有时则非常怪异。
这里引用的诗歌很好地反映了 PF 有时激发用户情感的程度。这首诗出现在 PF 邮件列表中,最初是一封主题为“Things pf can’t do?”的邮件,在 2004 年 5 月开始。这封邮件的作者是一个没有太多防火墙经验的人,因此在设置过程中遇到了困难。
当然,这引发了一些讨论,几位参与者表示,如果 PF 对新手来说很难,其他替代方案也不会更好。该讨论以 Jason Dixon 于 2004 年 5 月 20 日写的俳句结尾。
Compared to working with iptables, PF is like this haiku:
A breath of fresh air,
floating on white rose petals,
eating strawberries.
Now I'm getting carried away:
Hartmeier codes now,
Henning knows not why it fails,
fails only for n00b.
Tables load my lists,
tarpit for the asshole spammer,
death to his mail store.
CARP due to Cisco,
redundant blessed packets,
licensed free for me.
Dixon 在这里提到的一些概念可能听起来有些陌生,但如果你继续阅读,很快就会明白。
^([1]) Windows 时代之前著名的蠕虫包括 IBM 圣诞树 EXEC 蠕虫(1987 年)和第一只互联网蠕虫 Morris 蠕虫(1988 年)。有关这两者的丰富信息可以轻松通过你喜欢的搜索引擎找到。Windows 时代的网络蠕虫被认为是从 2000 年 5 月的 ILOVEYOU 蠕虫开始的。
^([2]) 有关 OpenBSD 安全方法的多个演讲可以通过* www.openbsd.org/papers/* 的汇总找到。我最喜欢的一些包括 Theo de Raadt 的《Exploit Mitigation Techniques》(以及 2013 年的后续讲座《Security Mitigation Techniques: An Update After 10 Years》),Damien Miller 的《Security Measures in OpenSSH》,以及 Henning Brauer 和 Sven Dehmlow 的《Puffy at Work—Getting Code Right and Secure, the OpenBSD Way》。
^([3]) 如果 BSD 这个词听起来不熟悉,这里有一个简短的解释:这个缩写代表 Berkeley Software Distribution,最初指的是加利福尼亚大学伯克利分校的工作人员和学生为 Unix 操作系统开发的一系列有用软件。随着时间的推移,这些软件扩展成了一个完整的操作系统,而这个操作系统成为了一个系列操作系统的前身,包括 OpenBSD、FreeBSD、NetBSD、DragonFly BSD,甚至按某些定义,包括苹果的 Mac OS X。要了解 BSD 是什么,可以阅读 Greg Lehey 的《Explaining BSD》,该文发表于 www.freebsd.org/doc/en/articles/explaining-bsd/(当然,还有各个项目的官方网站)。
^([4]) IPFilter 版权事件促使 OpenBSD 团队对整个源代码树进行许可证审计,以避免未来出现类似情况。在接下来的几个月中,解决了几个潜在问题,最终消除了许多可能的许可证陷阱,惠及所有参与自由软件开发的人。Theo de Raadt 在 2003 年 2 月 20 日向openbsd-misc邮件列表发送的消息中总结了这一努力。许可证危机的初步剧烈波动已经平息,而最终的成果是一个新的基于自由许可证的包过滤系统,具备最佳的代码质量,并且 OpenBSD 本身及其他广泛使用的自由软件中的大量代码也获得了更好的自由许可证。
^([5]) 与 IPFilter 配置的兼容性是 PF 开发者早期的设计目标,但一旦可以安全地假设所有 OpenBSD 用户已经迁移到 PF(大约在 OpenBSD 3.2 发布时,如果不是更早的话),这一目标就不再是优先事项。你不应该假设现有的 IPFilter 配置在任何版本的 PF 中都能无修改地工作。随着 OpenBSD 4.7 引入的语法变化,即使是从早期 PF 版本的升级,也需要进行一定的转换工作。
^([6]) 提供这些测试详细信息的文章可以在 Daniel Hartmeier 的网站上找到。请参阅 www.benzedrine.cx/pf-paper.html。
^([7]) 曾经存在过一款运行在 Microsoft Windows 上的个人防火墙产品,名为Core Force,它是基于 PF 的移植版本。到 2010 年初,开发 Core Force 的公司 Core Security(http://force.coresecurity.com/)似乎已经将重心转向了其他安全领域,如渗透测试,但该产品仍然可以下载。
^([8]) 三个字母的缩写 FAQ 可以扩展为frequently asked questions或frequently answered questions——两者都是有效的。
第二章:PF 配置基础

在本章中,我们将创建一个非常简单的 PF 配置。我们将从最简单的配置开始:一台配置为与单一网络通信的机器。这个网络很可能就是互联网。
配置 PF 的两大工具是你最喜欢的文本编辑器和 pfctl 命令行管理工具。PF 配置文件通常存储在 /etc/pf.conf 中,称为 规则集,因为配置文件中的每一行都是一个 规则,帮助确定数据包过滤子系统应该如何处理它看到的网络流量。在日常管理中,你编辑 /etc/pf.conf 文件中的配置,然后使用 pfctl 加载更改。虽然有 Web 界面可用于 PF 管理任务,但它们并不是系统的基础部分。PF 的开发者对这些选项并不排斥,但他们至今尚未见到一个显著优于编辑 pf.conf 文件并使用 pfctl 命令的图形界面。
第一步:启用 PF
在你开始使用 PF 和相关工具进行网络配置的有趣部分之前,你需要确保 PF 可用并已启用。具体的操作细节取决于你使用的操作系统:OpenBSD、FreeBSD 或 NetBSD。根据你的操作系统,按照相应的说明检查设置,然后继续阅读 简单的 PF 规则集:单一独立机器。
pfctl 命令是一个需要比普通用户默认权限更高的程序。在本书的其余部分,你将看到需要额外权限的命令,这些命令前面会加上 sudo。如果你还没有开始使用 sudo,应该开始使用它。OpenBSD 系统中自带了 sudo。在 FreeBSD、DragonFly BSD 和 NetBSD 中,也可以通过端口系统或 pkgsrc 系统轻松获取,并作为 security/sudo 提供。
这里有一些关于使用 pfctl 的一般说明:
-
禁用 PF 的命令是
pfctl -d。输入该命令后,所有可能已启用的基于 PF 的过滤规则将被禁用,所有流量将被允许通过。 -
为了方便,
pfctl可以在单行命令中执行多个操作。要启用 PF 并加载规则集,可以输入以下命令:$ **sudo pfctl -ef /etc/pf.conf**
在 OpenBSD 上设置 PF
在 OpenBSD 4.6 及之后的版本中,你无需手动启用 PF,因为它默认启用并且已有基本配置。如果你在系统启动时密切关注系统控制台,可能会注意到在内核消息完成后不久,pf enabled 消息就会出现。
如果你在启动时没有在控制台看到 pf enabled 消息,你有几个选项可以检查 PF 是否已经启用。一种简单的方法是输入你通常用来从命令行启用 PF 的命令:
$ **sudo pfctl -e**
如果 PF 已经启用,系统会返回以下消息:
pfctl: pf already enabled
如果 PF 没有启用,pfctl -e 命令会启用 PF 并显示如下信息:
pf enabled
在 OpenBSD 4.6 版本之前,PF 默认并未启用。你可以通过编辑 /etc/rc.conf.local 文件(如果该文件不存在,可以创建该文件)来覆盖默认设置。虽然在较新的 OpenBSD 版本中不再需要这么做,但在你的 /etc/rc.conf.local 文件中添加这一行并不会有害:
pf=YES # enable PF
如果你查看在全新 OpenBSD 安装中的 /etc/pf.conf 文件,你会第一次接触到一个有效的规则集。
默认的 OpenBSD pf.conf 文件从 set skip on lo 规则开始,确保环回接口组上的流量不会受到任何过滤。接下来的有效行是一个简单的 pass 默认规则,允许网络流量默认通过。最后,一个显式的 block 规则阻止远程的 X11 流量访问你的机器。
正如你可能已经注意到的,默认的 pf.conf 文件中也包含了一些以井号(#)开头的注释行。在这些注释中,你会找到一些建议的规则,这些规则提示了有用的配置,比如通过 ftp-proxy 实现的 FTP 透传(见 第三章)和 OpenBSD 的垃圾邮件延迟守护进程 spamd(见 第六章)。这些项在各种实际场景中可能有用,但由于它们在所有配置中并不总是相关,因此默认情况下被注释掉了。
如果你查看 /etc/rc.conf 文件中的与 PF 相关的设置,你会找到 pf_rules= 设置。原则上,这让你可以指定你的配置文件位于默认的 /etc/pf.conf 之外。然而,改变这个设置可能不值得麻烦。使用默认设置可以让你利用一些自动化管理功能,比如将配置文件自动备份到 /var/backups。
在 OpenBSD 中,/etc/rc 脚本内置了一种机制,如果你在没有 pf.conf 文件或文件中包含无效规则集的情况下重启,它可以帮助你。在启用任何网络接口之前,rc 脚本加载一个规则集,允许一些基本服务:从任何地方的 SSH 连接、基本的名称解析和 NFS 挂载。这使得你可以登录并修正规则集中的任何错误,加载修正后的规则集,然后继续工作。
在 FreeBSD 上设置 PF
好的代码传播得很快,FreeBSD 用户会告诉你,其他地方的好代码最终都会找到通向 FreeBSD 的路。PF 也不例外,从 FreeBSD 5.2.1 和 4.x 系列版本开始,PF 和相关工具成为了 FreeBSD 的一部分。
如果你阅读了关于在 OpenBSD 上设置 PF 的前一节,你会发现,在 OpenBSD 上 PF 默认是启用的。但在 FreeBSD 上情况不同,PF 是三种可能的包过滤选项之一。在这里,你需要采取明确的步骤来启用 PF,与 OpenBSD 相比,你似乎需要在/etc/rc.conf中做更多的配置。从/etc/defaults/rc.conf文件来看,FreeBSD 中 PF 相关设置的默认值如下:
pf_enable="NO" # Set to YES to enable packet filter (PF)
pf_rules="/etc/pf.conf" # rules definition file for PF
pf_program="/sbin/pfctl" # where pfctl lives
pf_flags="" # additional flags for pfctl
pflog_enable="NO" # set to YES to enable packet filter logging
pflog_logfile="/var/log/pflog" # where pflogd should store the logfile
pflog_program="/sbin/pflogd" # where pflogd lives
pflog_flags="" # additional flags for pflogd
pfsync_enable="NO" # expose pf state to other hosts for syncing
pfsync_syncdev="" # interface for pfsync to work through
pfsync_ifconfig="" # additional options to ifconfig(8) for pfsync
幸运的是,你可以安全地忽略其中的大部分内容——至少目前是这样。以下是你需要添加到/etc/rc.conf配置中的唯一选项:
pf_enable="YES" # Enable PF (load module if required)
pflog_enable="YES" # start pflogd(8)
在 FreeBSD 的不同版本中,PF 有一些差异。请参阅FreeBSD 手册,该手册可从www.freebsd.org/获取——特别是“防火墙”章节中的 PF 部分——以查看哪些信息适用于你的情况。FreeBSD 9 和 10 中的 PF 代码与 OpenBSD 4.5 中的代码等效,只有一些 bug 修复。本书中的说明假设你正在运行 FreeBSD 9.0 或更新版本。
在 FreeBSD 上,PF 默认是作为内核可加载模块编译的。如果你的 FreeBSD 系统使用 GENERIC 内核,你应该能够使用以下命令启动 PF:
$ **sudo kldload pf**
$ **sudo pfctl -e**
假设你已经在/etc/rc.conf中添加了刚才提到的行,并创建了/etc/pf.conf文件,你也可以使用 PF 的rc脚本来运行 PF。以下命令启用 PF:
$ **sudo /etc/rc.d/pf start**
这将禁用包过滤:
$ **sudo /etc/rc.d/pf stop**
注意
在 FreeBSD 上, /etc/rc.d/pf 脚本至少需要在 /etc/rc.conf 中有一行pf_enable="YES",并且需要一个有效的 /etc/pf.conf 文件。如果这两个要求中的任何一个没有满足,脚本将会退出并显示错误信息。在默认的 FreeBSD 安装中没有 /etc/pf.conf 文件,因此你需要在启用 PF 后重新启动系统之前创建一个。为了我们的目的,使用touch创建一个空的 /etc/pf.conf 文件就足够了,但你也可以从系统提供的 /usr/share/examples/pf/pf.conf *文件的副本开始。
提供的示例文件* /usr/share/examples/pf/pf.conf 没有任何活动的设置。它只有以#字符开头的注释行和被注释掉的规则,但它确实给你展示了一个工作规则集的预览。例如,如果你删除位于set skip on lo前面的#符号来取消注释该行,然后将文件保存为你的/etc/pf.conf*,一旦启用 PF 并加载规则集,你的回环接口组将不会被过滤。然而,即使 PF 在你的 FreeBSD 系统上被启用,由于我们还没有编写实际的规则集,PF 也没有做什么,所有数据包都会通过。
截至本文撰写时(2014 年 8 月),FreeBSD 的rc脚本未设置默认规则集作为回退选项,如果从/etc/pf.conf读取的配置无法加载。这意味着,如果启用 PF 时没有规则集,或者pf.conf文件的内容在语法上无效,PF 将启用并应用默认的pass all规则集。
在 NetBSD 上设置 PF
在 NetBSD 2.0 中,PF 作为一个可加载的内核模块提供,可以通过软件包(security/pflkm)安装,或者编译到静态内核配置中。在 NetBSD 3.0 及更高版本中,PF 是系统的基础部分。在 NetBSD 中,PF 是多种数据包过滤系统之一,你需要采取明确的措施来启用它。
一些 PF 配置的细节在 NetBSD 的不同版本之间发生了变化。本书假设你使用的是 NetBSD 6.0 或更高版本。^([10])
要在 NetBSD 上使用可加载的 PF 模块,请将以下行添加到/etc/rc.conf中,以分别启用可加载的内核模块、PF 和 PF 日志接口。
lkm="YES" # do load kernel modules
pf=YES
pflogd=YES
若要手动加载pf模块并启用 PF,请输入以下命令:
$ **sudo modload /usr/lkm/pf.o**
$ **sudo pfctl -e**
另外,你可以运行rc.d脚本来启用 PF 和日志记录,方法如下:
$ **sudo /etc/rc.d/pf start**
$ **sudo /etc/rc.d/pflogd start**
若要在启动时自动加载模块,请将以下行添加到/etc/lkm.conf文件中:
/usr/lkm/pf.o - - - - BEFORENET
如果你的/usr文件系统位于单独的分区上,请将以下行添加到/etc/rc.conf中:
critical_filesystems_local="${critical_filesystems_local} /usr"
如果此时没有错误,说明你已在系统上启用了 PF,并且可以继续创建完整的配置。
提供的/etc/pf.conf文件不包含任何活动设置;它只有以井号(#)开头的注释行和被注释掉的规则。不过,它确实给你提供了一个工作规则集的预览。例如,如果你去掉set skip on lo那一行前的井号以取消注释,并保存文件,那么在启用 PF 并加载规则集后,你的回环接口将不会被过滤。然而,即使在你的 NetBSD 系统上启用了 PF,我们还没有编写实际的规则集,所以 PF 实际上并没有做任何事情,只是传递数据包。
NetBSD 通过文件/etc/defaults/pf.boot.conf实现了默认或回退规则集。此规则集仅用于在/etc/pf.conf文件不存在或包含无效规则集时,帮助系统完成启动过程。你可以通过在/etc/pf.boot.conf中添加自定义规则来覆盖默认规则。
简单的 PF 规则集:单台独立机器
主要为了提供一个通用的、最小的基准,我们将从最简单的配置开始构建规则集。
最小规则集
最简单的 PF 设置是在一台不会运行任何服务、只与一个网络(可能是互联网)通信的单一计算机上。
我们将从一个类似下面的/etc/pf.conf文件开始:
block in all
pass out all keep state
该规则集拒绝所有进入流量,允许我们发送的流量,并保持我们连接的状态信息。PF 从上到下读取规则;规则集中与数据包或连接匹配的最后一条规则是被应用的规则。
在这里,来自任何其他地方进入我们系统的任何连接都会匹配block in all规则。即使有了这个初步结果,规则评估将继续进行到下一条规则(pass out all keep state),但流量不会匹配到这条规则中的第一个标准(即out方向)。由于没有更多的规则可以评估,状态不会发生变化,流量将被阻塞。以类似的方式,任何从具有此规则集的机器发起的连接都不会匹配第一条规则(再次是错误的方向),但会匹配第二条规则,这是一条pass规则,连接将被允许通过。
我们将在第三章中,结合一个稍长的规则集,更详细地探讨 PF 如何评估规则以及规则顺序的重要性。
对于任何包含keep state部分的规则,PF 会保持有关连接的信息,包括各种计数器和序列号,作为状态表中的一项条目。状态表是 PF 保存已匹配规则的现有连接的信息的地方,新的到达数据包会首先与现有的状态表条目进行比较以查找匹配。只有当数据包与任何现有状态不匹配时,PF 才会进行完整的规则集评估,检查数据包是否与加载的规则集中的某条规则匹配。我们还可以指示 PF 以各种方式处理状态信息,但在这种简单的情况下,我们的主要目标是允许我们发起的连接的返回流量返回给我们。
请注意,在 OpenBSD 4.1 及更高版本中,pass规则的默认行为是保持状态信息,^([11]),我们在这种简单情况下不再需要显式地指定keep state。这意味着规则集可以写成这样:
# minimal rule set, OpenBSD 4.1 onward keeps state by default
block in all
pass out all
实际上,如果你愿意,甚至可以省略这里的all关键字。
其他 BSD 系统现在大多数已经跟上了这个变化,接下来的部分我们将坚持使用较新的规则,如果你正在使用旧系统,偶尔会提醒你。
不言而喻,允许来自特定主机的所有流量通过意味着该主机是可信任的。只有在你确信可以信任该机器时,才会这么做。
当你准备好使用此规则集时,使用以下命令加载:
$ **sudo pfctl -ef /etc/pf.conf**
规则集应该能够加载而不显示任何错误消息或警告。在除最慢的系统外,应该会立即返回到$提示符。
测试规则集
测试规则集以确保它们按预期工作始终是一个好主意。当你转向更复杂的配置时,适当的测试变得至关重要。
要测试这个简单的规则集,看看它是否能够执行域名解析。例如,你可以检查$ host nostarch.com是否返回信息,如主机nostarch.com的 IP 地址和该域名邮件交换器的主机名。或者直接看看你是否能上网。如果你能够通过域名连接到外部网站,那么这个规则集就允许你的系统执行域名解析。基本上,你尝试从自己的系统访问的任何服务都应该能正常工作,而你尝试从另一台机器访问系统中的任何服务则应该产生Connection refused消息。
稍微严格一点:使用列表和宏来提高可读性
上一节中的规则集是一个极其简单的规则集——可能对于实际使用来说太过简化。但它是一个很好的起点,可以在此基础上构建出稍微更有结构和完整的配置。我们将从拒绝所有服务和协议开始,然后只允许我们知道需要的那些,^([12]),并使用列表和宏来提高可读性和控制性。
列表只是指两个或更多相同类型的对象,你可以在规则集中引用它们,如下所示:
pass proto tcp to port { 22 80 443 }
这里,{ 22 80 443 }就是一个列表。
宏是一个纯粹的可读性工具。如果你在配置中有多次引用的对象,例如一个重要主机的 IP 地址,那么定义一个宏可能会很有用。例如,你可以在规则集中早期定义这个宏:
external_mail = 192.0.2.12
然后,你可以在规则集中稍后引用这个主机,写成$external_mail:
pass proto tcp to $external_mail port 25
这两种技术在保持规则集的可读性方面具有巨大潜力,因此,它们是有助于实现保持对网络控制的总体目标的重要因素。
一个更严格的基础规则集
到目前为止,我们在处理我们自己生成的任何流量时相对宽松。宽松的规则集在检查基本连通性是否正常或检查过滤是否是我们遇到的问题的一部分时非常有用。一旦“我们有连通性吗?”的阶段结束,就该开始收紧规则,创建一个我们能够掌控的基础规则集。
首先,将以下规则添加到/etc/pf.conf:
block all
这个规则是完全限制性的,它将阻止所有方向的流量。这是我们将在接下来几章中使用的所有完整规则集的初始基础过滤规则。我们基本上是从零开始,配置一个不允许任何流量通过的规则。稍后,我们将添加一些规则,放宽流量限制,但我们会逐步进行,并确保始终保持对系统的控制。
接下来,我们将定义一些宏,以便在规则集中后续使用:
tcp_services = "{ ssh, smtp, domain, www, pop3, auth, https, pop3s }"
udp_services = "{ domain }"
在这里,你可以看到列表和宏的结合如何对我们有利。宏可以是列表,正如示例中所演示的,PF 理解使用服务名称和端口号的规则,这些服务名称和端口号列在你的 /etc/services 文件中。我们将小心使用所有这些元素以及一些进一步提高可读性的技巧,来处理那些需要更复杂规则集的复杂情况。
定义了这些宏之后,我们可以在规则中使用它们,接下来我们将稍微编辑规则,使其如下所示:
block all
pass out proto tcp to port $tcp_services
pass proto udp to port $udp_services
字符串 $tcp_services 和 $udp_services 是宏引用。规则集中出现的宏会在规则集加载时展开,运行中的规则集会在引用宏的地方插入完整的列表。根据宏的具体性质,它们可能导致带有宏引用的单一规则展开为多个规则。即使在像这样的小规则集中,使用宏也使得规则更易于理解和维护。规则集中需要显示的信息量减少,且通过合理的宏名称,逻辑变得更清晰。在典型规则集的逻辑中,大多数时候我们不需要在每个宏引用的位置看到完整的 IP 地址或端口号列表。
从实际的规则集维护角度来看,重要的是要记住允许哪些服务使用哪些协议,以保持一个适当的控制状态。根据协议将允许的服务分开列出,很可能有助于保持规则集既功能完备又易于阅读。
TCP 与 UDP
我们已经小心地将 UDP 服务与 TCP 服务分开。几个服务主要运行在 TCP 或 UDP 上的知名端口号上,一些服务则根据特定条件在 TCP 和 UDP 之间切换。
这两种协议在多个方面有所不同。TCP 是面向连接的,并且可靠,是进行有状态过滤的完美候选。而 UDP 是无状态的、无连接的,但 PF 会为 UDP 流量创建并维护等同于状态信息的数据,以确保如果返回的 UDP 流量与现有状态匹配,则允许其通过。
一个常见的 UDP 状态信息有用的例子是处理名称解析。当你请求一个名称服务器将域名解析为 IP 地址,或将 IP 地址解析回主机名时,合理的假设是你希望收到答案。保留 UDP 流量的状态信息,或其功能等效物,可以实现这一点。
重新加载规则集并查找错误
在我们修改了 pf.conf 文件之后,需要按如下方式加载新规则:
$ **sudo pfctl -f /etc/pf.conf**
如果没有语法错误,pfctl 在加载规则时不应显示任何消息。
如果你更喜欢显示详细输出,可以使用 -v 标志:
$ **sudo pfctl -vf /etc/pf.conf**
当你使用详细模式时,pfctl 应该会在返回命令行提示符之前,将你的宏展开为各自的规则,如下所示:
$ **sudo pfctl -vf /etc/pf.conf**
tcp_services = "{ ssh, smtp, domain, www, pop3, auth, https, pop3s }"
udp_services = "{ domain }"
block drop all
pass out proto tcp from any to any port = ssh flags S/SA keep state
pass out proto tcp from any to any port = smtp flags S/SA keep state
pass out proto tcp from any to any port = domain flags S/SA keep state
pass out proto tcp from any to any port = www flags S/SA keep state
pass out proto tcp from any to any port = pop3 flags S/SA keep state
pass out proto tcp from any to any port = auth flags S/SA keep state
pass out proto tcp from any to any port = https flags S/SA keep state
pass out proto tcp from any to any port = pop3s flags S/SA keep state
pass proto udp from any to any port = domain keep state
$ _
将此输出与您实际编写的/etc/pf.conf文件的内容进行比较。我们的单个 TCP 服务规则扩展为八个不同的规则:每个服务都有一个对应的规则。单个 UDP 规则只处理一个服务,并且它从我们写的内容扩展到包括默认选项。请注意,规则被完整地显示出来,默认值如flags S/SA keep state已应用到你未明确指定的任何选项。这就是实际加载的配置。
检查你的规则
如果你对规则集做了大量更改,在尝试加载规则集之前,使用以下命令检查它们:
$ pfctl -nf /etc/pf.conf
-n选项告诉 PF 仅解析规则,而不加载它们——这基本上是一次模拟运行,可以让你检查和纠正任何错误。如果pfctl在规则集中发现任何语法错误,它将退出并显示错误信息,指明错误发生的行号。
一些防火墙指南建议你确保旧的配置已被完全删除,否则你可能会遇到问题——你的防火墙可能处于某种中间状态,这种状态既不符合之前的状态,也不符合之后的状态。使用 PF 时,这种情况完全不成立。最后加载的有效规则集会一直处于活动状态,直到你禁用 PF 或加载新的规则集。pfctl 会检查语法,然后在切换到新规则集之前完全加载新的规则集。这通常被称为原子规则集加载,意味着一旦有效的规则集加载完毕,就不会有部分规则集或没有加载规则的中间状态。一个后果是,匹配旧规则集和新规则集中都有效的状态的流量不会被中断。
除非你真的按照一些旧指南的建议,刷新了现有的规则(这确实可以通过使用pfctl -F all或类似命令实现),然后再尝试从配置文件加载新的规则,否则最后有效的配置将继续保持加载。实际上,刷新规则集通常不是一个好主意,因为它实际上将你的数据包过滤器置于pass all模式,这样既会为任何流量打开大门,又可能在你准备加载新规则时中断有用的流量。
测试已更改的规则集
一旦你有了一个pfctl能加载且没有任何错误的规则集,就该检查你编写的规则是否按预期工作了。像我们之前做的那样,使用命令$ host nostarch.com测试名称解析应该仍然能正常工作。不过,最好选择一个你最近没有访问过的域名,比如你不会考虑投票给的某个政党,这样可以确保你没有从缓存中拉取 DNS 信息。
你应该能够浏览网页并使用几个与邮件相关的服务,但由于这个更新的规则集的性质,任何尝试访问除已定义的 TCP 服务(如 SSH、SMTP 等)以外的远程系统服务应该会失败。就像我们简单的规则集一样,系统应拒绝所有不匹配现有状态表条目的连接;只有由本机发起的连接的返回流量才会被允许进入。
显示有关系统的信息
你对规则集进行的测试应该已经显示 PF 正在运行,并且规则行为符合预期。有几种方法可以跟踪运行中的系统中发生的事情。提取 PF 信息的其中一种更直接的方式是使用已经熟悉的pfctl程序。
一旦 PF 启用并运行,系统会根据网络活动更新各种计数器和统计信息。要确认 PF 是否正在运行并查看其活动的统计信息,可以使用pfctl -s,后面跟上你想显示的信息类型。可以显示的信息类型有很多种(请参阅man 8 pfctl并查找-s选项)。我们将在第九章中回到这些显示选项,并在第十章中更详细地讨论它们提供的一些统计信息,我们将利用这些数据来优化我们正在构建的配置。
以下是pfctl -s info输出的一个例子(摘自我的家庭网关)。显示系统实际传输流量的高层信息可以在这部分找到。
**$ sudo pfctl -s info**
Status: Enabled for 24 days 12:11:27 Debug: err
Interface Stats for nfe0 IPv4 IPv6
Bytes In 43846385394 0
Bytes Out 20023639992 64
Packets In
Passed 49380289 0
Blocked 49530 0
Packets Out
Passed 45701100 1
Blocked 1034 0
State Table Total Rate
current entries 319
searches 178598618 84.3/s
inserts 4965347 2.3/s
removals 4965028 2.3/s
pfctl输出的第一行表明 PF 已经启用,并且运行了稍多于三周的时间,这与我上次进行需要重启的系统升级的时间相等。
显示中的Interface Stats部分表明系统管理员选择了一个接口(此处为nfe0)作为系统的日志接口,并显示该接口处理的进出字节。如果没有选择日志接口,则显示会稍有不同。现在是检查系统输出的好时机。接下来的几项可能会更引人兴趣,在我们的上下文中,它们显示了每个方向上被阻止或通过的数据包数量。在这里,我们可以看到过滤规则是否有效的初步迹象。在这个例子中,规则集要么很好地匹配了预期的流量,要么我们有相当守规矩的用户和访客,在两个方向上,通过的数据包数量远大于被阻止的数据包数量。
接下来,系统处理流量的另一个重要指标是 状态表 统计信息。状态表的 当前条目 行显示有 319 个活动状态或连接,而状态表平均每秒搜索匹配现有状态的次数略多于 84 次,自计数器重置以来,总共进行了超过 1.78 亿次搜索。插入 和 移除 计数器显示了状态被创建和删除的次数。如预期所示,插入和移除的次数与当前活动状态的数量不同,速率计数器显示,自计数器最后重置以来,创建和删除状态的速率与此显示的分辨率完全一致。
这里的信息大致与配置为仅支持 IPv4 的小型网络网关上你应当看到的统计数据一致。IPv6 列中的数据包传递不必惊慌。OpenBSD 内置了 IPv6。在网络接口配置期间,默认情况下,TCP/IP 堆栈会发送 IPv6 邻居发现请求,用于链路本地地址。在一个正常的仅 IPv4 配置中,只有前几个数据包实际上会通过,等到来自/etc/pf.conf的 PF 规则集完全加载时,IPv6 数据包会被默认的“阻止所有”规则拦截。(在这个示例中,它们不会出现在 nfe0 的统计信息中,因为 IPv6 是通过不同的接口进行隧道传输的。)
展望未来
现在你应该有一台能够与其他联网机器通信的机器,使用一个非常基本的规则集,它作为控制网络流量的起点。随着你阅读本书,你将学习如何添加执行各种有用操作的规则。在第三章中,我们将扩展配置,使其充当一个小型网络的网关。为几台计算机提供服务会有一些后果,我们将研究如何至少允许某些 ICMP 和 UDP 流量通过——如果没有别的,至少用于你自己的故障排除需求。
在第三章中,我们还将考虑对你的安全有影响的网络服务,如 FTP。智能使用数据包过滤来处理那些安全要求较高的服务,是本书中的一个反复出现的主题。
^([9]) 如果你在设置 OpenBSD 版本低于这个版本的第一个 PF 配置,最好的建议是升级到最新的稳定版本。如果由于某种原因你必须继续使用旧版本,查阅本书的第一版以及你使用的特定版本的 man 页和其他文档可能会有所帮助。
^([10]) 有关在早期版本中使用 PF 的说明,请参阅你所使用版本的文档,并查阅本书附录 A 中列出的相关资料。
^([11]) 事实上,新的默认设置对应于保持状态标志 S/SA,确保只有在连接建立过程中初始的 SYN 数据包会创建状态,从而消除了某些令人困惑的错误场景。为了无状态地过滤,你可以为那些不想记录或保留状态信息的规则指定no state。在 FreeBSD 中,OpenBSD 4.1 等效的 PF 代码已被合并到 7.0 版本中。如果你正在使用一个较旧的 PF 版本,且它没有这个默认设置,这强烈表明你应该尽快考虑升级操作系统。
^([12]) 为什么要将规则集设置为默认拒绝?简短的答案是,这样能让你更好地控制。数据包过滤的目的是掌控局面,而不是在坏人行动之后再做补救。Marcus Ranum 写过一篇非常有趣且富有启发性的文章,名为《计算机安全中的六大愚蠢想法》(www.ranum.com/security/computer_security/editorials/dumb/index.html)。
第三章. 进入真实世界

前一章演示了单台机器上基本数据包过滤的配置。在这一章中,我们将在此基础设置上进行扩展,但将进入更常规的领域:数据包过滤“网关”。尽管这一章中的大部分内容在单机设置中可能也很有用,但我们的主要关注点是设置一个网关,转发特定的网络流量并处理基本本地网络的常见网络服务。
一个简单的网关
我们将从构建你可能与“防火墙”一词关联的东西开始:一台作为至少一台其他机器网关的机器。除了在它的多个网络之间转发数据包外,这台机器的使命是提高它所处理的网络流量的信噪比。这就是我们的 PF 配置发挥作用的地方。
但在深入实际的配置细节之前,我们需要深入一些理论,完善一些概念。请耐心等一下;这将帮助你避免我在邮件列表、新闻组和网络论坛上经常看到的一些头痛问题。
保持简单:避免陷入“in”、“out”和“on”的误区
在单机设置中,生活相对简单。你创建的流量应该要么传递到外部世界,要么被你的过滤规则阻止,而你可以决定想要让哪些流量从其他地方进入。
当你设置一个网关时,你的视角会发生变化。你从“是我与外部网络对抗”的心态转变为“我是决定所有我连接的网络之间传输什么内容的人”。这台机器有多个,或者至少有两个,网络接口,每个接口连接到一个独立的网络,它的主要功能(或者至少是我们在这里关注的功能)是转发网络流量在各个网络之间。概念上,网络看起来应该像图 3-1 那样。
图 3-1. 具有单一网关的网络
如果你希望将流量从连接到re1的网络传递到连接到re0的主机,认为你需要像下面这样的规则是非常合理的:^([13])
pass in proto tcp on re1 from re1:network to re0:network \
port $ports keep state
然而,在防火墙配置中,最常见且最令人抱怨的错误之一是没有意识到to关键字本身并不能保证数据包能到达终点。这里的to关键字仅仅意味着数据包或连接必须具有与这些标准匹配的目的地址,才能匹配该规则。我们刚才写的规则允许流量“进入”到网关本身,并且“在”规则中指定的特定接口上。为了让数据包进一步进入并传递到下一个网络,我们需要一个匹配的规则,类似这样:
pass out proto tcp on re0 from re1:network to re0:network \
port $ports keep state
但是,请停下来再看一遍这些规则。最后这一条规则只允许目的地在直接连接到 re0 的网络中的数据包通过,其他的都不行。如果这正是你想要的,那就好了。在其他情况下,这样的规则虽然完全有效,但比实际需要的要更具体。很容易让自己陷入具体细节而忽视配置目的的更高层次视角——并可能在此过程中让自己陷入更多的调试循环。
如果有充分的理由编写非常具体的规则,比如前面的规则,你可能已经知道需要这些规则以及为什么需要它们。当你读完本书(如果不是更早一些的话)时,你应该能够清楚地表达出在何种情况下需要更具体的规则。然而,对于本章中的基本网关配置,你可能会希望编写不特定于接口的规则。事实上,在某些情况下,指定方向并没有什么实际意义;你只需使用如下规则来让本地网络访问互联网:
pass proto tcp from re1:network to port $ports keep state
对于简单的设置,绑定到接口的 in 和 out 规则可能会为你的规则集增加比其价值更多的杂乱。对于繁忙的网络管理员来说,一个易于阅读的规则集是更安全的。(我们将在第十章中讨论一些额外的安全措施,如 antispoof。)
本书的其余部分,在一些例外情况下,我们将尽可能简化规则,以便于阅读。
网络地址转换与 IPv6
一旦我们开始处理不同网络之间的流量,了解网络地址是如何工作的以及为什么你可能会遇到几种不同的地址方案就变得非常有用。多年来,网络地址一直是混乱和流行词汇的来源。除非你查阅相关文档并深入研究一系列 RFC,否则很难确定其中的事实。在接下来的几段中,我将努力澄清一些混乱。
例如,一个广泛流传的观点是,如果你拥有一个使用与连接互联网接口分配的地址范围完全不同的内部网络,那么你就安全了,外界无法访问你的网络资源。这个观点与这样一个想法密切相关:防火墙在本地网络中的 IP 地址必须是 192.168.0.1 或 10.0.0.1。
这两种观点都有一定的真实性,而且这些地址是常见的默认值。但真正的情况是,尽管 PF 提供了一些技巧使这一任务更难,但仍然有可能绕过网络地址转换。
我们使用一套特定的内部地址范围和一套不同的外部地址范围,并不是为了主要解决安全问题。而是因为这是绕过互联网协议设计问题的最简单方式:有限的地址范围。
在 1980 年代,当互联网协议被制定时,互联网上的大多数计算机(当时被称为 ARPANET)都是大型计算机,每台有几十到几千个用户。当时,32 位地址空间提供的超过 40 亿个地址似乎相当充足,但几个因素证明了这一假设是错误的。一个因素是地址分配过程导致在一些世界人口较多的国家甚至还没有连接互联网之前,已经分配了大量的地址空间。另一个,更重要的因素是,到 1990 年代初,互联网不再是一个研究项目,而是一个商业化的资源,消费者和各种规模的公司以惊人的速度消耗 IP 地址空间。
长期解决方案是重新定义互联网,使用更大的地址空间。1998 年,IPv6 的规范发布了,提供了 128 位的地址空间,总共有 2¹²⁸个地址,作为 RFC 2460 发布。但在我们等待 IPv6 广泛可用的同时,我们需要一个临时解决方案。这个解决方案通过一系列 RFCs 提出,指定了网关如何转发经过地址转换的流量,使得大型本地网络在互联网上看起来像一个计算机。某些以前未分配的 IP 地址范围被划定为这些私有网络。这些地址范围对任何人都是免费的,前提是这些范围内的流量不得未经转换直接进入互联网。因此,网络地址转换(NAT)在 1990 年代中期诞生,并迅速成为本地网络中处理地址的默认方式。^([14])
PF 支持 IPv6 以及各种 IPv4 地址转换技巧。(实际上,BSD 是最早采用 IPv6 的系统之一,得益于 KAME 项目的努力。^([15])) 所有支持 PF 的系统也都支持 IPv4 和 IPv6 地址族。如果你的 IPv4 网络需要 NAT 配置,你可以在 PF 规则集中根据需要集成地址转换。换句话说,如果你使用的是支持 PF 的系统,你可以合理地确信,至少在操作系统层面,你的 IPv6 需求已经得到满足。然而,一些带有 PF 端口的操作系统使用的是较旧版本的代码,值得注意的是,“更新的 PF 代码更好”这一通用规则同样适用于 IPv6 环境。
本书中的示例主要使用 IPv4 地址和必要时的 NAT,但大部分内容同样适用于已实现 IPv6 的网络。
最终准备:定义本地网络
在第二章中,我们为一台单独的独立机器设置了配置。接下来,我们将把这个配置扩展到网关版本,并且定义一些宏来帮助提高可读性,并在概念上将你有一定控制权的本地网络与其他网络分开。那么,如何在 PF 术语中定义你的“本地”网络呢?
在本章前面,你已经看到了interface:network的表示法。这是一种很好的简写方式,但你可以通过稍微扩展宏的使用,使规则集更加易读且便于维护。例如,你可以将$localnet宏定义为直接连接到你内部接口的网络(在我们的示例中是re1:network)。或者,你也可以将$localnet的定义更改为 IP 地址/子网掩码表示法,来表示一个网络,比如192.168.100.0/24表示一个私有 IPv4 地址子网,或者2001:db8:dead:beef::/64表示一个 IPv6 范围。
如果你的网络环境要求,你可以将$localnet定义为一个网络列表。例如,结合使用宏的pass规则,像下面这样的合理$localnet定义,可能会帮你省去不少麻烦:
pass proto { tcp, udp } from $localnet to port $ports
从这里开始,我们将坚持使用像$localnet这样的宏,来提高可读性。
设置网关
我们将以上一章中从头开始构建的单机配置为基础,来构建我们的数据包过滤网关。我们假设机器已经安装了另一块网络卡(或者你已经通过以太网、PPP 或其他方式,从本地网络连接到一个或多个其他网络)。
在我们的语境中,接口如何配置并不特别重要。我们只需要知道接口已经启用并正常工作。
在接下来的讨论和示例中,PPP 和以太网设置之间的唯一不同是接口名称,我们会尽量快速地去除接口名称,以便让示例更简洁。
首先,因为 BSD 系统默认禁用了数据包转发,我们需要启用它,以便让计算机将接收到的网络流量从一个接口转发到其他网络通过一个或多个单独的接口。最初,我们将在命令行使用sysctl命令来启用传统的 IPv4 转发:
# sysctl net.inet.ip.forwarding=1
如果需要转发 IPv6 流量,我们使用这个sysctl命令:
# sysctl net.inet6.ip6.forwarding=1
目前这样设置是可以的。然而,为了在将来重新启动计算机时仍然有效,你需要将这些设置输入到相关的配置文件中。
在 OpenBSD 和 NetBSD 中,你需要通过编辑/etc/sysctl.conf并在文件末尾添加 IP 转发行,使得最后几行看起来像这样:
net.inet.ip.forwarding=1
net.inet6.ip6.forwarding=1
在 FreeBSD 中,通过将这些行添加到/etc/rc.conf来进行更改:
gateway_enable="YES" #for ipv4
ipv6_gateway_enable="YES" #for ipv6
最终效果是相同的;FreeBSD 的rc脚本通过sysctl命令设置这两个值。然而,FreeBSD 的更大一部分配置集中在rc.conf文件中。
现在是检查你打算使用的所有接口是否已经启动并运行的时机。使用ifconfig -a或ifconfig interface_name来查看。
我的一台系统上ifconfig -a的输出如下所示:
$ **ifconfig -a**
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 33224
groups: lo
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x5
xl0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:60:97:83:4a:01
groups: egress
media: Ethernet autoselect (100baseTX full-duplex)
status: active
inet 194.54.107.18 netmask 0xfffffff8 broadcast 194.54.107.23
inet6 fe80::260:97ff:fe83:4a01%xl0 prefixlen 64 scopeid 0x1
fxp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:30:05:03:fc:41
media: Ethernet autoselect (100baseTX full-duplex)
status: active
inet 194.54.103.65 netmask 0xffffffc0 broadcast 194.54.103.127
inet6 fe80::230:5ff:fe03:fc41%fxp0 prefixlen 64 scopeid 0x2
pflog0: flags=141<UP,RUNNING,PROMISC> mtu 33224
enc0: flags=0<> mtu 1536
你的设置很可能与此不同。这里,网关上的物理接口是xl0和fxp0。系统中可能还会有逻辑接口lo0(回环接口)、enc0(用于 IPSEC 的封装接口)和pflog0(PF 日志设备)。
如果你是拨号连接,可能使用某种变体的 PPP 进行互联网连接,外部接口是tun0伪设备。如果你的连接是通过某种宽带连接,你可能会有一个以太网接口可供使用。然而,如果你是使用以太网 PPP(PPPoE)的 ADSL 用户群体中的一员,那么正确的外部接口将是tun0或pppoe0中的一个伪设备(取决于你使用的是用户空间pppoe(8)还是内核模式pppoe(4)),而不是物理以太网接口。
根据你的具体设置,你可能需要对接口进行一些其他的设备特定配置。在配置完成后,你可以继续处理 TCP/IP 层面的问题,并进行数据包过滤配置。
如果你仍然打算允许来自内部机器的任何流量,你的初始网关设置中的/etc/pf.conf可能看起来大致如下:
ext_if = "re0" # macro for external interface - use tun0 or pppoe0 for PPPoE
int_if = "re1" # macro for internal interface
localnet = $int_if:network
# ext_if IPv4 address could be dynamic, hence ($ext_if)
match out on $ext_if inet from $localnet nat-to ($ext_if) # NAT, match IPv4 only
block all
pass from { self, $localnet }
注意使用宏为网络接口分配逻辑名称。在这里,使用的是 Realtek 以太网卡,但这是我们在此上下文中最后一次关注这个问题。
在像这样简单的设置中,使用这些宏可能不会带来太多好处,但一旦规则集变得稍微复杂些,你会开始欣赏它们所带来的可读性。
对这个规则集的一个可能优化是去除宏ext_if,并将$ext_if的引用替换为字符串egress,这是包含默认路由的接口所在的接口组的名称。接口组不是宏,因此你应当不带$字符地写出名称egress。
还请注意nat-to的match规则。这是处理从你本地网络中不可路由地址到分配给你的唯一官方地址的 NAT 的地方。如果你的网络使用官方的、可路由的 IPv4 地址,你可以直接省略这行配置。match规则是 OpenBSD 4.6 中引入的,它们可以在连接匹配某些标准时应用动作,而不决定连接是应该被阻止还是通过。
match规则最后部分($ext_if)周围的括号是为了补偿外部接口的 IP 地址可能是动态分配的情况。这个细节将确保即使接口的 IP 地址发生变化,你的网络流量也能顺利运行,不会出现严重的中断。
是时候总结我们迄今为止构建的规则集了:(1) 我们阻止来自我们自己网络外部的所有流量。 (2) 我们确保所有从我们本地网络中的主机发起的 IPv4 流量,在进入外部世界时,其源地址会被重写为网关外部接口分配的可路由地址。 (3) 最后,我们允许来自我们本地网络(IPv4 和 IPv6 均包括)和网关本身的所有流量通过。最后pass规则中的self关键字是 PF 语法中的一个类似宏的保留字,表示本地主机上所有接口分配的所有地址。
如果你的操作系统运行的是 OpenBSD 4.7 之前的 PF 版本,你的第一个网关规则集可能看起来像这样:
ext_if = "re0" # macro for external interface - use tun0 or pppoe0 for PPPoE
int_if = "re1" # macro for internal interface
localnet = $int_if:network
# ext_if IP address could be dynamic, hence ($ext_if)
nat on $ext_if inet from $localnet to any -> ($ext_if) # NAT, match IPv4 only
block all
pass from { self, $localnet } to any keep state
这里的nat规则处理的转换方式与前一个示例中的nat-to的match规则类似。
另一方面,这个规则集可能允许比你实际希望通过的流量更多。在我曾经工作过的一些网络中,规则集的主要部分基于一个名为client_out的宏:
client_out = "{ ftp-data, ftp, ssh, domain, pop3, auth, nntp, http,\
https, 446, cvspserver, 2628, 5999, 8000, 8080 }"
它有这个pass规则:
pass proto tcp from $localnet to port $client_out
这可能是一个有些奇特的端口选择,但它正是我那里的同事们当时所需要的。有些编号端口是为其他站点上为特定目的设置的系统所需要的。你的需求在某些细节上可能有所不同,但这应该涵盖了一些更常用的服务。
这里有另一个pass规则,对那些希望能够从其他地方管理机器的人很有用:
pass in proto tcp to port ssh
如果你更喜欢,也可以使用这种格式:
pass in proto tcp to $ext_if port ssh
当你完全省略from部分时,默认值是from any,这相当宽松。它允许你从任何地方登录,这对你经常旅行并需要从全球未知地点进行 SSH 访问时非常有用。如果你不怎么流动——比如你还没完全习惯参加远方的会议,或者你觉得同事们可以在你度假时自己应对——你可能想通过包含仅限你和其他管理员从合法地点登录的from部分来加强安全性。
我们的非常基础的规则集仍然不完整。接下来,我们需要让名称服务为我们的客户端工作。我们从规则集开始时的另一个宏开始:
udp_services = "{ domain, ntp }"
这通过一个规则来补充,该规则允许我们想要的流量通过防火墙:
pass quick proto { tcp, udp } to port $udp_services
请注意此规则中的quick关键字。我们已经开始编写由多个规则组成的规则集,现在是时候重新审视它们之间的关系和互动了。
如前一章所述,规则是按照它们在配置文件中书写的顺序从上到下进行评估的。对于 PF 评估的每个数据包或连接,最后匹配的规则就是被应用的规则。
quick关键字提供了逃离普通顺序的方式。当一个数据包匹配到quick规则时,数据包会按照当前规则进行处理。规则处理停止,不再考虑可能匹配该数据包的其他规则。随着你的规则集变得越来越长和复杂,你会发现这非常方便。例如,当你需要一些与一般规则隔离的例外时,这会非常有用。
这个quick规则还处理了 NTP,它用于时间同步。名称服务和时间同步协议的共同点是,在某些情况下,它们可能会交替通过 TCP 和 UDP 进行通信。
测试你的规则集
你可能还没来得及为你的规则集编写正式的测试套件,但完全有理由测试你的配置是否按预期工作。
前一章中的独立示例中的基本测试仍然适用。但现在,你需要从你网络中的其他主机以及你的数据包过滤网关进行测试。对于你在pass规则中指定的每项服务,测试你本地网络中的机器是否获得了有意义的结果。从你本地网络中的任何机器,输入类似这样的命令:
$ **host nostarch.com**
它应该返回与前一章中测试独立规则集时完全相同的结果,并且你指定的服务流量应该通过^([16])。
你可能觉得没必要,但检查一下规则集是否在你的网关外部按预期工作并没有坏处。如果你迄今为止做了本章所说的,那么从外部应该无法联系到你本地网络中的机器。
为什么只有 IP 地址——而不是主机名或域名?
看到到目前为止的示例,你可能已经注意到规则集中的宏总是展开为 IP 地址或地址范围,而从不展开为主机名或域名。你可能在想,为什么呢?毕竟,你已经看到 PF 允许在规则集中使用服务名称,那么为什么不包括主机名或域名呢?
答案是,如果你在规则集中使用了域名和主机名,那么规则集只有在名称服务运行并且可访问时才有效。在默认配置下,PF 会在任何网络服务运行之前加载。这意味着,如果你希望在 PF 配置中使用域名和主机名,你需要改变系统的启动顺序(可能需要编辑 /etc/rc.local),以便在名称服务可用后再加载依赖名称服务的规则集。如果你只需要引用少量的主机名或域名,将它们作为 IP 地址添加到 /etc/hosts 文件的名称映射条目中,并且不更改 rc 脚本,可能会更有用。
那个悲伤的旧 FTP 协议
我们刚才简要查看的实际 TCP 端口清单中,包含了FTP——经典的文件传输协议。FTP 是早期互联网的遗物,那时实验是常态,安全性并不是现代意义上的一个关注点。事实上,FTP 比 TCP/IP 还要早,^([17]),我们可以通过超过 50 个 RFC 来追踪这个协议的发展。经过 30 多年,FTP 既是一个悲伤的老东西,也是一个问题孩子——特别是对于那些试图将 FTP 和防火墙结合使用的人来说。FTP 是一个古老且奇怪的协议,存在很多值得反感的地方。以下是反对它的最常见几点:
-
密码以明文传输。^([18])
-
该协议要求至少使用两个 TCP 连接(控制和数据),并且需要使用不同的端口。
-
当会话建立时,数据通常通过随机选择的端口进行传输。
所有这些问题都会带来安全上的挑战,即便在考虑到客户端或服务器软件中的潜在弱点可能导致安全问题之前。正如任何网络老兵所说的,这些问题通常会在你最不需要的时候出现。
无论如何,存在其他更现代、更安全的文件传输选项,例如 SFTP 和 SCP,这些协议都通过加密连接进行身份验证和数据传输。称职的 IT 专业人员应该偏好使用除 FTP 之外的某种文件传输方式。
不论我们多么专业,也有时候不得不处理一些我们根本不想使用的东西。在通过防火墙使用 FTP 时,我们可以通过将流量重定向到一个专门为此目的编写的小程序来解决问题。对我们来说,处理 FTP 给了我们一个机会,去了解两个相对先进的 PF 特性:重定向和锚点。
在像我们这样默认阻止的情况下,处理 FTP 的最简单方法是让 PF 将该服务的流量重定向到一个外部应用程序,这个应用程序充当该服务的代理。代理维护自己的命名子规则集(在 PF 术语中称为锚点),在其中根据需要插入或删除 FTP 流量的规则。重定向和锚点的组合提供了一个干净、明确的接口,连接数据包过滤子系统和代理。
如果必须:带有 Divert 或 Redirect 的 ftp-proxy
通过你的网关启用 FTP 传输非常简单,这要归功于 OpenBSD 基础系统中包含的 FTP 代理程序。这个程序叫做——你猜对了——ftp-proxy。
要启用ftp-proxy,你需要在 OpenBSD 的/etc/rc.conf.local文件中添加这一行:
ftpproxy_flags=""
在 FreeBSD 中,/etc/rc.conf至少需要包含以下两行中的第一行:
ftpproxy_enable="YES"
ftpproxy_flags="" # and put any command line options here
如果你需要为ftp-proxy指定任何命令行选项,你可以将它们放入ftpproxy_flags变量中。
如果你愿意,你可以手动启动代理,运行/usr/sbin/ftp-proxy(或者更好的是,使用 OpenBSD 上的/etc/rc.d/ftp-proxy脚本并加上start选项),你可能想这样做,以检查你即将对 PF 配置所做的更改是否按预期生效。
对于基本配置,你只需要向/etc/pf.conf添加三个元素:锚点和两个pass规则。锚点声明看起来像这样:
anchor "ftp-proxy/*"
在 OpenBSD 4.7 之前的版本中,需要两个锚点声明:
nat-anchor "ftp-proxy/*"
rdr-anchor "ftp-proxy/*"
代理将在此插入它为 FTP 会话生成的规则。然后,你还需要一个pass规则,以允许 FTP 流量进入代理:
pass in quick inet proto tcp to port ftp divert-to 127.0.0.1 port 8021
注意divert-to部分。它将流量重定向到本地端口,在那里代理通过高效的、本地连接专用的 divert(4)接口监听流量。在 OpenBSD 4.9 及之前的版本中,流量重定向是通过rdr-to实现的。如果你正在升级一个旧的 OpenBSD 5.0 之前的配置,你需要更新 FTP 代理的rdr-to规则,将其改为使用divert-to。
如果你的操作系统使用的是 OpenBSD 4.7 之前的 PF 版本,你需要这个版本的重定向规则:
rdr pass on $int_if inet proto tcp from any to any port ftp -> 127.0.0.1 port 8021
最后,确保你的规则集包含一个pass规则,以允许数据包从代理传递到其他地方,在那里,$proxy扩展为代理守护进程绑定的地址:
pass out inet proto tcp from $proxy to any port ftp
重新加载你的 PF 配置:
$ **sudo pfctl -f /etc/pf.conf**
不久之后,你的用户会感谢你让 FTP 工作。
ftp-proxy 设置的变体
上面的示例涵盖了一个基本配置,其中你本地网络中的客户端需要联系其他地方的 FTP 服务器。这个配置应该与大多数 FTP 客户端和服务器的组合配合得很好。
你可以通过向ftpproxy_flags=行添加选项来改变代理的行为。你可能会遇到一些具有特定怪癖的客户端或服务器,你需要在配置中进行补偿,或者你可能希望以特定方式将代理集成到你的设置中,比如将 FTP 流量分配到特定的队列。对于这些以及其他ftp-proxy配置的细节,最好的办法是从研究手册页开始。
如果你有兴趣了解如何在 PF 和ftp-proxy保护下运行 FTP 服务器,你可以考虑在单独的端口上以反向模式(使用-R选项)运行单独的ftp-proxy,并为其设置自己的重定向pass规则。甚至可以将代理设置为在 IPv6 模式下运行,但如果你在运行现代协议方面走在前面,你可能不会再将 FTP 作为主要的文件传输协议。
注意
如果你的 PF 版本早于这里描述的版本,那么你正在使用一个过时且不受支持的操作系统。我强烈建议你尽快安排操作系统升级。如果由于某种原因升级不可行,请查阅本书的第一版并研究你操作系统的文档,了解如何使用一些早期的 FTP 代理。
使你的网络故障排除更友好
使你的网络故障排除更友好是一个可能相当庞大的话题。一般来说,你的 TCP/IP 网络的调试或故障排除友好性取决于你如何处理专门为调试设计的 Internet 协议:ICMP。
ICMP 是用于在主机和网关之间发送和接收控制消息的协议,主要是为了向发送方提供有关到达目标主机的过程中出现的任何异常或困难情况的反馈。
有大量的 ICMP 流量,通常在你浏览网页、阅读邮件或传输文件时会在后台发生。路由器(记住,你正在构建一个)使用 ICMP 来协商数据包大小和其他传输参数,这一过程通常被称为路径 MTU 发现。
你可能听过管理员称 ICMP 为“邪恶”或,如果他们的理解稍微深入一点,则称其为“必要的邪恶”。这种态度的原因纯粹是历史性的。几年前,人们发现一些操作系统的网络堆栈中包含代码,如果收到足够大的 ICMP 请求,机器可能会崩溃。
受此影响最严重的公司之一是微软,你可以通过使用你喜欢的搜索引擎找到大量关于死亡 ping漏洞的资料。然而,这一切发生在 1990 年代后半期,自那时以来,所有现代操作系统都彻底清理了它们的网络代码(至少,这是我们被告知的情况)。
早期的解决方法之一是直接阻止 ICMP 回显(ping)请求,甚至是所有 ICMP 流量。这种措施几乎肯定导致了性能差和难以调试的网络问题。然而,在某些地方,这些规则集已经存在了将近二十年,而且设置这些规则的人仍然很害怕。现在,几乎没有理由再担心有害的 ICMP 流量了,但我们还是会看看如何管理哪些 ICMP 流量能够进出你的网络。
在现代的 IPv6 网络中,更新后的 icmp6 协议在参数传递甚至主机配置中扮演着比以往更为关键的角色,网络管理员在学习如何屏蔽或允许 icmp6 流量时,实际上是在玩一场高风险的游戏。在很大程度上,IPv4 的 ICMP 问题对于 IPv6 的 ICMP6 也同样适用,但除了这些,ICMP6 还用于几个在 IPv4 中处理方式不同的机制。我们将在梳理适用于这两个 IP 协议版本的相关问题后,再深入这些问题。
我们是否让它全部通过?
显然的问题是:“如果 ICMP 是如此有用且重要的东西,我们是不是应该一直让它全部通过?”答案是,这取决于具体情况。
当然,让诊断流量不加限制地通过可以让调试变得更容易,但这也让其他人相对容易获取到关于你网络的信息。所以,如果你想隐藏你 IPv4 网络的内部工作原理,像下面这样的规则可能不是最优的:
pass inet proto icmp
如果你想让你的 IPv6 流量也有类似的自由信息流通,对应的规则是这样的:
pass inet6 proto icmp6
公正地说,还应该提到,你可能会发现一些 ICMP 和 ICMP6 流量无害地通过你的 keep state 规则悄悄进行。
最简单的解决办法:问题在这里解决
最简单的解决方案可能是允许本地网络的所有 ICMP 和 ICMP6 流量通过,同时让来自其他地方的探测在你的网关处停止:
pass inet proto icmp icmp-type $icmp_types from $localnet
pass inet6 proto icmp6 icmp6-type $icmp6_types from $localnet
pass inet proto icmp icmp-type $icmp_types to $ext_if
pass inet6 proto icmp6 icmp6-type $icmp6_types to $ext_if
当然,这是假设你已经识别了你希望通过的 ICMP 和 ICMP6 类型,并将它们填入你的宏定义中。稍后我们会回到这些内容。无论如何,停止网关的探测可能是一个有吸引力的选项,但让我们看看其他几种选项,这些选项能展示 PF 的灵活性。
让 ping 通过
到目前为止,我们在本章中开发的规则集有一个明显的缺点:常用的故障排除命令,如 ping 和 traceroute(以及它们的 IPv6 等价命令 ping6 和 traceroute6),将无法使用。这对于你的用户来说可能影响不大,而且正是 ping 命令最早让人们害怕 ICMP 流量,从而去过滤或阻止它,所以显然有些人认为没有它我们反而会更好。然而,你会发现这些故障排除工具非常有用。只需对规则集做几个小的调整,它们就能重新可用。
诊断命令ping和ping6依赖于 ICMP 和 ICMP6 的回显请求(以及匹配的回显回复)类型,为了保持规则集的整洁,我们首先定义另一个宏集:
icmp_types = "echoreq"
icmp6_types = "echoreq"
然后,我们添加使用这些定义的规则:
pass inet proto icmp icmp-type $icmp_types
pass inet6 proto icmp6 icmp6-type $icmp6_types
这些宏和规则意味着类型为回显请求的 ICMP 和 ICMP6 数据包将被允许通过,并且匹配的回显回复将被允许返回,因为 PF 具有状态性特征。这就是ping和ping6命令所需的一切,以产生它们预期的结果。
如果你需要更多或其他类型的 ICMP 或 ICMP6 数据包通过,你可以将icmp_types和icmp6_types扩展为你希望允许的那些数据包类型的列表。
帮助 traceroute
traceroute命令(以及 IPv6 版本traceroute6)在用户声称互联网无法连接时非常有用。默认情况下,Unix 的traceroute使用 UDP 连接,依据基于目标的固定公式。以下规则适用于所有我能访问的 Unix 版本中的traceroute和traceroute6命令,包括 GNU/Linux:
# allow out the default range for traceroute(8):
# "base+nhops*nqueries-1" (33434+64*3-1)
pass out on egress inet proto udp to port 33433:33626 # For IPv4
pass out on egress inet6 proto udp to port 33433:33626 # For IPv6
这也让你初步了解了端口范围的样子。在某些情况下它们非常有用。
迄今为止的经验表明,其他操作系统上的traceroute和traceroute6实现方式大致相同。一个显著的例外是微软 Windows 平台。在该平台上,tracert.exe程序及其 IPv6 版本tracert6.exe使用 ICMP 回显请求来实现此目的。因此,如果你希望让 Windows 的 traceroute 通过,你只需要第一个规则,就像允许ping通过一样。Unix 的traceroute程序也可以被指示使用其他协议,并且如果你使用其-I命令行选项,它将表现得与微软的对应程序非常相似。你可以查阅traceroute手册页(或者它的源代码)来获取所有详细信息。
这个解决方案基于我在openbsd-misc帖子中找到的一个示例规则。我发现了这个列表,并且可以在* marc.info/*等地方访问的可搜索的列表存档,是你需要 OpenBSD 或 PF 相关信息时一个有价值的资源。
路径 MTU 发现
在故障排除时我最后要提醒你的一点是路径 MTU 发现。互联网协议被设计为设备无关的,而设备无关的一个后果是你无法总是可靠地预测给定连接的最佳数据包大小。数据包大小的主要限制称为最大传输单元,或MTU,它设置了接口数据包大小的上限。ifconfig命令将显示你的网络接口的 MTU。
现代 TCP/IP 实现期望通过一个过程来确定连接的正确数据包大小,该过程仅涉及在本地链路的 MTU 范围内发送大小不同的数据包,并设置“不要分片”标志。如果数据包在到达目的地的过程中超出了某处的 MTU,具有较低 MTU 的主机将返回一个 ICMP 数据包,指示“类型 3,代码 4”,并在本地上限已达到时引用其本地 MTU。现在,你不需要马上去查 RFC。类型 3 表示目的地不可达,代码 4 表示需要分片,但“不要分片”标志已设置。因此,如果你与其他网络的连接(它们的 MTU 可能与你的不同)似乎表现不佳,你可以尝试稍微更改你的 ICMP 类型列表,允许 IPv4 目的地不可达数据包通过:
icmp_types = "{ echoreq, unreach }"
如你所见,这意味着你不需要修改pass规则本身:
pass inet proto icmp icmp-type $icmp_types
现在,我要告诉你一个小秘密:在几乎所有情况下,这些规则对于路径 MTU 发现来说并不是必须的(但它们也无妨)。然而,尽管 PF 的默认keep state行为可以处理大部分你需要的 ICMP 流量,PF 确实允许你过滤所有 ICMP 类型和代码的变体。对于 IPv6,你可能希望让更常见的 ICMP6 诊断信息通过,例如以下内容:
icmp6_types = "{ echoreq unreach timex paramprob }"
这意味着我们允许回显请求、目的地不可达、超时以及参数问题消息通过 IPv6 流量。感谢宏定义,你也无需更改 ICMP6 情况的pass规则:
pass inet6 proto icmp6 icmp6-type $icmp6_types
但值得记住的是,IPv6 主机依赖 ICMP6 消息来执行自动配置相关任务,你可能需要显式地过滤,以便在网络中的不同位置允许或拒绝特定的 ICMP6 类型。
例如,你可能希望让路由器及其客户端交换路由请求和路由通告消息(分别为 ICMP6 类型routeradv和routersol),同时,你可能希望确保邻居通告和邻居请求(分别为 ICMP6 类型neighbradv和neighbrsol)仅限于它们直接连接的网络内。
如果你想深入了解更多细节,可能的类型和代码列表已经在icmp(4)和icmp6(4)手册页中记录。背景信息可以在 RFC 文档中找到。^([19])
表格让你的生活更轻松
到目前为止,你可能会觉得这个设置显得非常静态和僵化。毕竟,某些时间点可能会有一些与过滤和重定向相关的数据,但它们不值得被写入配置文件!说得对,PF 提供了处理这些情况的机制。
表格就是这样的一个功能。它们作为 IP 地址列表非常有用,可以在不重新加载整个规则集的情况下进行操作,并且在需要快速查找时也非常实用。
表格名称总是用< >括起来,像这样:
table <clients> persist { 192.168.2.0/24, !192.168.2.5 }
这里,网络192.168.2.0/24是表格的一部分,唯一的例外是:使用!操作符(逻辑非)排除了地址192.168.2.5。关键字persist确保即使当前没有规则引用该表格,表格本身仍然存在。
还可以从文件加载表格,其中每个项位于单独的一行,例如文件/etc/clients:
192.168.2.0/24
!192.168.2.5
这反过来用于初始化/etc/pf.conf中的表格:
table <clients> persist file "/etc/clients"
因此,例如,您可以将我们早些时候的规则更改为如下所示,以管理来自客户端计算机的出站流量:
pass inet proto tcp from <clients> to any port $client_out
有了这个,您可以像这样实时操作表格的内容:
$ **sudo pfctl -t clients -T add 192.168.1/16**
请注意,这只会改变表格的内存副本,这意味着除非您安排存储更改,否则该更改在断电或重启后将不会保存。
您也可以选择通过cron作业维护表格的磁盘副本,该作业定期将表格内容转储到磁盘,使用如下命令:
$ **sudo pfctl -t clients -T show >/etc/clients**
或者,您可以编辑/etc/clients文件,并使用该文件数据替换内存中的表格内容:
$ **sudo pfctl -t clients -T replace -f /etc/clients**
对于您将频繁执行的操作,迟早您会编写 shell 脚本。很可能,表格的常规操作,例如插入或删除项或替换表格内容,将成为您不久后用于维护的脚本的一部分。
一个常见的例子是通过cron作业在特定时间替换作为from地址引用的表格内容,从而强制实施网络访问限制。在某些网络中,您甚至可能需要为一周中的不同天设置不同的访问规则。唯一的真正限制在于您自身的需求和创造力。
在接下来的章节中,我们将频繁回到表格的一些便捷用途,并将查看几个与表格交互的有用程序。
^([13]) 实际上,keep state部分表示默认行为,如果您使用的是 OpenBSD 4.1 或更高版本的 PF,通常是多余的。然而,通常不需要在从早期版本升级时删除现有规则中的此规范。为了简化过渡,本书中的示例在需要时会做出此区分。
^([14]) RFC 1631,“IP 网络地址转换器(NAT)”,1994 年 5 月发布,以及 RFC 1918,“私人互联网地址分配”,1996 年 2 月发布,提供了有关 NAT 的详细信息。
^([15]) 引用项目主页上的内容,* www.kame.net/*, “KAME 项目是日本六家公司联合发起的,旨在为 BSD 变种提供免费的 IPv6、IPsec 和移动 IPv6 栈。”主要的研发活动已于 2006 年 3 月完成,重要部分已经纳入相关系统,现在只剩下维护活动。
^([16]) 除非信息在此期间发生了变化,否则这一点是正确的。当然,有些系统管理员喜欢恶作剧,但大多数情况下,DNS 区域信息的变化是由于该特定组织或网络的实际需求。
^([17]) 描述 FTP 协议的最早 RFC 是 RFC 114,日期为 1971 年 4 月 10 日。随着 FTP 版本 5 的推出,切换到 TCP/IP 协议,该版本定义在 RFC 765 和 775 中,分别发布于 1980 年 6 月和 12 月。
^([18]) 一种加密版本的协议,称为 FTPS,在 RFC4217 中有详细说明,但其支持仍然有些不稳定。
^([19]) 描述 ICMP 及相关技术的主要 RFC 包括 792、950、1191、1256、2521 和 6145。IPv6 的 ICMP 更新在 RFC 3542 和 RFC 4443 中。这些文档可以在互联网上的多个地方找到,例如 www.ietf.org/ 和 www.faqs.org/,并可能也可以通过你的包管理系统访问。
第四章 无线网络简易设置

很容易说,在 BSD 系统,尤其是 OpenBSD 中,不需要“简化无线网络设置”,因为它本身就已经很简单。让无线网络运行并不比让有线网络运行更复杂,但确实有一些问题是因为我们处理的是无线电波而不是电缆。我们将在继续讲解创建可用设置的实用步骤之前,简要探讨一些问题。
一旦我们掌握了设置无线网络的基本方法,我们将转向一些选项,使你的无线网络更加有趣且更难以被破解。
IEEE 802.11 背景介绍
设置任何网络接口,原则上是一个两步过程:首先建立连接,然后配置接口以支持 TCP/IP 流量。
对于有线以太网类型的接口,建立连接通常只是插入电缆并看到连接指示灯亮起。然而,一些接口需要额外的步骤。例如,拨号连接的网络设置需要电话步骤,比如拨打一个号码以获取载波信号。
对于 IEEE 802.11 风格的无线网络,获取载波信号涉及底层的几个步骤。首先,你需要在分配的频谱中选择正确的频道。一旦找到信号,你需要设置一些链路级网络识别参数。最后,如果你想连接的站点使用某种链路级加密,你需要设置正确的加密类型,并可能需要协商一些额外的参数。
幸运的是,在 OpenBSD 系统上,所有无线网络设备的配置都通过 ifconfig 命令和选项进行,就像配置任何其他网络接口一样。虽然其他 BSD 系统也通过 ifconfig 配置大多数网络设置,但在一些系统上,特定功能可能需要其他配置。^([20])不过,由于我们在这里介绍无线网络,所以我们需要从这个新视角来审视网络堆栈中各个层次的安全性。
基本上,有三种流行且简单的 IEEE 802.11 隐私机制,我们将在接下来的章节中简要讨论它们。
注意
有关无线网络安全问题的更全面概述,请参见 Kjell Jørgen Hole 教授的文章和幻灯片,网址为 www.kjhole.com/ 和 www.nowires.org/.
MAC 地址过滤
关于 PF 和 MAC 地址过滤的简短版本是,我们不使用它。许多消费级的现成无线接入点提供 MAC 地址过滤,但与普遍看法相反,它们并没有真正增加太多安全性。其营销成功主要是因为大多数消费者不知道,今天市场上几乎所有无线网络适配器的 MAC 地址都是可以更改的。^([21])
注意
如果你真的想尝试 MAC 地址过滤,可以查阅 OpenBSD 4.7 及以后的版本中,使用bridge(4)功能和ifconfig(8)中的桥接相关规则选项。我们将在第五章中探讨桥接及其与包过滤结合的更多实用方法。请注意,你可以仅通过将一个接口添加到桥接中,而不实际运行桥接,就使用桥接过滤。
WEP
使用无线电波而不是电缆传输数据的一个后果是,外部人员相对更容易捕获通过无线电波传输的数据。802.11 系列无线网络标准的设计者似乎意识到了这一点,他们提出了一个解决方案,并将其推向市场,命名为有线等效隐私,或称WEP。
不幸的是,WEP 的设计者在设计有线等效加密时,并没有深入研究最新的研究成果或咨询该领域的活跃研究人员。因此,他们推荐的链路级加密方案被密码学专业人士认为是相当原始的自制产品。当 WEP 加密在首批产品发布几个月后被逆向工程并破解时,大家并不感到惊讶。
尽管你可以免费下载工具,在几分钟内破解 WEP 编码的流量,但由于多种原因,WEP 仍然被广泛支持和使用。本质上,今天所有可用的 IEEE 802.11 设备至少支持 WEP,而且惊人的是,许多设备还提供 MAC 地址过滤功能。
你应该将仅由 WEP 保护的网络流量视为比公开广播的数据稍微安全一点。不过,破解 WEP 网络所需的微小努力,可能足以吓退那些懒惰且技术水平低的攻击者。
WPA
802.11 的设计者们很快意识到,他们的 WEP 系统并不像宣传的那样强大,因此他们提出了一个修订版且稍微更全面的解决方案,称为Wi-Fi 保护访问,或称WPA。
WPA 在纸面上看起来比 WEP 要好,但由于规范复杂,其广泛实施被推迟了。此外,WPA 因设计问题和存在的一些漏洞,偶尔会产生互操作性问题,受到了批评。再加上访问文档和硬件的常见问题,免费软件的支持程度各不相同。大多数免费系统都支持 WPA,尽管你可能会发现并非所有设备都支持,但随着时间推移,情况有所改善。如果你的项目规范包括 WPA,务必仔细查看你的操作系统和驱动文档。
当然,几乎不言而喻,为了保持数据流的机密性,你需要进一步的安全措施,如 SSH 或 SSL 加密。
任务所需的正确硬件
选择合适的硬件不一定是个艰巨的任务。在 BSD 系统中,以下简单命令即可查看所有包含wireless关键词的手册页面列表。^([22])
$ **apropos wireless**
即使在刚安装的系统上,这个命令也会给出操作系统中所有可用的无线网络驱动程序的完整列表。
下一步是阅读驱动手册页面,并将兼容设备列表与可用的硬件部分或你正在考虑的系统中内置的设备进行对比。花些时间思考你具体的需求。作为测试用途,低端的rum或ural USB 加密狗(或更新的urtwn和run)会非常有效,并且非常方便。稍后,当你准备构建一个更永久的基础设施时,可能会考虑更高端的设备,尽管你可能会发现便宜的测试设备表现得相当好。一些无线芯片组需要固件,由于法律原因,这些固件不能包含在 OpenBSD 安装介质中。在大多数情况下,只要网络连接可用,fw_update脚本在首次启动时会自动获取所需的固件。如果你在一个已经配置好的系统中安装这些设备,可以尝试手动运行fw_update。你还可能想阅读本书的附录 B,以获得更多讨论。
设置一个简单的无线网络
对于我们的第一个无线网络,使用上一章的基本网关配置作为起点是有意义的。在你的网络设计中,无线网络很可能并不直接连接到互联网,而是需要某种形式的网关。因此,重新使用已有的工作网关设置来配置这个无线接入点是合理的,接下来的几段会对其进行一些小的修改。毕竟,这比从头开始配置更方便。
注意
我们现在处于基础设施建设模式,首先设置接入点。如果您更喜欢先查看客户端设置,请参见客户端部分。
第一步是确保您拥有一个受支持的卡,并检查驱动程序是否加载并正确初始化该卡。启动时系统消息会在控制台上滚动显示,但它们也会记录在文件/var/run/dmesg.boot中。您可以查看该文件本身,或者使用dmesg命令查看这些消息。成功配置的 PCI 卡应该会显示类似如下内容:
ral0 at pci1 dev 10 function 0 "Ralink RT2561S" rev 0x00: apic 2 int 11 (irq
11), address 00:25:9c:72:cf:60
ral0: MAC/BBP RT2561C, RF RT2527
如果您要配置的接口是热插拔类型,例如 USB 或 PC 卡设备,您可以通过查看/var/log/messages文件来查看内核消息——例如,在插入设备之前,您可以运行tail -f命令查看该文件。
接下来,您需要配置接口:首先启用链路,最后配置系统以支持 TCP/IP。您可以通过命令行完成此操作,方法如下:
$ **sudo ifconfig ral0 up mediaopt hostap mode 11g chan 1 nwid unwiredbsd nwkey 0x1deadbeef9**
该命令一次完成多项任务。它配置了ral0接口,通过up参数启用接口,并指定该接口是无线网络的接入点,使用mediaopt hostap。然后,明确设置操作模式为11g,频道为11。最后,使用nwid参数将网络名称设置为unwiredbsd,WEP 密钥(nwkey)设置为十六进制字符串0x1deadbeef9。
使用ifconfig检查命令是否成功配置了接口:
**$ ifconfig ral0**
ral0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:25:9c:72:cf:60
priority: 4
groups: wlan
media: IEEE802.11 autoselect mode 11g hostap
status: active
ieee80211: nwid unwiredbsd chan 1 bssid 00:25:9c:72:cf:60 nwkey <not displayed> 100dBm
inet6 fe80::225:9cff:fe72:cf60%ral0 prefixlen 64 scopeid 0x2
请注意media和ieee80211行的内容。这里显示的信息应与您在ifconfig命令行中输入的内容匹配。
在无线网络的链路部分正常工作后,您可以为接口分配一个 IP 地址。首先,设置一个 IPv4 地址:
$ **sudo ifconfig ral0 10.50.90.1 255.255.255.0**
设置 IPv6 也同样简单:
$ **sudo ifconfig alias ral0 2001:db8::baad:f00d:1 64**
在 OpenBSD 上,您可以通过创建一个/etc/hostname.ral0文件将这两个步骤合并为一个,大致如下:
up mediaopt hostap mode 11g chan 1 nwid unwiredbsd nwkey 0x1deadbeef9
inet6 alias 2001:db8::baad:f00d:1 64
然后,以sh /etc/netstart ral0(作为 root 用户)运行,或者耐心等待下次启动完成。
请注意,前面的配置分为几行。第一行生成一个ifconfig命令,用正确的参数为物理无线网络设置接口。第二行生成命令,在第一条命令完成后设置 IPv4 地址,然后为双栈配置设置 IPv6 地址。因为这是我们的接入点,所以我们显式设置频道,并通过设置nwkey参数启用弱 WEP 加密。
在 NetBSD 上,通常可以将所有这些参数合并为一个rc.conf设置:
ifconfig_ral0="mediaopt hostap mode 11g chan 1 nwid unwiredbsd nwkey
0x1deadbeef inet 10.50.90.1 netmask 255.255.255.0 inet6 2001:db8::baad:f00d:1
prefixlen 64 alias"
FreeBSD 8 及更高版本采用稍有不同的方法,将无线网络设备绑定到统一的wlan(4)驱动程序。根据您的内核配置,您可能需要将相关的模块加载行添加到/boot/loader.conf中。在我的一个测试系统中,/boot/loader.conf看起来是这样的:
if_rum_load="YES"
wlan_scan_ap_load="YES"
wlan_scan_sta_load="YES"
wlan_wep_load="YES"
wlan_ccmp_load="YES"
wlan_tkip_load="YES"
在加载相关模块后,设置是一个多命令的过程,最好通过一个适用于你的无线网络的start_if.if文件来处理。下面是一个 FreeBSD 8 中用于 WEP 接入点的/etc/start_if.rum0文件示例:
wlans_rum0="wlan0"
create_args_wlan0="wlandev rum0 wlanmode hostap"
ifconfig_wlan0="inet 10.50.90.1 netmask 255.255.255.0 ssid unwiredbsd \
wepmode on wepkey 0x1deadbeef9 mode 11g"
ifconfig_wlan0_ipv6="2001:db8::baad:f00d:1 prefixlen 64"
配置成功后,ifconfig的输出应该显示物理接口和wlan接口都已启动并正在运行:
rum0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 2290
ether 00:24:1d:9a:bf:67
media: IEEE 802.11 Wireless Ethernet autoselect mode 11g <hostap>
status: running
wlan0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
ether 00:24:1d:9a:bf:67
inet 10.50.90.1 netmask 0xffffff00 broadcast 10.50.90.255
inet6 2001:db8::baad:f00d:1 prefixlen 64
media: IEEE 802.11 Wireless Ethernet autoselect mode 11g <hostap>
status: running
ssid unwiredbsd channel 6 (2437 Mhz 11g) bssid 00:24:1d:9a:bf:67
country US authmode OPEN privacy ON deftxkey UNDEF wepkey 1:40-bit
txpower 0 scanvalid 60 protmode CTS dtimperiod 1 -dfs
status: running这一行意味着你已经至少在链路层面上启动并运行。
注意
务必查看最新的ifconfig手册页,以获取可能更适合你配置的其他选项。
一个 OpenBSD WPA 接入点
WPA 支持在 OpenBSD 4.4 中引入,并扩展到大多数无线网络驱动程序,所有基本的 WPA 密钥管理功能在 OpenBSD 4.9 中合并到ifconfig(8)中。
注意
可能仍然有不支持 WPA 的无线网络驱动程序,所以在尝试配置网络以使用 WPA 之前,请检查驱动程序的手册页,看看是否支持 WPA。你可以通过security/wpa_supplicant包将 802.1x 密钥管理与外部认证服务器结合使用“企业”模式,但为了简便起见,我们将使用共享密钥设置。*
设置带有 WPA 的接入点的过程与我们为 WEP 配置时类似。对于带有预共享密钥的 WPA 设置(有时称为网络密码),通常会写一个类似这样的hostname.if文件:
up media autoselect mediaopt hostap mode 11g chan 1 nwid unwiredbsd wpakey 0x1deadbeef9
inet6 alias 2001:db8::baad:f00d:1 64
如果你已经在运行前面描述的 WEP 设置,可以通过以下命令禁用这些设置:
$ sudo ifconfig ral0 -nwid -nwkey
然后,使用以下命令启用新的设置:
$ sudo sh /etc/netstart ral0
你可以使用ifconfig检查接入点是否已启动并运行:
$ ifconfig ral0
ral0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:25:9c:72:cf:60
priority: 4
groups: wlan
media: IEEE802.11 autoselect mode 11g hostap
status: active
ieee80211: nwid unwiredbsd chan 1 bssid 00:25:9c:72:cf:60 wpapsk <not displayed>
wpaprotos wpa1,wpa2 wpaakms psk wpaciphers tkip,ccmp wpagroupcipher tkip 100dBm
inet6 fe80::225:9cff:fe72:cf60%ral0 prefixlen 64 scopeid 0x2
inet6 2001:db8::baad:f00d:1 prefixlen 64
inet 10.50.90.1 netmask 0xff000000 broadcast 10.255.255.255
请注意status: active的指示,以及我们没有明确设置的 WPA 选项,它们显示了合理的默认值。
一个 FreeBSD WPA 接入点
从我们之前配置的 WEP 接入点转到稍微安全一点的 WPA 设置是很简单的。FreeBSD 上的 WPA 支持通过hostapd(一个与 OpenBSD 的hostapd类似但不完全相同的程序)来实现。我们首先编辑/etc/start_if.rum0文件,以移除认证信息。编辑后的文件应该像这样:
wlans_rum0="wlan0"
create_args_wlan0="wlandev rum0 wlanmode hostap"
ifconfig_wlan0="inet 10.50.90.1 netmask 255.255.255.0 ssid unwiredbsd mode 11g"
ifconfig_wlan0_ipv6="2001:db8::baad:f00d:1 prefixlen 64"
接下来,我们在/etc/rc.conf中添加启用hostapd的行:
hostapd_enable="YES"
最后,hostapd需要在/etc/hostapd.conf中进行一些配置:
interface=wlan0
debug=1
ctrl_interface=/var/run/hostapd
ctrl_interface_group=wheel
ssid=unwiredbsd
wpa=1
wpa_passphrase=0x1deadbeef9
wpa_key_mgmt=WPA-PSK
wpa_pairwise=CCMP TKIP
这里,接口规范比较直观,而debug值被设置为产生最少的消息。范围是0到4,其中0表示没有调试消息。除非你正在开发hostapd,否则不需要更改ctrl_interface*设置。接下来的五行中的第一行设置了网络标识符。后续的行启用了 WPA 并设置了密码。最后两行指定了接受的密钥管理算法和加密方案。(有关详细信息和更新,请参阅hostapd(8)和hostapd.conf(5)的手册页。)
在成功配置后(运行 sudo /etc/rc.d/hostapd force-start),ifconfig 应该会显示类似于以下的关于两个接口的输出:
rum0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 2290
ether 00:24:1d:9a:bf:67
media: IEEE 802.11 Wireless Ethernet autoselect mode 11g <hostap>
status: running
wlan0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
ether 00:24:1d:9a:bf:67
inet 10.50.90.1 netmask 0xffffff00 broadcast 10.50.90.255
inet6 2001:db8::baad:f00d:1 prefixlen 64
media: IEEE 802.11 Wireless Ethernet autoselect mode 11g <hostap>
status: running
ssid unwiredbsd channel 6 (2437 Mhz 11g) bssid 00:24:1d:9a:bf:67
country US authmode WPA privacy MIXED deftxkey 2 TKIP 2:128-bit
txpower 0 scanvalid 60 protmode CTS dtimperiod 1 -dfs
status: running 这一行意味着你已经成功启动,至少在链路层级上已经运行。
接入点的 PF 规则集
配置好接口后,接下来是将接入点配置为数据包过滤网关。你可以从第三章复制基础网关设置开始。通过在接入点的 sysctl.conf 或 rc.conf 文件中进行适当的条目设置来启用网关功能,然后复制 pf.conf 文件。根据上一章中对你最有用的部分,pf.conf 文件可能看起来像这样:
ext_if = "re0" # macro for external interface - use tun0 or pppoe0 for PPPoE
int_if = "re1" # macro for internal interface
localnet = $int_if:network
# nat_address = 203.0.113.5 # Set addess for nat-to
client_out = "{ ssh, domain, pop3, auth, nntp, http,\
https, cvspserver, 2628, 5999, 8000, 8080 }"
udp_services = "{ domain, ntp }"
icmp_types = "{ echoreq, unreach }"
# if IPv6, some ICMP6 accommodation is needed
icmp6_types = "{ echoreq unreach timex paramprob }"
# If ext_if IPv4 address is dynamic, ($ext_if) otherwise nat to specific address, ie
# match out on $ext_if inet from $localnet nat-to $nat_address
match out on $ext_if inet from $localnet nat-to ($ext_if)
block all
pass quick inet proto { tcp, udp } from $localnet to port $udp_services
pass log inet proto icmp icmp-type $icmp_types
pass inet6 proto icmp6 icmp6-type $icmp6_types
pass inet proto tcp from $localnet port $client_out
如果你使用的是 OpenBSD 4.6 或更早版本的 PF,match 规则中的 nat-to 将变成如下(假设外部接口只有一个地址,并且是动态分配的):
nat on $ext_if from $localnet to any -> ($ext_if)
让接入点正常工作所需的唯一差异是 int_if 的定义。你必须将 int_if 的定义修改为匹配无线接口。在我们的例子中,这意味着这一行应该改为如下所示:
int_if = "ral0" # macro for internal interface
很可能,你还希望设置 dhcpd 来为与接入点关联后的 IPv4 客户端分配地址和其他相关的网络信息。对于 IPv6 网络,你可能需要设置 rtadvd(甚至是 DHCP6 守护进程)来帮助你的 IPv6 客户端进行自动配置。如果你阅读手册页,设置 dhcpd 和 rtadvd 是相当直接的。
就是这样。这个配置为你提供了一个功能性的 BSD 接入点,至少通过 WEP 加密或稍微强一些的链路层加密(如 WPA)提供了基本的安全性(实际上更像是一个 禁止进入! 的标志)。如果你需要支持 FTP,从你在第三章中设置的机器复制 ftp-proxy 配置,并进行类似你为其余规则集所做的更改。
三个或更多接口的接入点
如果你的网络设计要求接入点同时是有线局域网或多个无线网络的网关,你需要对规则集做一些小的调整。除了修改 int_if 宏的值外,你可能还需要为无线接口添加另一个(描述性)定义,例如如下所示:
air_if = "ral0"
你的无线接口很可能位于不同的子网中,因此为每个接口单独设置一个规则来处理任何 IPv4 NAT 配置可能会很有用。以下是 OpenBSD 4.7 及更新系统的示例:
match out on $ext_if from $air_if:network nat-to ($ext_if)
这里是关于 OpenBSD 4.7 之前的 PF 版本的内容:
nat on $ext_if from $air_if:network to any -> ($ext_if) static-port
根据你的策略,你可能还需要调整localnet定义,或者至少在适当的位置将$air_if包含在你的pass规则中。再一次,如果你需要支持 FTP,可能需要为无线网络设置一个单独的 pass 规则,并将流量重定向或转发到ftp-proxy。
处理 IPSec,VPN 解决方案
你可以使用内置的 IPsec 工具、OpenSSH 或其他工具来设置虚拟专用网络(VPN)。然而,由于无线网络的安全性普遍较差,或者出于其他原因,你可能希望设置一些额外的安全措施。
选项大致分为三类:
-
SSH。如果你的 VPN 基于 SSH 隧道,基线规则集已经包含了你所需的所有过滤。你的隧道流量对于数据包过滤器来说将与其他 SSH 流量无异。
-
带 UDP 密钥交换的 IPsec(IKE/ISAKMP)。几种 IPsec 变种严重依赖通过
proto udp port 500的密钥交换,并使用proto udp port 4500进行NAT 穿越(NAT-T)。你需要允许这些流量通过,以使流建立。几乎所有实现也都严重依赖允许 ESP 协议流量(协议号 50)在主机之间传递,配置如下:pass proto esp from $source to $target -
基于 IPsec 封装接口的过滤。通过正确配置的 IPsec 设置,你可以设置 PF 以在封装接口
enc0本身上进行过滤,使用如下内容:^([23])pass on enc0 proto ipencap from $source to $target keep state (if-bound)
请参见附录 A,其中包含一些关于该主题的有用文献。
客户端侧
只要你有 BSD 客户端,设置就非常简单。将 BSD 机器连接到无线网络的步骤与我们刚才设置无线接入点的步骤非常相似。在 OpenBSD 中,配置重点在于无线接口的hostname.if文件。在 FreeBSD 中,配置重点在于rc.conf,但根据具体配置,可能还需要涉及其他一些文件。
OpenBSD 设置
以 OpenBSD 为例,为了连接到我们刚才配置的 WEP 接入点,你的 OpenBSD 客户端需要一个hostname.if(例如,/etc/hostname.ral0)配置文件,其中包含以下内容:
up media autoselect mode 11g chan 1 nwid unwiredbsd nwkey 0x1deadbeef9
dhcp
rtsol
第一行比通常需要的更详细地设置了链路层参数。只有up、nwid和nwkey参数是严格必要的。在几乎所有情况下,驱动程序会在适当的频道和最佳可用模式下与接入点关联。第二行要求进行 DHCP 配置,实际上会导致系统运行dhclient命令以获取 TCP/IP 配置信息。最后一行调用rtsol(8)来启动 IPv6 配置。
如果你选择 WPA 配置,文件看起来会是这样的:
up media autoselect mode 11g chan 1 nwid unwiredbsd wpakey 0x1deadbeef9
dhcp
rtsol
同样,第一行设置链路级参数,其中最关键的参数是网络选择和加密参数nwid和wpakey。你可以尝试省略mode和chan参数;在几乎所有情况下,驱动程序都会在适当的频道和最佳模式下与接入点关联。
如果你希望在将配置写入/etc/hostname.if文件之前先在命令行尝试配置命令,设置 WEP 网络客户端的命令如下:
$ sudo ifconfig ral0 up mode 11g chan 1 nwid unwiredbsd nwkey 0x1deadbeef9
ifconfig命令应该没有任何输出。然后你可以使用ifconfig检查接口是否已成功配置。输出应该类似于以下内容:
$ ifconfig ral0
ral0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:25:9c:72:cf:60
priority: 4
groups: wlan
media: IEEE802.11 autoselect (OFDM54 mode 11g)
status: active
ieee80211: nwid unwiredbsd chan 1 bssid 00:25:9c:72:cf:60 nwkey <not displayed> 100dBm
inet6 fe80::225:9cff:fe72:cf60%ral0 prefixlen 64 scopeid 0x2
注意,ieee80211:行显示了网络名称和频道,以及其他一些参数。这里显示的信息应该与你在ifconfig命令行中输入的内容匹配。
这是配置你的 OpenBSD 客户端连接 WPA 网络的命令:
$ sudo ifconfig ral0 nwid unwiredbsd wpakey 0x1deadbeef9
命令应该在没有任何输出的情况下完成。如果你再次使用ifconfig检查接口状态,输出应该类似于以下内容:
$ ifconfig ral0
ral0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:25:9c:72:cf:60
priority: 4
groups: wlan
media: IEEE802.11 autoselect (OFDM54 mode 11g)
status: active
ieee80211: nwid unwiredbsd chan 1 bssid 00:25:9c:72:cf:60 wpapsk <not
displayed> wpaprotos wpa1,wpa2 wpaakms psk wpaciphers tkip,ccmp wpagroupcipher
tkip 100dBm
inet6 fe80::225:9cff:fe72:cf60%ral0 prefixlen 64 scopeid 0x2
检查ieee80211:行是否显示正确的网络名称和合理的 WPA 参数。
一旦确认接口在链路层配置完毕,可以使用dhclient命令配置接口以支持 TCP/IP,如下所示:
$ sudo dhclient ral0
dhclient命令应该打印出与 DHCP 服务器的对话摘要,类似于以下内容:
DHCPREQUEST on ral0 to 255.255.255.255 port 67
DHCPREQUEST on ral0 to 255.255.255.255 port 67
DHCPACK from 10.50.90.1 (00:25:9c:72:cf:60)
bound to 10.50.90.11 -- renewal in 1800 seconds.
要初始化接口以支持 IPv6,请输入以下命令:
$ **sudo rtsol ral0**
rtsol命令通常会在没有任何消息的情况下完成。使用ifconfig检查接口配置,确认接口确实已接收到 IPv6 配置。
FreeBSD 设置
在 FreeBSD 上,可能需要做比 OpenBSD 更多的配置工作。根据你的内核配置,可能需要将相关的模块加载行添加到/boot/loader.conf文件中。在我的一台测试系统中,/boot/loader.conf文件内容如下:
if_rum_load="YES"
wlan_scan_ap_load="YES"
wlan_scan_sta_load="YES"
wlan_wep_load="YES"
wlan_ccmp_load="YES"
wlan_tkip_load="YES"
在加载了相关模块后,你可以通过执行以下命令加入我们之前配置的 WEP 网络:
$ sudo ifconfig wlan create wlandev rum0 ssid unwiredbsd wepmode on wepkey 0x1deadbeef9 up
然后,执行此命令以获取接口的 IPv4 配置:
$ sudo dhclient wlan0
要初始化接口以支持 IPv6,请输入以下命令:
$ **sudo rtsol ral0**
rtsol命令通常会在没有任何消息的情况下完成。使用ifconfig检查接口配置,确认接口确实已接收到 IPv6 配置。
为了更永久的配置,创建一个start_if.rum0文件(如果物理接口名称不同,请将rum0替换为实际名称),文件内容如下:
wlans_rum0="wlan0"
create_args_wlan0="wlandev rum0 ssid unwiredbsd wepmode on wepkey 0x1deadbeef9 up"
ifconfig_wlan0="DHCP"
ifconfig_wlan0_ipv6="inet6 accept_rtadv"
如果你想加入 WPA 网络,需要设置wpa_supplicant并稍微更改网络接口设置。对于 WPA 接入点,使用以下配置连接,添加到你的start_if.rum0文件中:
wlans_rum0="wlan0"
create_args_wlan0="wlandev rum0"
ifconfig_wlan0="WPA"
你还需要一个包含以下内容的/etc/wpa_supplicant.conf文件:
network={
ssid="unwiredbsd"
psk="0x1deadbeef9"
}
最后,在 rc.conf 中添加第二行 ifconfig_wlan0,以确保 dhclient 正常运行。
ifconfig_wlan0="DHCP"
对于 IPv6 配置,将以下内容添加到 rc.conf 中:
ifconfig_wlan0_ipv6="inet6 accept_rtadv"
其他 WPA 网络可能需要额外的选项。在成功配置后,ifconfig 的输出应该类似于这样:
rum0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 2290
ether 00:24:1d:9a:bf:67
media: IEEE 802.11 Wireless Ethernet autoselect mode 11g
status: associated
wlan0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
ether 00:24:1d:9a:bf:67
inet 10.50.90.16 netmask 0xffffff00 broadcast 10.50.90.255
inet6 2001:db8::baad:f00d:1635 prefixlen 64
media: IEEE 802.11 Wireless Ethernet OFDM/36Mbps mode 11g
status: associated
ssid unwiredbsd channel 1 (2412 Mhz 11g) bssid 00:25:9c:72:cf:60
country US authmode WPA2/802.11i privacy ON deftxkey UNDEF
TKIP 2:128-bit txpower 0 bmiss 7 scanvalid 450 bgscan bgscanintvl 300
bgscanidle 250 roam:rssi 7 roam:rate 5 protmode CTS roaming MANUAL
使用 authpf 保护你的无线网络
安全专家普遍认为,尽管 WEP 加密提供的保护非常有限,但它勉强足够向潜在攻击者表明你不打算让任何人都能使用你的网络资源。使用 WPA 能显著提高安全性,代价是需要一些复杂的配置,特别是在需要“企业级”选项的场景下。
到目前为止,在本章中我们构建的配置是有效的。无论是 WEP 还是 WPA 配置,都会让所有合理配置的无线客户端连接,这本身可能就是一个问题,因为这种配置没有内建的真实支持来决定谁可以使用你的网络。
如前所述,MAC 地址过滤并不能真正有效地防御攻击者,因为更改 MAC 地址实在太简单了。当 Open-BSD 开发者在 OpenBSD 3.1 版本中引入 authpf 时,他们选择了一个截然不同的方法来解决这个问题。与其将访问权限绑定到硬件标识符(如网络卡的 MAC 地址),他们决定使用已经存在的强大且灵活的用户身份验证机制来处理此问题。用户外壳 authpf 允许系统根据每个用户来加载 PF 规则,实际上决定了哪个用户可以做什么。
要使用 authpf,你需要创建用户,并将 authpf 程序作为他们的 shell。为了获得网络访问权限,用户通过 SSH 登录到网关。一旦用户成功完成 SSH 身份验证,authpf 会加载你为该用户或相关用户类别定义的规则。
这些规则通常仅适用于用户登录时的 IP 地址,并且在用户通过 SSH 连接登录期间保持加载并有效。一旦 SSH 会话终止,这些规则就会卸载,在大多数情况下,来自用户 IP 地址的所有非 SSH 流量将被拒绝。通过合理的配置,只有经过身份验证的用户产生的流量才会被允许通过。
注意
在 OpenBSD 中,authpf 是默认提供的登录类之一,正如你下次使用 adduser 创建用户时会注意到的那样。
对于默认没有 authpf 登录类的系统,你可能需要将以下几行添加到你的 /etc/login.conf 文件中:
authpf:\
:welcome=/etc/motd.authpf:\
:shell=/usr/sbin/authpf:\
:tc=default:
接下来的几节内容包含了一些示例,虽然可能并不完全适用于你的情况,但我希望它们能为你提供一些启发,帮助你找到适合的解决方案。
一个基本的认证网关
使用 authpf 设置一个身份验证网关涉及创建并维护一些文件,除了基本的 pf.conf 文件之外。主要的新增文件是 authpf.rules。其他文件是相对静态的实体,一旦创建好,你不会再花太多时间在它们上面。
首先创建一个空的 /etc/authpf/authpf.conf 文件。这个文件需要存在才能使 authpf 正常工作,但实际上不需要任何内容,所以用 touch 创建一个空文件即可。
接下来是其他相关的 /etc/pf.conf 部分。首先,这里是接口宏:
ext_if = "re0"
int_if = "athn0"
此外,如果你定义了一个名为 <authpf_users> 的表,authpf 将把已认证用户的 IP 地址添加到该表中:
table <authpf_users> persist
如果需要运行 NAT,负责翻译的规则可以直接放入 authpf.rules 中,但在像这样简单的设置中,将其保留在 pf.conf 文件中不会有坏处:
pass out on $ext_if inet from $localnet nat-to ($ext_if)
以下是 OpenBSD 4.7 之前的语法:
nat on $ext_if inet from $localnet to any -> ($ext_if)
接下来,我们创建 authpf anchor,一旦用户进行身份验证,authpf.rules 中的规则就会被加载:
anchor "authpf/*"
对于 OpenBSD 4.7 之前的 authpf 版本,需要几个 anchor,因此相应的部分如下所示:
nat-anchor "authpf/*"
rdr-anchor "authpf/*"
binat-anchor "authpf/*"
anchor "authpf/*"
这标志着 authpf 设置所需的 pf.conf 文件部分的结束。
对于过滤部分,我们从默认的阻止所有流量开始,然后添加所需的 pass 规则。此时唯一必需的项是允许在内部网络上通过 SSH 流量:
pass quick on $int_if proto { tcp, udp } to $int_if port ssh
从这里开始,实际上完全由你决定。你是否希望在用户进行身份验证之前让客户端进行名称解析?如果是的话,也可以将 TCP 和 UDP 服务域的 pass 规则放入你的 pf.conf 文件中。
对于一个相对简单且平等的设置,你可以包括我们基线规则集的其余部分,将 pass 规则修改为允许来自 <authpf_users> 表中的地址的流量,而不是本地网络中的任何地址:
pass quick proto { tcp, udp } from <authpf_users> to port $udp_services
pass proto tcp from <authpf_users> to port $client_out
对于更为细分的设置,你可以将其余的规则集放入 /etc/authpf/authpf.rules 中,或将每个用户的规则放在每个用户目录下的定制 authpf.rules 文件中,路径为 /etc/authpf/users/。如果你的用户通常需要一些保护,你的通用 /etc/authpf/authpf.rules 文件可以包含如下内容:
client_out = "{ ssh, domain, pop3, auth, nntp, http, https }"
udp_services = "{ domain, ntp }"
pass quick proto { tcp, udp } from $user_ip to port $udp_services
pass proto tcp from $user_ip to port $client_out
宏 user_ip 内置于 authpf 中,并展开为用户认证时的 IP 地址。这些规则将适用于任何在你的网关上完成身份验证的用户。
一个相对简单且容易实现的功能是为需求与普通用户群体不同的用户设置特殊规则。如果用户目录下的 /etc/authpf/users/ 中存在 authpf.rules 文件,那么该文件中的规则将为该用户加载。这意味着你的天真用户 Peter,只需要上网并访问在特定机器的高端口上运行的某个服务,可以通过 /etc/authpf/users/peter/authpf.rules 文件来满足他的需求,内容如下:
client_out = "{ domain, http, https }"
pass inet from $user_ip to 192.168.103.84 port 9000
pass quick inet proto { tcp, udp } from $user_ip to port $client_out
另一方面,Peter 的同事 Christina 运行 OpenBSD,并且通常知道自己在做什么,即使她有时会产生来自奇怪端口的流量。你可以通过在 /etc/authpf/users/christina/authpf.rules 中加入以下内容来给她完全的自由:
pass from $user_ip os = "OpenBSD" to any
这意味着 Christina 只要从她的 OpenBSD 机器进行认证,就几乎可以在 TCP/IP 上做任何她喜欢的事情。
看似开放,但实际上已关闭
在某些设置中,配置网络在链路层保持开放和未加密,同时通过 authpf 强制实施一些限制是有意义的。下一个示例非常类似于你可能在机场或其他公共场所遇到的 Wi-Fi 区域,在这些区域中,任何人都可以连接到接入点并获得 IP 地址,但任何访问 Web 的尝试都会被重定向到一个特定的网页,直到用户通过某种认证。^([24])
这个 pf.conf 文件再次基于我们的基础设置,加入了两个对基本 authpf 设置非常重要的内容——一个宏和一个重定向:
ext_if = "re0"
int_if = "ath0"
auth_web="192.168.27.20"
dhcp_services = "{ bootps, bootpc }" # DHCP server + client
table <authpf_users> persist
pass in quick on $int_if proto tcp from ! <authpf_users> to port http rdr-to $auth_web
match out on $ext_if from $int_if:network nat-to ($ext_if)
anchor "authpf/*"
block all
pass quick on $int_if inet proto { tcp, udp } to $int_if port $dhcp_services
pass quick inet proto { tcp, udp } from $int_if:network to any port domain
pass quick on $int_if inet proto { tcp, udp } to $int_if port ssh
For older authpf versions, use this file instead:
ext_if = "re0"
int_if = "ath0"
auth_web="192.168.27.20"
dhcp_services = "{ bootps, bootpc }" # DHCP server + client
table <authpf_users> persist
rdr pass on $int_if proto tcp from ! <authpf_users> to any port http -> $auth_web
nat on $ext_if from $localnet to any -> ($ext_if)
nat-anchor "authpf/*"
rdr-anchor "authpf/*"
binat-anchor "authpf/*"
anchor "authpf/*"
block all
pass quick on $int_if inet proto { tcp, udp } to $int_if port $dhcp_services
pass quick inet proto { tcp, udp } from $int_if:network to port domain
pass quick on $int_if inet proto { tcp, udp } to $int_if port ssh
auth_web 宏和重定向确保所有来自不在 <authpf_users> 表中的地址的 Web 流量都会将所有未认证的用户引导到一个特定地址。在该地址上,你可以设置一个 Web 服务器,提供你所需的内容。这可以是一个包含联系人的单页,用于获取网络访问权限,也可以是一个接受信用卡并处理用户创建的系统。
请注意,在这种设置中,名称解析会正常工作,但所有上网尝试都会最终转到 auth_web 地址。用户通过认证后,你可以根据需要向 authpf.rules 文件中添加通用规则或特定用户规则。
^([20]) 在一些系统上,旧的设备特定程序,如 wicontrol 和 ancontrol 仍然存在,但大多数情况下,它们已经被弃用,并长期被 ifconfig 功能所取代。在 OpenBSD 上,ifconfig 的整合已经完成。
^([21]) 在 OpenBSD 上快速查阅 man 页面可以告诉你,修改 rum0 接口的 MAC 地址的命令就是 ifconfig rum0 lladdr 00:ba:ad:f0:0d:11。
^([22]) 此外,您还可以在 Web 上查找 man 页面。请查看 www.openbsd.org/ 及其他项目网站,它们提供基于关键字的 man 页面搜索。
^([23]) 在 OpenBSD 4.8 中,封装接口变成了可克隆的接口,你可以配置多个独立的 enc 接口。所有 enc 接口都会成为 enc 接口组的成员。
^([24]) 感谢 Vegard Engen 提供这个想法,并向我展示他的配置,虽然没有保留所有细节,但精神得以保留在这里。
第五章:更大或更复杂的网络

在本章中,我们将在前几章的基础上,解决大中型网络中相对要求较高的应用或用户的现实挑战。本章中的示例配置假设你的数据包过滤设置需要容纳你在本地网络上运行的服务。我们将主要从 Unix 的角度来审视这一挑战,重点关注 SSH、电子邮件和 Web 服务(并提供一些关于如何处理其他服务的提示)。
本章介绍了当你需要将数据包过滤与必须在本地网络外部可访问的服务结合使用时需要做的事情。这将如何增加你的规则集的复杂性,取决于你的网络设计,并在一定程度上取决于你拥有的可路由地址数量。我们将首先讨论官方的、可路由的 IPv4 地址配置,以及通常较为宽松的 IPv6 地址范围。然后,我们将讨论即使只有一个可路由的 IPv4 地址时的情况,并介绍基于 PF 的解决方案,这些解决方案即使在这些限制下也能使服务可用。
内部的 Web 服务器和邮件服务器:可路由的 IPv4 地址
你的网络有多复杂?它需要多复杂?
我们将从第三章中的示例客户端的基础场景开始。我们将客户端设置在一个基础的 PF 防火墙后,并让它们访问托管在其他地方的各种服务,但本地网络上没有运行任何服务。这些客户端将获得三个新的邻居:一个邮件服务器、一个 Web 服务器和一个文件服务器。在这个场景中,我们使用官方的、可路由的 IPv4 地址,因为这样会稍微简化工作。另一种优势是,使用可路由地址时,我们可以让两台新机器为我们的 example.com 域名提供 DNS 服务:一个作为主服务器,另一个作为权威从属服务器。^([25]) 正如你将看到的,添加 IPv6 地址并运行双栈网络不一定会使你的规则集变得显著复杂。
注意
对于 DNS,至少在你的网络外部有一个权威的从属服务器总是有意义的(事实上,一些顶级域名不允许你在没有它的情况下注册域名)。你可能还希望安排一个备份邮件服务器托管在其他地方。在构建网络时,请牢记这些事项。
在这一阶段,我们将物理网络布局保持得相对简单。我们将新的服务器放在与客户端相同的本地网络中——可能在一个单独的服务器房间中,但肯定与客户端位于同一网络段或交换机上。从概念上讲,新的网络看起来像是图 5-1。
在网络的基本参数设置完成后,我们可以开始为所需的服务设置一个合理的规则集。再次强调,我们从基础规则集开始,并添加一些宏以提高可读性。
我们需要的宏自然来自于规范:
-
Web 服务器:
webserver = "{ 192.0.2.227, 2001:db8::baad:f00d:f17 }" -
Web 服务器服务:
webports = "{ http, https }" -
邮件服务器:
emailserver = "{ 192.0.2.225, 2001:db8::baad:f00d:f117 }"
图 5-1. 内部有服务器和客户端的基本网络
-
邮件服务器服务:
email = "{ smtp, pop3, imap, imap3, imaps, pop3s }" -
域名服务器:
nameservers = "{ 192.0.2.221, 192.0.2.223 , \ 2001:db8::baad:f00d:fbaa, 2001:db8::baad:f00d:ff00 }"
注意
此时,您可能已经注意到我们的服务器的 IPv4 和 IPv6 地址都比较靠近各自的地址范围。某些思潮认为,在 IPv6 的情况下,每个接口如果总分配量允许,应该分配至少一个 /64 范围。其他人则提倡更为适度的分配。IETF 关于此问题的当前最佳实践文档是 RFC6177,可以从 IETF 网站获取 (www.ietf.org).
我们假设文件服务器不需要对外界可访问,除非我们选择为它设置一个需要在本地网络外部可见的服务,例如作为我们域的权威从属域名服务器。然后,在手头有了宏之后,我们添加 pass 规则。从 Web 服务器开始,我们使用以下方式让它对外界可访问:
pass proto tcp to $webserver port $webports
SYN 代理值得一试吗?
多年来,synproxy state 选项作为一种防御外部恶意流量的可能屏障,受到了大量关注。具体来说,synproxy state 选项旨在防御可能导致后端资源耗尽的 SYN 洪水攻击。
它是这样工作的:当创建一个新连接时,PF 通常让通信伙伴自行处理连接设置,只要数据包匹配 pass 规则,就简单地将其传递。启用 synproxy 后,PF 处理初始连接设置,只有在连接正确建立后才将连接交给通信伙伴,基本上在通信伙伴之间创建了一个缓冲区。SYN 代理比默认的 keep state 稍微昂贵一些,但在合理规模的设备上不一定会显著感觉到。
在负载均衡设置中,潜在的缺点变得显而易见,其中一个 SYN 代理 PF 可能会接受后端尚未准备好接受的连接,在某些情况下通过将连接设置到负载均衡逻辑原本不会选择的主机上,从而短路冗余。这里的经典例子是一个使用轮询 DNS 的 HTTP 服务器池。但这个问题在像 SMTP 这样的协议中变得尤为明显,因为内置的冗余机制规定(至少按惯例—实际的 RFC 有点模糊)如果主邮件交换器不接受连接,应该尝试一个次级邮件交换器。
在考虑一个synproxy看起来有吸引力的设置时,记住这些问题,并分析将synproxy添加到配置中可能对设置造成的影响。如果你认为需要使用 SYN 代理,只需在需要该选项的规则末尾添加synproxy state。经验法则是,如果你正在遭受主动攻击,插入synproxy选项可能作为临时措施有用。在正常情况下,它不需要作为配置的永久部分。
同样,我们允许外部世界与邮件服务器进行通信:
pass proto tcp to $emailserver port $email
这样,任何地方的客户端都能与局域网中的客户端拥有相同的访问权限,包括一些可能未加密运行的邮件检索协议。这在现实世界中是常见的,但如果你正在设置一个新的网络,可能需要考虑你的选择。
为了让邮件服务器发挥作用,它也需要能够向局域网外的主机发送邮件:
pass log proto tcp from $emailserver to port smtp
请记住,规则集以block all规则开始,这意味着只有邮件服务器被允许从局域网发起到外部世界的 SMTP 流量。如果网络上的其他主机需要向外界发送或接收邮件,它们需要使用指定的邮件服务器。这可能是一个很好的方法,确保例如你能尽可能让任何可能出现在网络中的垃圾邮件僵尸机难以投递其负载。
最后,名称服务器需要能够被外部网络中的客户端访问,这些客户端需要查找关于example.com以及我们权威回答的任何其他域的信息:
pass proto { tcp, udp } to $nameservers port domain
在整合了所有需要从外部访问的服务后,我们的规则集大致如下所示:
ext_if = "ep0" # macro for external interface - use tun0 or pppoe0 for PPPoE
int_if = "ep1" # macro for internal interface
localnet = $int_if:network
webserver = "{ 192.0.2.227, 2001:db8::baad:f00d:f17 }"
webports = "{ http, https }"
emailserver = "{ 192.0.2.225, 2001:db8::baad:f00d:f117 }"
email = "{ smtp, pop3, imap, imap3, imaps, pop3s }"
nameservers = "{ 192.0.2.221, 192.0.2.223, \
2001:db8::baad:f00d:fbaa, 2001:db8::baad:f00d:ff00 }"
client_out = "{ ssh, domain, pop3, auth, nntp, http,\
https, cvspserver, 2628, 5999, 8000, 8080 }"
udp_services = "{ domain, ntp }"
icmp_types = "{ echoreq, unreach }"
icmp6_types = "{ echoreq unreach timex paramprob }"
block all
pass quick proto { tcp, udp } from $localnet to port $udp_services
pass log inet proto icmp all icmp-type $icmp_types
pass inet6 proto icmp6 icmp6-type $icmp6_types
pass proto tcp from $localnet to port $client_out
pass proto { tcp, udp } to $nameservers port domain
pass proto tcp to $webserver port $webports
pass log proto tcp to $emailserver port $email
pass log proto tcp from $emailserver to port smtp
这仍然是一个相对简单的设置,但不幸的是,它有一个可能令人担忧的安全缺点。这个网络的设计方式是,所有为外界提供服务的服务器都与客户端位于同一局域网内,而你需要将任何内部服务限制为仅本地访问。原则上,这意味着攻击者只需要入侵你局域网中的一个主机,就能访问该局域网中的任何资源,从而使恶意者与你局域网中的任何用户处于平等地位。根据每台机器和资源对未经授权访问的保护程度,这可能从一个小麻烦到一个大问题不等。
在接下来的章节中,我们将探讨一些将需要与外界交互的服务与局域网隔离的选项。
一定的隔离度:引入 DMZ(隔离区)
在上一节中,你已经学习了如何在本地网络上设置服务,并通过合理的 PF 规则集将它们选择性地公开给外部世界。为了更精细地控制对内部网络的访问,以及需要对外界可见的服务,可以增加一定程度的物理隔离。即使是一个单独的虚拟局域网(VLAN)也能很好地解决问题。
实现物理和逻辑分离相当简单:只需将运行公共服务的机器移到一个连接到网关单独接口的网络中。其最终效果是创建一个独立的网络,它既不是你本地网络的一部分,也不完全属于互联网上的公共部分。从概念上讲,这个隔离的网络看起来像是图 5-2 中所示。
图 5-2。具有服务器的 DMZ 网络
注意
把这个小网络看作是敌对派系领土之间相对平静的区域。几年前,有人创造了“非军事区(DMZ)”这一术语,用来描述这种配置。
对于地址分配,你可以为新的 DMZ 网络隔离出一块适当大小的官方地址空间。或者,你可以将那些没有特定需求需要使用公开可访问和可路由的 IPv4 地址的网络部分,转移到 NAT 环境中。无论哪种方式,你最终都会在过滤配置中至少拥有一个额外的接口。正如你稍后将看到的,如果你的官方 IPv4 地址非常紧张,也可以在全 NAT 环境中运行 DMZ 设置。
对规则集本身的调整不需要太复杂。如果需要,你可以修改每个接口的配置。基本的规则集逻辑保持不变,但你可能需要调整宏的定义(webserver,mailserver,nameservers,以及可能的其他宏),以适应你的新网络布局。
在我们的示例中,我们可以选择将已经放置服务器的地址范围部分进行隔离。如果为扩展留出一些空间,我们可以为新的dmz_if设置 IPv4 范围,使用/25 子网,其网络地址和子网掩码为 192.0.2.128/255.255.255.128。这样,192.0.2.129 到 192.0.2.254 将作为 DMZ 中主机可用的地址范围。由于我们已经将服务器放置在 2001:db8::baad:f00d:0/112 网络中(该网络有 65,536 个地址可供使用),对于 IPv6 范围来说,最简单的方式是也将该网络进行隔离,并为面向该网络的接口分配适当的 IPv6 地址,就像图 5-2 中所示。
使用该配置,并且没有更改分配给服务器的 IP 地址,你实际上不需要触碰规则集,只需在设置物理隔离的 DMZ 后,数据包过滤就能正常工作。这是一个很好的副作用,可能是由于懒惰或者是出色的长远规划。无论如何,它都强调了拥有合理地址分配策略的重要性。
通过编辑你的 pass 规则来严格限制规则集可能会很有用,这样流量进出服务器时只能通过与服务相关的接口:
pass in on $ext_if proto { tcp, udp } to $nameservers port domain
pass in on $int_if proto { tcp, udp } from $localnet to $nameservers \
port domain
pass out on $dmz_if proto { tcp, udp } to $nameservers port domain
pass in on $ext_if proto tcp to $webserver port $webports
pass in on $int_if proto tcp from $localnet to $webserver port $webports
pass out on $dmz_if proto tcp to $webserver port $webports
pass in log on $ext_if proto tcp to $mailserver port smtp
pass in log on $int_if proto tcp from $localnet to $mailserver \
port $email
pass out log on $dmz_if proto tcp to $mailserver port smtp
pass in on $dmz_if from $mailserver to port smtp
pass out log on $ext_if proto tcp from $mailserver to port smtp
你可以选择使其他引用本地网络接口的 pass 规则也具有接口特定性,但如果你保持它们不变,它们将继续有效。
负载共享:重定向到地址池
一旦你设置了可以向全球提供访问的服务,一个可能的场景是,随着时间的推移,你的某些服务会变得更复杂、需要更多资源,或者只是吸引了比你希望单台服务器处理的更多流量。
有多种方法可以使多台机器共享运行服务的负载,包括对服务本身进行微调的方式。对于网络级负载均衡,PF 提供了你所需的基本功能,通过重定向到表格或地址池。实际上,你可以在不触碰 pass 规则的情况下实现一种负载均衡,至少如果你的环境还不是双栈的情况下。
以我们示例中的 Web 服务器为例。我们已经有了表示服务的宏,我们的 Web 服务器。由于接下来将要讨论的原因,我们需要将该宏简化为只表示公共 IPv4 地址(webserver = "192.0.2.227"),该地址与用户书签中可能标记的主机名(例如:www.example.com)相关联。当需要分担负载时,设置所需数量的相同或至少等效的服务器,然后稍微修改规则集以引入重定向。首先,定义一个表格,用于存储 Web 服务器池的 IPv4 地址:
table <webpool> persist { 192.0.2.214, 192.0.2.215, 192.0.2.216, 192.0.2.217 }
然后,执行重定向:
match in on $ext_if protp tcp to $webserver port $webports \
rdr-to <webpool> round-robin
与早期示例中的重定向不同,比如 第三章 中的 FTP 代理,此规则将 webpool 表中的所有成员设置为即将到来的连接的潜在重定向目标,这些连接目标是指向 webserver 地址上的 webports 端口。每个与此规则匹配的传入连接都会被重定向到表中的一个地址,从而将负载分散到多个主机上。完成此重定向切换后,你可以选择退休原 Web 服务器,或者让其被新的 Web 服务器池吸收。
在 OpenBSD 4.7 之前的 PF 版本中,相应的规则如下:
rdr on $ext_if proto tcp to $webserver port $webports -> <webpool> round-robin
在这两种情况下,round-robin 选项意味着 PF 通过依次循环重定向地址表中的机器,来分担池中机器的负载。
一些应用程序期望每个单独的源地址的访问总是定向到后端的同一主机(例如,有些服务依赖于客户端或会话特定的参数,如果新连接访问不同的后端主机,这些参数将丢失)。如果你的配置需要支持这种服务,你可以添加 sticky-address 选项,确保来自客户端的新连接始终定向到与初始连接相同的后端主机。使用此选项的缺点是,PF 需要为每个客户端维护源跟踪数据,而最大源节点跟踪的默认值是 10,000,这可能成为一个限制因素。(有关调整此类限制值的建议,请参见第十章。)
当负载分配的平衡不是绝对要求时,选择 random 的重定向地址可能是合适的:
match in on $ext_if proto tcp to $webserver port $webports \
rdr-to <webpool> random
注意
在 OpenBSD 4.7 之前的 PF 版本中,random 选项不支持用于重定向到地址表或地址列表。
即使是拥有大量官方可路由 IPv4 地址的组织,也选择在其负载均衡服务器池与互联网之间引入 NAT。这种技术在各种基于 NAT 的设置中同样有效,但迁移到 NAT 提供了一些额外的可能性和挑战。
为了以这种方式支持 IPv4 和 IPv6 双栈环境,你需要为地址池设置独立的表,并为 IPv4 和 IPv6 设置独立的 pass 或 match 规则和重定向。虽然一开始听起来将 IPv4 和 IPv6 地址放在一个表中是一个优雅的想法,但这里概述的简单重定向规则并不智能到足以根据单个表条目的地址族做出正确的重定向决策。
使用 relayd 进行负载均衡
在通过轮询重定向进行负载均衡运行一段时间后,你可能会注意到重定向不会自动适应外部条件。例如,除非采取特殊措施,否则如果重定向目标列表中的某个主机宕机,流量仍然会被重定向到可能的 IP 地址列表中。
显然,需要一个监控解决方案。幸运的是,OpenBSD 基础系统提供了一个。中继守护进程 relayd^([26]) 与你的 PF 配置交互,提供了从你的池中筛选掉不正常主机的功能。然而,将 relayd 引入你的设置可能需要对你的规则集做一些小的调整。
relayd守护进程基于两类主要服务工作,分别称为重定向和中继。它期望能够将主机的 IP 地址添加到或从它控制的 PF 表中删除。该守护进程通过一个专门的锚点relayd与你的规则集交互(在 OpenBSD 4.7 之前的版本中,还包含一个名为rdr-anchor的重定向锚点,名称相同)。
为了看看我们如何通过使用relayd使我们的示例配置更好,我们回顾一下负载均衡规则集。从pf.conf文件的顶部开始,添加relayd的锚点,以便根据需要插入规则:
anchor "relayd/*"
在 OpenBSD 4.7 之前的版本中,你还需要使用重定向锚点:
rdr-anchor "relayd/*" anchor "relayd/*"
在负载均衡规则集中,我们为 Web 服务器池定义了以下内容:
table webpool persist { 192.0.2.214, 192.0.2.215, 192.0.2.216, 192.0.2.217 }
它有一个match规则来设置重定向:
match in on $ext_if proto tcp to $webserver port $webports \
rdr-to <webpool> round-robin
或者,在 OpenBSD 4.7 之前的版本中,你会使用以下配置:
rdr on $ext_if proto tcp to $webserver port $webports -> <webpool> round-robin
为了让这个配置效果更好,我们移除重定向和表(记得在双栈配置中处理好这两个部分),并让relayd通过在锚点内设置自己的版本来处理重定向。 (不过,不要移除pass规则,因为你的规则集仍然需要一个pass规则,允许流量访问relayd表中的 IP 地址。如果你有针对inet和inet6流量的单独规则,你可能可以将这些规则合并回一个规则。)
一旦处理了pf.conf的部分内容,我们转向relayd的relayd.conf配置文件。这个配置文件的语法与pf.conf类似,因此比较容易阅读和理解。首先,我们添加稍后将使用的宏定义:
web1="192.0.2.214"
web2="192.0.2.215"
web3="192.0.2.216"
web4="192.0.2.217"
webserver="192.0.2.227"
sorry_server="192.0.2.200"
所有这些都对应于我们可以在pf.conf文件中放置的定义。relayd的默认检查间隔是 10 秒,这意味着主机可能会停机近 10 秒钟,才会被标记为离线。为了小心起见,我们将检查间隔设置为 5 秒,以尽量减少可见的停机时间,配置如下:
interval 5 # check hosts every 5 seconds
现在,我们创建一个名为webpool的表,使用了大部分的宏:
table <webpool> { $web1, $web2, $web3, $web4 }
出于稍后会讲到的原因,我们定义了另一个表:
table <sorry> { $sorry_server }
到这一步,我们准备好设置重定向:
redirect www {
listen on $webserver port 80 sticky-address
tag relayd
forward to <webpool> check http "/status.html" code 200 timeout 300
forward to <sorry> timeout 300 check icmp
}
这个重定向表示对端口 80 的连接应该被重定向到webpool表中的成员。sticky-address选项在这里与 PF 规则中的rdr-to效果相同:来自同一源 IP 地址的新连接(在由timeout值定义的时间间隔内)会被重定向到后端池中与之前相同的主机。
relayd守护进程应检查主机是否可用,方法是请求文件/status.html,使用 HTTP 协议,并期望返回码为 200\。这是客户端请求运行中的 Web 服务器提供其可用文件时的预期结果。
到目前为止,应该没有什么大惊小怪的,对吧?relayd守护进程会处理从表中排除故障主机的情况。如果webpool表中的所有主机都宕机了怎么办?幸运的是,开发人员也考虑到了这一点,并引入了为服务配置备份表的概念。这是www服务定义的最后一部分,sorry表作为备份表:如果webpool表为空,sorry表中的主机会接管。这意味着你需要配置一个能够在webpool中的所有主机失败时提供“抱歉,我们已停机”消息的服务。
如果你运行的是 IPv6-only 服务,当然应该将你的 IPv6 地址替换为之前示例中给出的地址。如果你运行的是双栈配置,最好为每种协议分别设置负载均衡机制,其中配置仅在名称上有所不同(例如,将4或6附加到 IPv4 和 IPv6 名称集上),以及地址本身。
在所有有效的relayd配置元素都到位后,你可以启用你的新配置。
在你实际启动relayd之前,向你的/etc/rc.conf.local文件中添加一个空的relayd_flags集来启用:
relayd_flags="" # for normal use: ""
重新加载你的 PF 规则集,然后启动relayd。如果你想在实际启动relayd之前检查配置,可以使用-n命令行选项来检查relayd:
$ **sudo relayd -n**
如果你的配置正确,relayd会显示消息configuration OK并退出。
要实际启动守护进程,你可以不带任何命令行标志地启动relayd,但像大多数守护进程一样,最好通过其rc脚本包装器启动,包装器存储在/etc/rc.d/中,因此以下序列会重新加载你编辑过的 PF 配置并启用relayd。
**$ sudo pfctl -f /etc/pf.conf**
**$ sudo sh /etc/rc.d/relayd start**
配置正确时,两个命令将静默启动,而不会显示任何消息。(如果你更喜欢更详细的消息,pfctl和relayd都提供了-v标志。对于relayd,你可能希望在rc.conf.local条目中添加-v标志。)你可以使用top或ps检查relayd是否正在运行。在这两种情况下,你将看到三个relayd进程,大致如下所示:
**$ ps waux | grep relayd**
_relayd 9153 0.0 0.1 776 1424 ?? S 7:28PM 0:00.01 relayd: pf update engine
(relayd)
_relayd 6144 0.0 0.1 776 1440 ?? S 7:28PM 0:00.02 relayd: host check engine
(relayd)
root 3217 0.0 0.1 776 1416 ?? Is 7:28PM 0:00.01 relayd: parent (relayd)
正如我们之前提到的,在你的rc.conf.local文件中添加一个空的relayd_flags集时,relayd会在启动时启用。然而,一旦配置启用,与你的relayd交互的绝大多数操作将通过relayctl管理程序进行。除了让你监控状态外,relayctl还允许你重新加载relayd配置并有选择地禁用或启用主机、表和服务。你甚至可以以交互方式查看服务状态,如下所示:
**$ sudo relayctl show summary**
Id Type Name Avlblty Status
1 redirect www active
1 table webpool:80 active (2 hosts)
1 host 192.0.2.214 100.00% up
2 host 192.0.2.215 0.00% down
3 host 192.0.2.216 100.00% up
4 host 192.0.2.217 0.00% down
2 table sorry:80 active (1 hosts)
5 host 127.0.0.1 100.00% up
在这个示例中,webpool严重降级,只有四个主机中的两个正在运行。幸运的是,备用表仍在正常工作,如果最后两个服务器也失败,它应该仍然能够正常工作。目前,所有表格都至少有一个主机处于活动状态。对于没有任何成员的表格,Status列会显示为空。请求relayctl获取主机信息时,显示的是主机中心格式的状态信息:
**$ sudo relayctl show hosts**
Id Type Name Avlblty Status
1 table webpool:80 active (3 hosts)
1 host 192.0.2.214 100.00% up
total: 11340/11340 checks
2 host 192.0.2.215 0.00% down
total: 0/11340 checks, error: tcp connect failed
3 host 192.0.2.216 100.00% up
total: 11340/11340 checks
4 host 192.0.2.217 0.00% down
total: 0/11340 checks, error: tcp connect failed
2 table sorry:80 active (1 hosts)
5 host 127.0.0.1 100.00% up
total: 11340/11340 checks
如果你需要将某个主机从池中移除以进行维护(或其他任何耗时的操作),你可以使用relayctl禁用它,如下所示:
$ **sudo relayctl host disable 192.0.2.217**
在大多数情况下,操作会显示command succeeded,以表明操作成功完成。一旦你完成了维护并将机器重新上线,你可以使用以下命令将其重新启用为relayd池的一部分:
$ **sudo relayctl host enable 192.0.2.217**
再次,你应该几乎立即看到消息command succeeded,以表示操作成功。
除了此处演示的基本负载均衡外,relayd在最近的 OpenBSD 版本中进行了扩展,提供了多个功能,使其在更复杂的设置中具有吸引力。例如,它现在可以处理第 7 层代理或中继功能,用于 HTTP 和 HTTPS,包括带有头部附加和重写的协议处理、URL 路径附加和重写,甚至会话和 cookie 处理。协议处理需要根据你的应用进行定制。例如,以下是一个简单的 HTTPS 中继,用于负载均衡从客户端到 Web 服务器的加密 Web 流量。
http protocol "httpssl" {
header append "$REMOTE_ADDR" to "X-Forwarded-For"
header append "$SERVER_ADDR:$SERVER_PORT" to "X-Forwarded-By"
header change "Keep-Alive" to "$TIMEOUT"
query hash "sessid"
cookie hash "sessid"
path filter "*command=*" from "/cgi-bin/index.cgi"
ssl { sslv2, ciphers "MEDIUM:HIGH" }
tcp { nodelay, sack, socket buffer 65536, backlog 128 }
}
这个协议处理器定义演示了一系列对 HTTP 头部的简单操作,并设置了 SSL 参数和特定的 TCP 参数,以优化连接处理。头部选项对协议头部进行操作,通过附加到现有头部(append)或将内容更改为新值(change)来插入变量的值。
URL 和 cookie 哈希值由负载均衡器用于选择将请求转发到目标池中的哪个主机。path filter指定任何get请求,包括第一个引用字符串作为第二个字符串的子串,都应被丢弃。ssl选项指定只接受 SSL 版本 2 密码套件,密钥长度处于中高范围——换句话说,128 位或更高。^([27]) 最后,tcp选项指定nodelay以最小化延迟,指定使用选择确认方法(RFC 2018),并设置套接字缓冲区大小和负载均衡器跟踪的最大待处理连接数。这些选项仅作为示例;在大多数情况下,你的应用程序在默认值设置下表现良好。
使用协议处理器定义的中继遵循一种模式,考虑到之前定义的www服务,应该是很熟悉的:
relay wwwssl {
# Run as a SSL accelerator
listen on $webserver port 443 ssl
protocol "httpssl"
table <webhosts> loadbalance check ssl
}
尽管如此,启用 SSL 的 Web 应用程序可能会从稍微不同的一组参数中受益。
注意
我们添加了check ssl,假设webhosts表中的每个成员都已正确配置以完成 SSL 握手。根据您的应用程序,可能有必要考虑将所有 SSL 处理保持在relayd中,从而将加密处理任务从后端卸载。
最后,对于运行relayd的主机的基于 CARP 的故障切换(有关 CARP 的信息,请参见第八章),relayd可以通过在关机或启动时为指定的接口组设置 CARP 降级计数器来配置以支持 CARP 交互。
和 OpenBSD 系统的其他部分一样,relayd带有详细的手册页。对于这里没有涵盖的角度和选项(有一些),可以深入查阅relayd、relayd.conf和relayctl的手册页,并开始实验,找到所需的配置。
内部的 Web 服务器和邮件服务器——NAT 版本
让我们稍微回溯一下,从基线场景重新开始,在这个场景中,来自第三章的示例客户端获得了三个新邻居:一台邮件服务器、一台 Web 服务器和一台文件服务器。这次,外部可见的 IPv4 地址要么不可用,要么过于昂贵,而且在一台主要是防火墙的机器上运行其他多个服务并不理想。这意味着我们又回到了在网关处进行 NAT 的情况。幸运的是,PF 中的重定向机制使得将服务器保持在执行 NAT 的网关内部变得相对容易。
网络规格与我们刚才处理的example.com设置相同:我们需要运行一台 Web 服务器,提供明文(http)和加密(https)形式的数据,同时希望运行一台邮件服务器,发送和接收电子邮件,同时允许内外部的客户端使用多个知名的提交和检索协议。简而言之,我们希望的功能几乎与上一部分的设置相同,但只使用一个可路由地址。
在这三台服务器中,只有 Web 服务器和邮件服务器需要对外部可见,因此我们将它们的 IP 地址和服务添加到第三章规则集中:
webserver = "192.168.2.7"
webports = "{ http, https }"
emailserver = "192.168.2.5"
email = "{ smtp, pop3, imap, imap3, imaps, pop3s }"
由于只有一个可路由的地址,并且服务器隐藏在 NAT 地址空间中,我们需要在网关上设置规则,重定向我们希望服务器处理的流量。我们可以定义一组match规则来设置重定向,然后在稍后的规则集中单独处理block或pass问题,如下所示:
match in on $ext_if proto tcp to $ext_if port $webports rdr-to $webserver
match in on $ext_if proto tcp to $ext_if port $email rdr-to $emailserver
pass proto tcp to $webserver port $webports
pass proto tcp to $emailserver port $email
pass proto tcp from $emailserver to port smtp
这种 match 和 pass 规则的组合与 OpenBSD 4.7 之前的 PF 版本做法非常相似,如果您从旧版本升级,这种快速编辑可以迅速弥合语法差距。但您也可以选择使用新的风格,写出这种稍微简洁的版本:
pass in on $ext_if inet proto tcp to $ext_if port $webports rdr-to $webserver tag RDR
pass in on $ext_if inet proto tcp to $ext_if port $email rdr-to $mailserver tag RDR
pass on $int_if inet tagged RDR
请注意使用带有 rdr-to 的 pass 规则。这个过滤和重定向的组合将在稍后使事情变得更简单,因此现在可以尝试使用这种组合。
在 OpenBSD 4.7 之前的 PF 中,规则集会非常相似,区别在于我们处理重定向的方式。
webserver = "192.168.2.7"
webports = "{ http, https }"
emailserver = "192.168.2.5"
email = "{ smtp, pop3, imap, imap3, imaps, pop3s }"
rdr on $ext_if proto tcp to $ext_if port $webports -> $webserver
rdr on $ext_if proto tcp to $ext_if port $email -> $emailserver
pass proto tcp to $webserver port $webports
pass proto tcp to $emailserver port $email
pass proto tcp from $emailserver to any port smtp
带 NAT 的 DMZ
在全 NAT 配置下,分配给 DMZ 的可用地址池可能比我们之前的示例更大,但相同的原则适用。当您将服务器移动到物理隔离的网络时,需要检查规则集的宏定义是否合理,并在必要时调整这些值。
就像在路由地址的情况下,编辑 pass 规则以使进出服务器的流量仅能通过与服务实际相关的接口,这样可以更有效地精简您的规则集:
pass in on $ext_if inet proto tcp to $ext_if port $webports rdr-to $webserver
pass in on $int_if inet proto tcp from $localnet to $webserver port $webports
pass out on $dmz_if proto tcp to $webserver port $webports
pass in log on $ext_if inet proto tcp to $ext_if port $email rdr-to $mailserver
pass in log on $int_if proto tcp from $localnet to $mailserver port $email
pass out log on $dmz_if proto tcp to $mailserver port smtp
pass in on $dmz_if from $mailserver to port smtp
pass out log on $ext_if proto tcp from $mailserver to port smtp
对于 OpenBSD 4.7 之前的 PF,规则集在某些细节上有所不同,重定向仍然是在单独的规则中进行:
pass in on $ext_if proto tcp to $webserver port $webports
pass in on $int_if proto tcp from $localnet to $webserver port $webports
pass out on $dmz_if proto tcp to $webserver port $webports
pass in log on $ext_if proto tcp to $mailserver port smtp
pass in log on $int_if proto tcp from $localnet to $mailserver port $email
pass out log on $dmz_if proto tcp to $mailserver port smtp
pass in on $dmz_if from $mailserver to port smtp
pass out log on $ext_if proto tcp from $mailserver to port smtp
您可以创建特定的 pass 规则来引用您的本地网络接口,但如果保持现有的 pass 规则不变,它们仍会继续有效。
用于负载均衡的重定向
来自前一个示例的基于重定向的负载均衡规则在 NAT 环境下同样有效,其中公共地址是网关的外部接口,重定向地址位于私有范围内。
这是 webpool 的定义:
table <webpool> persist { 192.168.2.7, 192.168.2.8, 192.168.2.9, 192.168.2.10 }
路由地址和 NAT 版本之间的主要区别在于,在添加了 webpool 定义之后,您需要编辑现有的带有重定向的 pass 规则,变成如下所示:
pass in on $ext_if inet proto tcp to $ext_if port $webports rdr-to <webpool> round-robin
或者对于 OpenBSD 4.7 之前的 PF 版本,使用如下配置:
rdr on $ext_if proto tcp to $ext_if port $webports -> <webpool> round-robin
从那时起,您的 NATed DMZ 的行为与具有正式路由地址的 DMZ 十分相似。
注意
您可以配置有效的 IPv6 设置,以与像这样的 NAT IPv4 设置共存,但如果选择这样做,请确保在 PF 规则中分别处理 inet 和 inet6 流量。与普遍看法相反,带有 nat-to 和 rdr-to 选项的规则在 IPv6 配置中与 IPv4 相同。
回到单一 NAT 网络
你可能会惊讶地发现,在某些情况下,设置一个小型网络比处理一个大型网络更为困难。例如,回到服务器与客户端位于同一物理网络的情况,基本的 NAT 配置运行得非常好——直到某个程度。事实上,只要您的目标是让外部主机流量到达您的服务器,一切都会运行得非常顺利。
这是完整的配置:
ext_if = "re0" # macro for external interface - use tun0 or pppoe0 for PPPoE
int_if = "re1" # macro for internal interface
localnet = $int_if:network
# for ftp-proxy
proxy = "127.0.0.1"
icmp_types = "{ echoreq, unreach }"
client_out = "{ ssh, domain, pop3, auth, nntp, http, https, \
446, cvspserver, 2628, 5999, 8000, 8080 }"
udp_services = "{ domain, ntp }"
webserver = "192.168.2.7"
webports = "{ http, https }"
emailserver = "192.168.2.5"
email = "{ smtp, pop3, imap, imap3, imaps, pop3s }"
# NAT: ext_if IP address could be dynamic, hence ($ext_if)
match out on $ext_if from $localnet nat-to ($ext_if)
block all
# for ftp-proxy: Remember to put the following line, uncommented, in your
# /etc/rc.conf.local to enable ftp-proxy:
# ftpproxy_flags=""
anchor "ftp-proxy/*"
pass in quick proto tcp to port ftp rdr-to $proxy port 8021
pass out proto tcp from $proxy to port ftp
pass quick inet proto { tcp, udp } to port $udp_services
pass proto tcp to port $client_out
# allow out the default range for traceroute(8):
# "base+nhops*nqueries-1" (33434+64*3-1)
pass out on $ext_if inet proto udp to port 33433 >< 33626 keep state
# make sure icmp passes unfettered
pass inet proto icmp icmp-type $icmp_types from $localnet
pass inet proto icmp icmp-type $icmp_types to $ext_if
pass in on $ext_if inet proto tcp to $ext_if port $webports rdr-to $webserver
pass in on $ext_if inet proto tcp to $ext_if port $email rdr-to $mailserver
pass on $int_if inet proto tcp to $webserver port $webports
pass on $int_if inet proto tcp to $mailserver port $email
这里的最后四个规则是我们最关心的。如果你尝试从自己网络中的主机访问官方地址上的服务,你很快会发现,来自本地网络的重定向服务请求很可能永远不会到达外部接口。这是因为所有的重定向和翻译都发生在外部接口上。网关接收到来自本地网络的包,在内部接口上,目标地址设置为外部接口的地址。网关将该地址识别为其自身的地址,并试图将请求处理为针对本地服务的请求;结果,重定向在内部无法正常工作。
对于 OpenBSD 4.7 之前的系统,前面规则集中的最后四行对应的部分如下所示:
rdr on $ext_if proto tcp to $ext_if port $webports -> $webserver
rdr on $ext_if proto tcp to $ext_if port $email -> $emailserver
pass proto tcp to $webserver port $webports
pass proto tcp to $emailserver port $email
pass proto tcp from $emailserver to any port smtp
幸运的是,有几个针对这个特定问题的解决方法。这个问题足够常见,以至于 PF 用户指南列出了四种不同的解决方案,^([28]),其中包括将服务器迁移到 DMZ,正如前面所述。因为这是一本关于 PF 的书,我们将专注于基于 PF 的解决方案(实际上是一个相当糟糕的解决方法),该方法通过将本地网络作为我们重定向和 NAT 规则的特殊情况来处理。
我们需要拦截来自本地网络的网络数据包,并正确处理这些连接,确保任何返回流量都能定向到实际发起连接的通信伙伴。这意味着为了使重定向在本地网络中按预期工作,我们需要添加特殊情况的重定向规则,这些规则与设计用来处理外部请求的规则相似。首先,下面是 OpenBSD 4.7 及更新版本的pass规则与重定向:
pass in on $ext_if inet proto tcp to $ext_if port $webports rdr-to $webserver
pass in on $ext_if inet proto tcp to $ext_if port $email rdr-to $mailserver
pass in log on $int_if inet proto tcp from $int_if:network to $ext_if \
port $webports rdr-to $webserver
pass in log on $int_if inet proto tcp from $int_if:network to $ext_if \
port $email rdr-to $mailserver
match out log on $int_if proto tcp from $int_if:network to $webserver \
port $webports nat-to $int_if
pass on $int_if inet proto tcp to $webserver port $webports
match out log on $int_if proto tcp from $int_if:network to $mailserver \
port $email nat-to $int_if
pass on $int_if inet proto tcp to $mailserver port $email
前两条规则与原始规则相同。接下来的两条规则拦截来自本地网络的流量,且两者中的rdr-to操作会重写目标地址,类似于对应规则在处理来自其他地方的流量时所做的事情。pass规则在$int_if接口上的作用与之前版本中的相同。
match规则与nat-to作为路由解决方法存在。没有它们,webserver和mailserver主机会将重定向连接的返回流量直接路由回本地网络中的主机,而这些流量将无法匹配任何外发连接。有了nat-to,这些服务器将网关视为流量的源,并将返回流量通过最初的路径重新定向回去。网关将返回流量与从本地网络的客户端创建的连接状态进行匹配,并采取适当的操作将流量返回到正确的客户端。
对于 OpenBSD 4.7 之前的版本,相应的规则乍一看可能更为复杂,但最终结果是相同的。
rdr on $int_if proto tcp from $localnet to $ext_if port $webports -> $webserver
rdr on $int_if proto tcp from $localnet to $ext_if port $email -> $emailserver
no nat on $int_if proto tcp from $int_if to $localnet
nat on $int_if proto tcp from $localnet to $webserver port $webports -> $int_if
nat on $int_if proto tcp from $localnet to $emailserver port $email -> $int_if
通过这种方式,我们可以扭转重定向和地址转换的逻辑,完成我们需要的操作,而根本不需要触及pass规则。(我有幸通过电子邮件和 IRC 目睹了几个网络管理员在意识到这五行重新配置的真相时的反应。)
过滤接口组
你的网络可能有多个子网,这些子网之间可能永远不需要相互通信,除非是一些公共服务,如电子邮件、网页、文件和打印。你如何处理这些子网的进出流量取决于你的网络设计。一个有用的方法是将每个低权限的网络视为一个独立的本地网络,并将其附加到一个公共过滤网关的独立接口上,然后为其设定一套规则,仅允许与主网关相邻网络的所需直接交互。
通过将逻辑相似的接口分组到接口组中,并将过滤规则应用于这些组而不是单个接口,你可以使 PF 配置更加可管理和易于阅读。接口组是通过ifconfig group选项实现的,最早出现在 OpenBSD 3.6 版本中,之后在 FreeBSD 7.0 及以后的版本中得到了采用。
所有配置的网络接口都可以配置为属于一个或多个组。有些接口会自动属于某个默认组。例如,所有 IEEE 802.11 无线网络接口都属于wlan组,而与默认路由相关的接口则属于egress组。幸运的是,接口可以是多个组的成员,你可以通过适当的ifconfig命令将接口添加到接口组中,下面是一个示例:
# ifconfig sis2 group untrusted
对于永久配置,在 OpenBSD 中等效的配置将出现在hostname.sis2文件中,或者在 FreeBSD 7.0 及以后的版本中出现在ifconfig_sis2=行中。
在有意义的地方,你可以像处理单个接口一样处理接口组中的过滤规则:
pass in on untrusted to any port $webports
pass out on egress to any port $webports
如果你现在想到,在到目前为止的大多数规则集示例中,完全可以在egress组上进行过滤,而不是宏$ext_if,那么你已经掌握了一个重要的概念。回顾你现有的规则集,看看使用接口组如何进一步帮助提高可读性,可能是一个有益的练习。记住,一个接口组可以有一个或多个成员。
请注意,过滤接口组使得编写本质上与硬件无关的规则集成为可能。只要你的hostname.if文件或ifconfig_if=行将接口放入正确的组中,始终在接口组上过滤的规则集将能够在硬件配置可能不同的机器之间完全移植。
在没有接口组功能的系统中,你可能能够通过创造性地使用宏来实现类似的效果,具体如下:
untrusted = "{ ath0 ath1 wi0 ep0 }"
egress = "sk0"
标签的力量
在某些网络中,不能轻易通过子网和服务等标准来决定数据包是否允许通过。站点策略所需的精细控制可能会使规则集变得复杂,且难以维护。
幸运的是,PF 提供了另一种分类和过滤机制,即数据包标签。实现数据包标签的有效方式是对符合特定pass规则的传入数据包进行tag标记,然后根据数据包所标记的标识符将其传递到其他地方。在 OpenBSD 4.6 及以后的版本中,甚至可以有单独的match规则,根据匹配标准进行tag标记,将传递、重定向或其他操作的决定留给规则集中后面的规则。
一个例子可能是我们在第四章中设置的无线接入点,我们可以合理地预期这些接入点将以与接入点的$ext_if地址相同的源地址将流量注入到本地网络中。在这种情况下,对于网关的规则集,可以合理地添加以下内容(当然,假设wifi_allowed和wifi_ports宏的定义符合站点的要求):
wifi = "{ 10.0.0.115, 10.0.0.125, 10.0.0.135, 10.0.0.145 }"
pass in on $int_if from $wifi to $wifi_allowed port $wifi_ports tag wifigood
pass out on $ext_if tagged wifigood
随着规则集复杂性的增加,考虑在传入的match和pass规则中使用tag,使规则集更具可读性并易于维护。
标签是粘性的,一旦数据包被匹配规则标记,标签就会保留,这意味着即使数据包不是由最后匹配的规则应用的,它也可以拥有标签。然而,任何时候一个数据包只能有一个标签。如果数据包匹配多个应用标签的规则,标签会被每个新的匹配tag规则覆盖。
例如,你可以通过一组match或pass规则为传入流量设置多个标签,然后再通过一组pass规则根据传入流量上设置的标签来确定数据包的去向。
桥接防火墙
以太网桥接由两个或更多配置为透明转发以太网帧的接口组成,这些接口对于上层(如 TCP/IP 协议栈)是不可见的。在过滤的上下文中,桥接配置通常被认为很有吸引力,因为它意味着过滤操作可以在没有自己 IP 地址的机器上进行。如果该机器运行 OpenBSD 或类似功能的操作系统,它仍然可以过滤和重定向流量。
这种设置的主要优点是攻击防火墙本身更加困难。^([29]) 缺点是,除非配置一个通过某种安全网络或串行控制台可以访问的网络接口,否则所有管理任务必须在防火墙的控制台上执行。由此可见,未配置 IP 地址的桥接不能作为网络的网关,也无法在桥接接口上运行任何服务。相反,你可以将桥接视为网络电缆上的一个智能凸起,它能够过滤并重定向数据。
使用作为桥接实现的防火墙时,需要注意一些通用的警告:
-
接口被设置为混杂模式,这意味着它们将接收(并在某种程度上处理)网络上的每一个数据包。
-
桥接在以太网层工作,并且默认情况下会转发所有类型的数据包,而不仅仅是 TCP/IP 数据包。
-
接口没有 IP 地址使得一些更有效的冗余功能(例如 CARP)无法使用。
配置桥接的方法在不同操作系统之间存在一些细微差别。以下示例非常基础,并未涵盖所有可能的细节,但它们应该足以让你入门。
在 OpenBSD 上的基本桥接设置
OpenBSD 的 GENERIC 内核包含了所有配置桥接和在其上过滤的必要代码。除非你编译了一个没有桥接代码的自定义内核,否则设置过程非常直接。
注意
在 OpenBSD 4.7 及更新版本中,brconfig 命令不再存在。所有桥接配置和相关功能已并入 ifconfig,适用于 OpenBSD 4.7 版本。如果你正在使用包含 brconfig 的 OpenBSD 版本,说明你正在使用过时且不再支持的配置。请尽快升级到更新的版本。
要在命令行上设置一个包含两个接口的桥接,首先需要创建桥接设备。第一种类型的设备通常会给定序号 0,因此我们使用以下命令创建 bridge0 设备:
$ **sudo ifconfig bridge0 create**
在下一个 ifconfig 命令之前,使用 ifconfig 检查预期的成员接口(在我们的例子中是 ep0 和 ep1)是否处于活动状态,但没有分配 IP 地址。接下来,通过输入以下命令来配置桥接:
$ **sudo ifconfig bridge0 add ep0 add ep1 blocknonip ep0 blocknonip ep1 up**
OpenBSD 的 ifconfig 命令本身包含了一些过滤代码。在这个例子中,我们为每个接口使用 blocknonip 选项来阻止所有非 IP 流量。
注意
OpenBSD 的 ifconfig 命令提供了自己的过滤选项,除此之外还有其他配置选项。bridge(4)和ifconfig(8)的手册页提供了更多的信息。因为它是在以太网层操作,所以可以使用ifconfig来指定过滤规则,使得桥接可以基于 MAC 地址进行过滤。通过这些过滤功能,也可以让桥接通过tagged关键字标记数据包,以便在 PF 规则集中进一步处理。对于标记功能,一个带有单个成员接口的桥接器就可以工作。
为了使配置永久生效,创建或编辑/etc/hostname.ep0并输入以下行:
up
对于另一个接口,/etc/hostname.ep1应包含相同的行:
up
最后,在/etc/hostname.bridge0中输入桥接设置:
add ep0 add ep1 blocknonip ep0 blocknonip ep1 up
现在你的桥接器应该已经启动,你可以继续创建 PF 过滤规则。
FreeBSD 上的基本桥接设置
对于 FreeBSD,过程比 OpenBSD 稍微复杂一些。为了能够使用桥接功能,你的运行中的内核必须包含if_bridge模块。默认的内核配置会构建这个模块,因此在普通情况下,你可以直接创建接口。要将桥接设备编译到内核中,请在内核配置文件中添加以下行:
device if_bridge
你也可以通过在/etc/loader.conf文件中加入以下行来在启动时加载该设备。
if_bridge_load="YES"
通过输入以下内容来创建桥接器:
$ **sudo ifconfig bridge0 create**
创建bridge0接口还会创建一组与桥接相关的sysctl值:
$ sudo sysctl net.link.bridge
net.link.bridge.ipfw: 0
net.link.bridge.pfil_member: 1
net.link.bridge.pfil_bridge: 1
net.link.bridge.ipfw_arp: 0
net.link.bridge.pfil_onlyip: 1
检查这些sysctl值是否可用是值得的。如果它们存在,说明桥接已启用。如果没有,返回检查出错原因。
注意
这些值适用于对桥接接口本身的过滤。你不需要修改它们,因为对成员接口(管道的两端)的 IP 级别过滤默认是启用的。
在执行下一个ifconfig命令之前,检查拟议的成员接口(在我们的例子中是ep0和ep1)是否已经启动,但尚未分配 IP 地址。然后输入以下内容来配置桥接器:
$ **sudo ifconfig bridge0 addm ep0 addm ep1 up**
为了使配置永久生效,将以下行添加到/etc/ rc.conf中:
ifconfig_ep0="up"
ifconfig_ep1="up"
cloned_interfaces="bridge0"
ifconfig_bridge0="addm ep0 addm ep1 up"
这意味着你的桥接器已经启动,你可以继续创建 PF 过滤规则。有关 FreeBSD 特定桥接信息,请参阅if_bridge(4)的手册页。
NetBSD 上的基本桥接设置
在 NetBSD 上,默认的内核配置没有编译过滤桥接支持。你需要编译一个自定义内核,并将以下选项添加到内核配置文件中。一旦你有了包含桥接代码的新内核,设置过程就很简单。
options BRIDGE_IPF # bridge uses IP/IPv6 pfil hooks too
在命令行上创建一个具有两个接口的桥接器,首先创建bridge0设备:
$ **sudo ifconfig bridge0 create**
在执行下一个brconfig命令之前,使用ifconfig检查拟议的成员接口(在我们的例子中是ep0和ep1)是否已经启动,但尚未分配 IP 地址。然后,输入以下内容来配置桥接器:
$ **sudo brconfig bridge0 add ep0 add ep1 up**
接下来,启用 bridge0 设备的过滤功能:
$ **sudo brconfig bridge0 ipf**
为了使配置永久生效,请创建或编辑 /etc/ifconfig.ep0 并输入以下行:
up
对于另一个接口,/etc/ifconfig.ep1 应包含相同的行:
up
最后,在 /etc/ifconfig.bridge0 中输入桥接设置:
create
!add ep0 add ep1 up
现在您的桥接应该已启动,您可以继续创建 PF 过滤规则。有关更多信息,请参见 www.netbsd.org/Documentation/network/pf.html 中的 NetBSD 文档。
桥接规则集
图 5-3 显示了一个基线规则集的pf.conf 文件,该规则集在本章开始时我们介绍的基础规则集的基础上进行了扩展。如您所见,网络稍有变化。
图 5-3. 带桥接防火墙的网络
本地网络中的计算机共享一个公共默认网关,该网关不是桥接器,但可以放置在桥接器内或外部。
ext_if = ep0
int_if = ep1
localnet= "192.0.2.0/24"
webserver = "192.0.2.227"
webports = "{ http, https }"
emailserver = "192.0.2.225"
email = "{ smtp, pop3, imap, imap3, imaps, pop3s }"
nameservers = "{ 192.0.2.221, 192.0.2.223 }"
client_out = "{ ssh, domain, pop3, auth, nntp, http, https, \
446, cvspserver, 2628, 5999, 8000, 8080 }"
udp_services = "{ domain, ntp }"
icmp_types = "{ echoreq, unreach }"
set skip on $int_if
block all
pass quick on $ext_if inet proto { tcp, udp } from $localnet \
to port $udp_services
pass log on $ext_if inet proto icmp all icmp-type $icmp_types
pass on $ext_if inet proto tcp from $localnet to port $client_out
pass on $ext_if inet proto { tcp, udp } to $nameservers port domain
pass on $ext_if proto tcp to $webserver port $webports
pass log on $ext_if proto tcp to $emailserver port $email
pass log on $ext_if proto tcp from $emailserver to port smtp
可能出现复杂得多的设置。但请记住,虽然重定向有效,但没有 IP 地址的接口无法运行任何服务。
处理来自其他地方的不可路由 IPv4 地址
即使有一个正确配置的网关来处理过滤和可能的 NAT(网络地址转换)以应对您自己的网络,您仍可能会发现自己处于不得不补偿其他人配置错误的尴尬境地。
建立全局规则
一种令人沮丧的常见配置错误类型是允许具有不可路由地址的流量访问互联网。来自不可路由 IPv4 地址的流量在几种拒绝服务(DoS)攻击技术中发挥作用,因此值得考虑显式地阻止来自不可路由地址的流量进入您的网络。这里概述了一个可能的解决方案。为确保万无一失,它还会阻止任何通过网关外部接口尝试与不可路由地址建立联系的行为。
martians = "{ 127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, \
10.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, \
0.0.0.0/8, 240.0.0.0/4 }"
block in quick on $ext_if from $martians to any
block out quick on $ext_if from any to $martians
在这里,martians 宏表示 RFC 1918 地址和其他一些 RFC 强制要求不应在开放互联网中流通的地址范围。到这些地址的流量以及来自这些地址的流量会在网关的外部接口上静默丢弃。
注意
martians 宏也可以轻松地实现为一个表格形式,这样您的规则集将有额外的表格优势。事实上,如果您查看包含此宏和规则组合的规则集中的加载规则,您会看到宏展开和规则集优化最有可能用每个规则一个表格替代了您的列表。然而,如果您自己编写表格,您可以为它选择一个更合适的名字。
如何实现这种保护的具体细节会根据你的网络配置有所不同,可能是更广泛的网络安全措施的一部分。你的网络设计可能还会决定是否包含或排除这些地址范围之外的内容。
使用锚点重构你的规则集
我们已经在多个场合提到过锚点,例如在使用锚点与运行中的 PF 配置交互的应用程序(如 FTP 代理或relayd)的上下文中。锚点是命名的子规则集,可以根据需要插入或删除规则,而无需重新加载整个规则集。
一旦你有了一个规则集,其中定义了一个平时未使用的锚点,你甚至可以使用pfctl的-a开关从命令行操作锚点内容,如下所示:
echo "block drop all" | pfctl -a baddies -f -
在这里,一个规则被插入到现有的锚点baddies中,覆盖了任何先前的内容。
你甚至可以从一个单独的文件加载规则到锚点中:
pfctl -a baddies -f /etc/anchor-baddies
或者你可以列出锚点的当前内容:
pfctl -a baddies -s rules
注意
你还会发现一些其他的 pfctl 选项对于处理锚点很有用。请参考 pfctl 手册页获取灵感。
你还可以通过将锚点的内容放入单独的文件,在规则集加载时加载这些文件,从而拆分你的配置。这样就可以单独编辑锚点中的规则,重新加载编辑后的锚点,当然,也可以进行如上所述的任何其他操作。为此,首先在pf.conf中添加如下行:
anchor ssh-good load anchor ssh-good from "/etc/anchor-ssh-good"
这引用了文件/etc/anchor-ssh-good,它可能看起来像这样:
table <sshbuddies> file "/etc/sshbuddies"
pass inet proto tcp from <sshbuddies> to any port ssh
可能只是为了让初级管理员能够委派sshbuddies表的责任,锚点从文件/etc/sshbuddies加载该表,文件内容可能是这样的:
192.168.103.84
10.11.12.13
这样,你可以通过以下方式操作锚点的内容:通过编辑文件并重新加载锚点来添加规则,通过标准输入从命令行传递其他规则来替换规则(如前面示例所示),或者通过操作它们引用的表的内容来改变锚点内部规则的行为。
注意
对于更复杂的锚点,例如以下段落中讨论的锚点,如果你希望将锚点作为单独的文件来维护,使用 include 语句可能更有用。 pf.conf 中。
前面提到的概念(指定适用于锚点内所有操作的共同标准集)在你的配置足够庞大,需要一些额外的结构化帮助时非常有用。例如,“在接口上”可能是一个有用的共同标准,用于到达特定接口的流量,因为这些流量通常具有某些相似性。例如,看看以下内容:
anchor "dmz" on $dmz_if {
pass in proto { tcp udp } to $nameservers port domain
pass in proto tcp to $webservers port { www https }
pass in proto tcp to $mailserver port smtp
pass in log (all, to pflog1) in proto tcp from $mailserver \
to any port smtp
}
一个单独的锚点ext将服务于egress接口组:
anchor ext on egress {
match out proto tcp to port { www https } set queue (qweb, qpri) set prio (5,6)
match out proto { tcp udp } to port domain set queue (qdns, qpri) set prio (6,7)
match out proto icmp set queue (q_dns, q_pri) set prio (7,6)
pass in log proto tcp to port smtp rdr-to 127.0.0.1 port spamd queue spamd
pass in log proto tcp from <nospamd> to port smtp
pass in log proto tcp from <spamd-white> to port smtp
pass out log proto tcp to port smtp
pass log (all) proto { tcp, udp } to port ssh keep state (max-src-conn 15, \
max-src-conn-rate 7/3, overload <bruteforce> flush global)
}
如果你根据接口亲和性将规则分组到锚点中,那么另一个显而易见的逻辑优化就是加入标签来帮助策略路由决策。一个简单但有效的例子可能是这样的:
anchor "dmz" on $dmz_if {
pass in proto { tcp udp } to $nameservers port domain tag GOOD
pass in proto tcp to $webservers port { www https } tag GOOD
pass in proto tcp to $mailserver port smtp tag GOOD
pass in log (all, to pflog1) in proto tcp from $mailserver
to any port smtp tag GOOD
block log quick ! tagged GOOD
}
即使这里的锚点示例都包含了在锚点内做出阻止决策,基于标签信息来决定是阻止还是放行并不一定需要在锚点内进行。
在对锚点作为结构工具的快速概览后,可能会有尝试将整个规则集转换为基于锚点的结构的冲动。如果你尝试这么做,你可能会找到让内部逻辑更清晰的方法。但不要惊讶于某些规则需要是全局性的,而不是与常见标准关联的锚点之外的规则。而且你几乎肯定会发现,最终在你的环境中有用的东西至少与我在这里呈现的场景有所不同。
你的网络有多复杂?——再探讨
在本章早期,我们提出了“你的网络有多复杂?”和“它需要有多复杂?”的问题。在本章的各个小节中,我们展示了一些工具和技术,使得可以用 PF 和相关工具构建复杂的基础设施,并帮助管理这些复杂性,同时保持网络管理员的理智。
如果你负责的站点需要应用我们在本章中提到的所有或大部分技术,我理解你的痛苦。另一方面,如果你负责的是一个如此多样化的网络,那么后续关于流量整形和资源可用性管理的章节可能对你也会很有帮助。
本书的其余部分主要讲述如何优化设置以提高性能和资源可用性,除了其中有一章稍微偏离,采用了更轻松的语气。在我们深入探讨如何优化性能并确保高可用性之前,是时候看看如何使基础设施对特定群体或个人变得不可用或难以访问。下一章专门讲述如何让那些试图滥用你所负责服务的未受洗礼的大众——或者甚至是组织严密的犯罪分子——的生活更加困难。
^([25]) 事实上,这里提到的 example.com 网络位于 192.0.2.0/24 网段,该网段在 RFC 3330 中为示例和文档用途保留。我们使用这个地址范围,主要是为了与本书中其他地方使用的 NAT 示例区分开来,后者使用的是“私有” RFC 1918 地址空间中的地址。
^([26]) 最初在 OpenBSD 4.1 中以 hoststated 名称引入,这个守护进程在多年的开发中(主要由 Reyk Floeter 和 Pierre-Yves Ritschard 完成)经历了积极的开发,包括对配置语法的一些重要更改,并且在 OpenBSD 4.3 版本中被重新命名为 relayd。
^([27]) 请参见 OpenSSL 手册页以获得更多关于加密选项的说明。
^([28]) 请参阅 PF 用户指南中的“重定向与反射”部分 (www.openbsd.org/faq/pf/rdr.html#reflect)。
^([29]) 这一措施实际增加的安全性,是在 openbsd-misc 和其他面向网络的邮件列表中偶尔激烈讨论的主题。阅读 OpenBSD 核心开发者对其优缺点的看法,既能娱乐又能启发思考。
第六章:为主动防御翻盘

在上一章中,你了解了如何确保即使在严格的包过滤规则下,你希望提供的服务仍然可以使用,并且需要花费大量时间和精力来完成。现在,在你的工作设置完成后,你很快就会注意到,一些服务可能比其他服务更容易吸引不必要的注意。
这里是一个场景:你有一个网络,配备了与你站点需求相匹配的包过滤,其中包括一些需要向外部用户提供访问的服务。不幸的是,当这些服务可用时,就存在某些人会试图利用它们进行恶作剧的风险。
你几乎肯定会通过 SSH(安全外壳协议)进行远程登录,并且你的网络上会运行 SMTP 邮件服务——这两者都是诱人的攻击目标。在本章中,我们将探讨如何通过 SSH 增加未经授权访问的难度,然后转向一些更有效的方法来阻止垃圾邮件发送者使用你的服务器。
拒绝暴力破解者
安全外壳服务(通常称为 SSH)对于 Unix 系统管理员来说是一个非常重要的服务。它通常是与机器交互的主要接口,也是脚本小子攻击的常见目标。
SSH 暴力破解攻击
如果你运行一个可以通过互联网访问的 SSH 登录服务,你可能在身份验证日志中看到了类似这样的条目:
Sep 26 03:12:34 skapet sshd[25771]: Failed password for root from 200.72.41.31 port 40992 ssh2
Sep 26 03:12:34 skapet sshd[5279]: Failed password for root from 200.72.41.31 port 40992 ssh2
Sep 26 03:12:35 skapet sshd[5279]: Received disconnect from 200.72.41.31: 11: Bye Bye
Sep 26 03:12:44 skapet sshd[29635]: Invalid user admin from 200.72.41.31
Sep 26 03:12:44 skapet sshd[24703]: input_userauth_request: invalid user admin
Sep 26 03:12:44 skapet sshd[24703]: Failed password for invalid user admin from 200.72.41.31
port 41484 ssh2
Sep 26 03:12:44 skapet sshd[29635]: Failed password for invalid user admin from 200.72.41.31
port 41484 ssh2
Sep 26 03:12:45 skapet sshd[24703]: Connection closed by 200.72.41.31
Sep 26 03:13:10 skapet sshd[11459]: Failed password for root from 200.72.41.31 port 43344 ssh2
Sep 26 03:13:10 skapet sshd[7635]: Failed password for root from 200.72.41.31 port 43344 ssh2
Sep 26 03:13:10 skapet sshd[11459]: Received disconnect from 200.72.41.31: 11: Bye Bye
Sep 26 03:13:15 skapet sshd[31357]: Invalid user admin from 200.72.41.31
Sep 26 03:13:15 skapet sshd[10543]: input_userauth_request: invalid user admin
Sep 26 03:13:15 skapet sshd[10543]: Failed password for invalid user admin from 200.72.41.31
port 43811 ssh2
Sep 26 03:13:15 skapet sshd[31357]: Failed password for invalid user admin from 200.72.41.31
port 43811 ssh2
Sep 26 03:13:15 skapet sshd[10543]: Received disconnect from 200.72.41.31: 11: Bye Bye
Sep 26 03:13:25 skapet sshd[6526]: Connection closed by 200.72.41.31
这就是一个暴力破解攻击的样子。某人或某物正试图通过暴力破解找到一个用户名和密码组合,从而让他们进入你的系统。
最简单的应对方式是编写一个pf.conf规则,阻止所有访问,但这会导致另一类问题,包括如何让合法用户访问系统。将你的sshd配置为只接受基于密钥的身份验证有帮助,但很可能无法阻止小白攻击者尝试。你可以考虑将服务移到另一个端口,但这又会带来问题,可能会导致那些在 22 端口向你发起攻击的攻击者通过扫描轻松找到 22222 端口,再次进行攻击。^([30])
从 OpenBSD 3.7(及其等效版本)开始,PF 提供了一个稍微更优雅的解决方案。
设置自适应防火墙
为了防止暴力破解攻击,你可以编写你的pass规则,限制连接主机的某些行为。为了更好地防范,你可以将违规者驱逐到一个地址表中,拒绝对这些地址的某些或全部访问。你甚至可以选择断开所有来自超出限制机器的现有连接。要启用此功能,首先通过在任何过滤规则之前将以下行添加到配置中来设置表:
table <bruteforce> persist
然后,在规则集的前面,block暴力破解者,如下所示:
block quick from <bruteforce>
最后,添加你的pass规则:
pass proto tcp to $localnet port $tcp_services \
keep state (max-src-conn 100, max-src-conn-rate 15/5, \
overload <bruteforce> flush global)
这个规则与你之前看到的例子非常相似。此处有趣的部分是括号内的内容,称为状态跟踪选项:
-
max-src-conn是允许来自单一主机的最大并发连接数。在这个例子中,它被设置为100。你可能需要根据网络的流量模式稍微调高或降低这个值。 -
max-src-conn-rate是允许来自任何单一主机的新连接速率。在此,它被设置为15个连接每 5 秒,表示为15/5。选择一个适合你配置的速率。 -
overload <bruteforce>意味着任何超出前述限制的主机地址将被加入到bruteforce表中。我们的规则集会阻止所有来自bruteforce表中地址的流量。一旦主机超过这些限制并被放入溢出表中,规则将不再匹配来自该主机的流量。确保溢出者被处理,即使只是通过默认的阻止规则或类似的方式。 -
flush global表示当主机达到限制时,它的所有连接状态将被终止(清除)。global选项意味着为了保险起见,flush适用于该主机所有由流量创建的状态,无论是哪条规则创建了该状态。
如你所想,这个小小的规则集修改会产生显著效果。在尝试几次后,暴力破解者会进入bruteforce表中。这意味着它们所有现有的连接会被终止(清除),任何新的尝试将被阻止,最可能在它们的端显示Fatal: timeout before authentication消息。你已经创建了一个自适应防火墙,它会根据网络中的条件自动调整并对不良活动作出反应。
注意
这些自适应规则仅对防御传统的、快速的暴力破解攻击有效。2008 年首次被识别并一直以来不断重复的低强度、分布式密码猜测攻击(又名The Hail Mary Cloud^([31])),不会产生符合这些规则的流量。
你可能希望在规则集中有一定的灵活性,为某些服务允许更多的连接,但在 SSH 方面可能想要更加严格。在这种情况下,你可以在规则集的前面补充一个类似下面的通用pass规则:
pass quick proto { tcp, udp } to port ssh \
keep state (max-src-conn 15, max-src-conn-rate 5/3, \
overload <bruteforce> flush global)
你应该通过阅读相关的手册页和PF 用户指南(见附录 A),找到最适合你情况的参数集。
注意
记住,这些示例规则仅作为说明,可能你的网络需求更适合其他规则。将同时连接数或连接速率设置得过低,可能会阻止合法流量。当配置中有许多主机在一个公共 NAT 网关后面,而 NAT 的主机上的用户有合法的业务需要穿越一个有严格overload规则的网关时,可能会造成自我引发的拒绝服务风险。
状态跟踪选项和overload机制并不需要仅仅应用于 SSH 服务,而且并不总是希望阻止所有来自违规者的流量。例如,你可以使用如下规则:
pass proto { tcp, udp } to port $mail_services \
keep state (max 1500, max-src-conn 100)
这里,max指定了每条规则可以创建的最大状态数,用于防止邮件或 Web 服务接收超过其处理能力的连接(请记住,加载的规则数取决于$mail_services宏的扩展内容)。一旦达到max限制,新的连接将不再匹配该规则,直到旧连接终止。或者,你可以移除max限制,给规则添加overload部分,并将违规者分配到一个带有最小带宽分配的队列中(有关设置队列的详细信息,请参见第七章中的流量整形讨论)。
一些站点使用overload实现多层次系统,其中触发一个overload规则的主机会被转移到一个或多个中间的“试用”表中,进行特别处理。在 Web 上下文中,不直接阻止来自overload表中的主机的流量,而是将这些主机的所有 HTTP 请求重定向到特定的网页,可能会很有用(就像第四章末尾的authpf示例一样)。
使用 pfctl 整理你的表
在上一节中设置了overload规则后,你现在拥有了一个自适应防火墙,可以自动检测不良行为并将违规者的 IP 地址添加到表中。观察日志和表格在短期内可能很有趣,但由于这些规则只会向表中添加内容,我们面临下一个挑战:保持表格内容的最新和相关性。
当你使用适应性规则集运行配置一段时间后,你会发现,上周由于暴力破解攻击而被overload规则阻止的某个 IP 地址,实际上是一个动态分配的地址,而这个地址现在被分配给了另一个 ISP 客户,该客户有合法的理由与你网络中的主机通信。^([32]) 如果你的适应性规则捕获了大量网络流量,你还可能发现,随着时间的推移,overload表将不断增长,占用越来越多的内存。
解决方案是 过期 表格条目——在一定时间后移除条目。在 OpenBSD 4.1 中,pfctl 获得了根据统计信息上次重置的时间来过期表格条目的能力。^([33])(在几乎所有情况下,这个重置时间等于表格条目被添加的时间。)关键词是 expire,并且表格条目的年龄以秒为单位指定。以下是一个示例:
$ **sudo pfctl -t bruteforce -T expire 86400**
此命令将移除 bruteforce 表格中,统计信息重置时间超过 86,400 秒(24 小时)以上的条目。
注
选择 24 小时作为过期时间是一个相当随意的决定。你应该选择一个你认为合理的时间值,以便任何问题在另一端能够被发现并修复。如果你已经设定了自适应规则,建议设置 crontab 条目以定期运行表格过期命令,类似于前述的命令,以确保你的表格保持最新。
给垃圾邮件发送者制造麻烦,使用 spamd
电子邮件是一个相当重要的服务,需要特别关注,因为它每天都会处理大量的垃圾邮件(spam)。当恶意软件开发者发现通过电子邮件传播蠕虫有效并开始使用电子邮件传播恶意载荷时,未经请求的商业邮件数量已经成为一个痛苦的问题。在 2000 年代初,垃圾邮件和通过电子邮件传播的恶意软件的数量已经增加到,若没有某种反垃圾邮件措施,运行一个 SMTP 邮件服务几乎变得不可想象。
反垃圾邮件措施几乎与垃圾邮件问题本身一样古老。早期的工作主要集中在分析邮件内容(称为 内容过滤),并在一定程度上对邮件的可伪造的头部进行解读,例如所谓的发件人地址(From:)或在 Received: 头部记录的中间传递的存储与转发路径。
当 OpenBSD 团队设计其反垃圾邮件解决方案 spamd 时,首次在 2003 年的 OpenBSD 3.3 中引入,开发者专注于网络层面以及 SMTP 会话中的直接通信伙伴,并结合任何关于尝试发送邮件的主机的可用信息。开发者的目标是创建一个小巧、简单且安全的程序。早期的实现几乎完全依赖于创意使用 PF 表格,并结合来自可信外部来源的数据。
注
除了 OpenBSD 的垃圾邮件延迟守护进程,基于内容过滤的反垃圾邮件软件包 SpamAssassin (spamassassin.apache.org/) 还包含一个名为 spamd 的程序。两个程序都是为了帮助抵抗垃圾邮件,但它们解决基础问题的方法完全不同,并且不会直接互操作。然而,当这两个程序都被正确配置并运行时,它们能够很好地互补。
网络层行为分析与黑名单
原始的 spamd 设计基于以下观察:垃圾邮件发送者发送大量邮件,而你成为第一个接收到某一特定邮件的人的可能性极小。此外,垃圾邮件通过一些垃圾邮件友好的网络和大量被劫持的机器发送。无论是单个邮件还是发送它们的机器都会很快报告给黑名单维护者,而由已知垃圾邮件发送者的 IP 地址构成的黑名单数据是 spamd 处理的基础。
当处理黑名单中的主机时,spamd 使用一种叫做 tarpitting 的方法。当守护进程接收到一个 SMTP 连接时,它会展示其横幅并立即切换到一个模式,在该模式下,它以每秒 1 字节的速度响应 SMTP 流量,使用一小部分 SMTP 命令确保邮件永远不会被投递,而是在邮件头被传输后被拒绝并返回到发送方的队列中。目的是尽可能浪费发送端的时间,同时对接收方几乎没有任何成本。这个特定的 tarpitting 实现,即每秒 1 字节的 SMTP 回复,通常被称为 stuttering(颤抖)。基于黑名单的 tarpitting 和 stuttering 是 spamd 在 OpenBSD 4.0 及之前版本的默认模式。
注意
在 FreeBSD 和 NetBSD 上,spamd 不是基础系统的一部分,但可以通过 ports 和 packages 以 mail/spamd* 的形式获取。如果你在 FreeBSD 或 NetBSD 上运行 PF,你需要先安装该 port 或 package,然后再按照接下来的页面中的指示进行操作。*
在黑名单模式下设置 spamd
要将 spamd 设置为传统的仅黑名单模式,你首先需要在 pf.conf 中添加一个专用的表格和相应的重定向,然后再关注 spamd 自身的 spamd.conf 文件。之后,spamd 通过该表格和重定向接入 PF 规则集。
以下是此配置的 pf.conf 配置行:
table <spamd> persist
pass in on $ext_if inet proto tcp from <spamd> to \
{ $ext_if, $localnet } port smtp rdr-to 127.0.0.1 port 8025
以下是 OpenBSD 4.7 之前的语法:
table <spamd> persist
rdr pass on $ext_if inet proto tcp from <spamd> to \
{ $ext_if, $localnet } port smtp -> 127.0.0.1 port 8025
表格 <spamd> 用于存储你从可信的黑名单源导入的 IP 地址。重定向负责处理所有来自黑名单中已存在主机的 SMTP 尝试。spamd 监听端口 8025,并对它接收到的所有 SMTP 连接进行慢速响应(每秒 1 字节),这是重定向导致的。稍后,在规则集的其他部分,你将会有一个规则,确保合法的 SMTP 流量通过并传送到邮件服务器。spamd.conf 是你指定黑名单数据源以及任何例外或本地覆盖设置的地方。
注意
在 OpenBSD 4.0 及之前版本(以及基于 OpenBSD 4.1 之前版本的 ports)中, spamd.conf 位于 /etc。从 OpenBSD 4.1 开始, spamd.conf 位于 /etc/mail。FreeBSD port 会在 /usr/local/etc/spamd/spamd.conf.sample* 中安装一个示例配置文件。*
在spamd.conf的开头附近,你会看到一行没有#注释符号,看起来像是all:\。这一行指定了你将使用的黑名单。以下是一个示例:
all:\
:uatraps:whitelist:
将你想使用的所有黑名单添加到all:\行下,每个黑名单之间用冒号(:)分隔。如果要使用白名单从黑名单中减去地址,则在每个黑名单名称后面立即添加白名单的名称,如:blacklist:whitelist:。
接下来是黑名单定义:
uatraps:\
:black:\
:msg="SPAM. Your address %A has sent spam within the last 24 hours":\
:method=http:\
:file=www.openbsd.org/spamd/traplist.gz
紧随名称(uatraps)之后,第一个数据字段指定了列表类型——在这种情况下是black。msg字段包含在 SMTP 对话过程中要显示给黑名单发件人的消息。method字段指定了spamd-setup如何获取列表数据——在这种情况下是通过 HTTP。其他可能的方式包括通过 FTP(ftp)、从挂载的文件系统中的文件(file),或通过执行外部程序(exec)获取。最后,file字段指定了spamd期望接收的文件名。
白名单的定义遵循相似的模式,但省略了消息参数:
whitelist:\
:white:\
:method=file:\
:file=/etc/mail/whitelist.txt
注意
当前默认的 spamd.conf 中的建议黑名单是积极维护的,几乎没有出现假阳性。然而,早期版本的该文件也建议使用了一些黑名单,这些黑名单排除了互联网上的大块区域,包括几个声称覆盖整个国家的地址范围。如果你的网站预计会与这些国家交换合法邮件,那么这些黑名单可能不适合你的配置。其他流行的黑名单已知会将整个/16地址范围列为垃圾邮件来源,因此,在将黑名单投入生产之前,查看该黑名单的维护政策非常值得。
将spamd的启动行和你希望的启动参数放入 OpenBSD 中的/etc/rc.conf.local,或者在 FreeBSD 或 NetBSD 中放入/etc/rc.conf。以下是一个示例:
spamd_flags="-v -b" # for normal use: "" and see spamd-setup(8)
在这里,我们启用spamd并设置它以黑名单模式运行,使用-b标志。此外,-v标志启用详细日志记录,这对于调试spamd的活动非常有用。
在 FreeBSD 上,控制spamd行为的/etc/rc.conf设置包括obspamd_enable,它应该设置为"YES"以启用spamd,以及obspamd_flags,你可以在这里填入任何spamd的命令行选项:
obspamd_enable="YES"
obspamd_flags="-v -b" # for normal use: "" and see spamd-setup(8)
注意
要使 spamd 在 OpenBSD 4.1 或更高版本中以纯黑名单模式运行,你可以通过将 spamd_black 变量设置为“YES”并重新启动 spamd 来实现相同的效果。
完成配置编辑后,使用所需的选项启动spamd,并使用spamd-setup完成配置。最后,创建一个cron作业,定期调用spamd-setup以更新黑名单。在纯黑名单模式下,你可以使用pfctl表命令查看和操作表内容。
spamd 日志
默认情况下,spamd记录到您的一般系统日志中。要将spamd日志消息发送到单独的日志文件,请向syslog.conf添加类似以下条目:
!!spamd
daemon.err;daemon.warn;daemon.info;daemon.debug /var/log/spamd
一旦您确信spamd正在运行并且正在执行其预期的操作,您可能希望将spamd日志文件添加到您的日志轮换中。在运行spamd-setup并填充表之后,您可以使用pfctl查看表内容。
注意
在本节开头的 pf.conf 片段示例中,重定向(rdr-to)*规则也是一个通过规则。如果您选择使用匹配规则(或者如果您使用较旧的 PF 版本并选择编写不包括通过部分的 rdr 规则),请确保设置一个通过规则以允许流量通过到您的重定向。您可能还需要设置规则以允许合法的电子邮件通过。但是,如果您已经在网络上运行电子邮件服务,您可能可以继续使用旧的 SMTP 通过规则。
给定一组可靠且维护良好的黑名单,spamd在纯黑名单模式下可以很好地减少垃圾邮件。然而,使用纯黑名单,您只能捕获那些已经尝试在其他地方投递垃圾邮件的主机的流量,并且您需要信任外部数据源来确定哪些主机应该被陷阱。对于提供对网络级行为更快速响应并在垃圾邮件预防方面提供一些真正收益的设置,请考虑灰名单,这是现代spamd工作的一个关键部分。
灰名单:我的管理员告诉我不要和陌生人说话
灰名单主要包括解释当前 SMTP 标准并添加一点善意谎言以使生活更轻松。
垃圾邮件发送者倾向于使用他人的设备发送他们的消息,他们未经合法所有者许可安装的软件需要相对轻量级才能在不被检测的情况下运行。与合法的邮件发送者不同,垃圾邮件发送者通常不认为他们发送的任何单个消息很重要。综合起来,这意味着典型的垃圾邮件和恶意软件发送者软件没有设置正确解释 SMTP 状态代码的功能。这是一个我们可以利用的事实,正如埃文·哈里斯在他 2003 年的论文《垃圾邮件控制战的下一步:灰名单》中提出的那样。
正如哈里斯所指出的,当被篡改的机器用于发送垃圾邮件时,发送应用程序往往只尝试一次投递,而不检查任何结果或返回代码。真正的 SMTP 实现会解释 SMTP 返回代码并根据其行动,如果初始尝试失败并出现任何临时错误,真正的邮件服务器会重试。
在他的论文中,哈里斯概述了一个实用的方法:
-
在与先前未知的通信伙伴的第一次 SMTP 联系中,不要在第一次投递尝试时接收电子邮件,而是用指示临时本地问题的状态代码回复,并存储发件人 IP 地址以供将来参考。
-
如果发送者立即重试,则如之前一样以临时失败状态代码回复。
-
如果发送者在设定的最小等待时间(例如 1 小时)后重试,但不超过最大等待时间(例如 4 小时),则接受消息并将发送者的 IP 地址记录在白名单中。
这就是灰名单的精髓。幸运的是,你可以在配备 PF 的网关上设置和维护一个灰名单 spamd。
配置灰名单模式下的 spamd
OpenBSD 的 spamd 在 OpenBSD 3.5 中获得了灰名单功能。从 OpenBSD 4.1 开始,spamd 默认在灰名单模式下运行。
在默认的灰名单模式下,用于黑名单的 spamd 表(如前一部分所述)变得多余。你仍然可以使用黑名单,但 spamd 会使用私有数据结构和 spamdb 数据库的组合来存储与灰名单相关的数据。默认模式下的 spamd 规则集通常如下所示:
table <spamd-white> persist
table <nospamd> persist file "/etc/mail/nospamd"
pass in log on egress proto tcp to port smtp \
rdr-to 127.0.0.1 port spamd
pass in log on egress proto tcp from <nospamd> to port smtp
pass in log on egress proto tcp from <spamd-white> to port smtp
pass out log on egress proto tcp to port smtp
这包括必要的 pass 规则,以便让合法的电子邮件从你自己的网络流向预定的目的地。<spamd-white> 表是由 spamd 维护的白名单。<spamd-white> 表中的主机已经通过灰名单验证,来自这些机器的邮件被允许通过,送到实际的邮件服务器或其内容过滤前端。此外,nospamd 表可以让你加载不希望暴露给 spamd 处理的主机地址,匹配的 pass 规则确保这些主机的 SMTP 流量通过。
在你的网络中,你可能希望收紧这些规则,仅允许通过 SMTP 从允许发送和接收电子邮件的主机传输 SMTP 流量。我们将在 处理不适合灰名单的站点 中再次讨论 nospamd 表。
以下是在 OpenBSD 4.7 之前语法的等效规则:
table <spamd-white> persist
table <nospamd> persist file "/etc/mail/nospamd"
rdr pass in log on egress proto tcp to port smtp \
-> 127.0.0.1 port spamd
pass in log on egress proto tcp from <nospamd> to port smtp
pass in log on egress proto tcp from <spamd-white> to port smtp
pass out log on egress proto tcp to port smtp
在 FreeBSD 上,为了在灰名单模式下使用 spamd,你需要一个文件描述符文件系统(参见 man 5 fdescfs),并将其挂载到 /dev/fd/。要实现这一点,将以下行添加到 /etc/fstab 文件中,并确保 fdescfs 代码已包含在你的内核中,或者通过适当的 kldload 命令加载模块。
fdescfs /dev/fd fdescfs rw 0 0
要开始配置 spamd,将 spamd 和你希望的启动参数放入 /etc/rc.conf.local 文件中。这里是一个示例:
spamd_flags="-v -G 2:4:864" # for normal use: "" and see spamd-setup(8)
在 FreeBSD 上,相应的行应该放在 /etc/rc.conf 中:
obspamd_flags="-v -G 2:4:864" # for normal use: "" and see spamd-setup(8)
你可以通过 spamd 命令行参数,使用 -G 选项后面的参数来微调与灰名单相关的多个参数。
为什么灰名单有效
在设计和开发过程中,已投入大量工作使得一些关键服务(例如 SMTP 邮件传输)具有容错性。实际上,这意味着,像 SMTP 这样的服务能够尽最大努力实现接近完美的邮件投递记录。这就是为什么我们可以依赖灰名单来最终接收来自合法邮件服务器的邮件。
当前的互联网邮件传输标准定义在 RFC 5321 中。^([35]) 以下是第 4.5.4.1 节“发送策略”的几段摘录:
“在一个典型的系统中,组成邮件的程序有某种方法来请求立即关注一封新的外发邮件,而那些无法立即传输的邮件必须被排队并由发件人定期重试……”
“发件人 MUST 在一次尝试失败后延迟重试某个特定的目标。通常,重试间隔 SHOULD 至少为 30 分钟;然而,当 SMTP 客户端能够确定未能传输的原因时,更复杂和多变的策略会更有益。”
“重试会继续,直到消息传输成功或发件人放弃;放弃时间通常需要至少 4 到 5 天。”
电子邮件的传递是一个协作性的最佳努力过程,RFC 明确指出,如果你试图发送邮件的站点报告它暂时无法接收邮件,那么你的责任(必需要求)是稍后再试,给接收服务器一个从问题中恢复的机会。
灰名单的巧妙之处在于它是一种方便的小谎言。当我们声称有一个临时的本地问题时,那个问题其实就是等同于“我的管理员告诉我不要与陌生人交谈。” 规范的发件人会再次尝试,但垃圾邮件发送者不会等机会重试,因为这样会增加他们发送邮件的成本。这就是为什么灰名单依然有效,而且由于它严格遵循已接受的标准,^([36]) 错误的假阳性很少见。
使用冒号分隔的列表 2:4:864 表示 passtime、greyexp 和 whiteexp 的值:
-
passtime表示spamd考虑的合理重试前的最短时间,默认值为 25 分钟,但在这里我们将其调整为 2 分钟。 -
greyexp是一个条目在灰名单状态下保留的小时数,直到它从数据库中移除。 -
whiteexp决定了一个白名单条目被保留的时间。greyexp和whiteexp的默认值分别为 4 小时和 864 小时(大约 1 个月)。
实践中的灰名单
实施灰名单的站点的用户和管理员普遍认为,灰名单有效地清除了大多数垃圾邮件,并显著减少了它们邮件内容过滤系统的负载。我们将首先查看spamd的灰名单日志记录的样子,然后返回一些数据。
如果你使用-v命令行选项启动spamd以进行详细日志记录,那么日志将包含一些额外的信息,除了 IP 地址之外。启用详细日志记录后,典型的日志摘录如下所示:
Oct 2 19:53:21 delilah spamd[26905]: 65.210.185.131: connected (1/1), lists: spews1
Oct 2 19:55:04 delilah spamd[26905]: 83.23.213.115: connected (2/1)
Oct 2 19:55:05 delilah spamd[26905]: (GREY) 83.23.213.115: <gilbert@keyholes.net> ->
<wkitp98zpu.fsf@datadok.no>
Oct 2 19:55:05 delilah spamd[26905]: 83.23.213.115: disconnected after 0 seconds.
Oct 2 19:55:05 delilah spamd[26905]: 83.23.213.115: connected (2/1)
Oct 2 19:55:06 delilah spamd[26905]: (GREY) 83.23.213.115: <gilbert@keyholes.net> ->
<wkitp98zpu.fsf@datadok.no>
Oct 2 19:55:06 delilah spamd[26905]: 83.23.213.115: disconnected after 1 seconds.
Oct 2 19:57:07 delilah spamd[26905]: (BLACK) 65.210.185.131: <bounce-3C7E40A4B3@branch15.
summer-bargainz.com> -> <adm@dataped.no>
Oct 2 19:58:50 delilah spamd[26905]: 65.210.185.131: From: Auto lnsurance Savings <noreply@
branch15.summer-bargainz.com>
Oct 2 19:58:50 delilah spamd[26905]: 65.210.185.131: Subject: Start SAVlNG M0NEY on Auto
lnsurance
Oct 2 19:58:50 delilah spamd[26905]: 65.210.185.131: To: adm@dataped.no
Oct 2 20:00:05 delilah spamd[26905]: 65.210.185.131: disconnected after 404 seconds. lists:
spews1
Oct 2 20:03:48 delilah spamd[26905]: 222.240.6.118: connected (1/0)
Oct 2 20:03:48 delilah spamd[26905]: 222.240.6.118: disconnected after 0 seconds.
Oct 2 20:06:51 delilah spamd[26905]: 24.71.110.10: connected (1/1), lists: spews1
Oct 2 20:07:00 delilah spamd[26905]: 221.196.37.249: connected (2/1)
Oct 2 20:07:00 delilah spamd[26905]: 221.196.37.249: disconnected after 0 seconds.
Oct 2 20:07:12 delilah spamd[26905]: 24.71.110.10: disconnected after 21 seconds. lists:
spews1
第一行是来自spews1黑名单中机器的连接开始。接下来的六行显示了来自另一台机器的两次连接尝试的完整记录,每次它都作为第二个活跃连接进行连接。这台机器还没有被列入任何黑名单,因此被列为灰名单。请注意,灰名单机器尝试投递的邮件中有一个相当奇怪的收件地址(wkitp98zpu.fsf@datadok.no)。这里有一个有用的技巧,我们将在灰名单陷阱中讨论。地址前面的(GREY)和(BLACK)标记表示灰名单或黑名单状态。接着,我们看到来自黑名单主机的更多活动,稍后我们看到,在 404 秒(即 6 分钟 44 秒)后,黑名单主机放弃了投递。
其余的几行显示了一些非常短的连接,包括一台已经被列入黑名单的机器。不过这次,机器断开连接太快,无法在 SMTP 对话的开头看到任何(BLACK)标记,但我们在末尾看到了对列表名称(spews1)的引用。
粗略估计,大约 400 秒是天真被列入黑名单的垃圾邮件发送者停留的时间(根据来自不同网站的数据),也是完成EHLO ...对话直到spamd拒绝邮件所需的时间(按每秒 1 字节的速率)。然而,在查看日志时,你可能会发现一些垃圾邮件发送者停留的时间明显更长。例如,在我们办公室网关的数据中,有一个日志条目格外引人注目:
Dec 11 23:57:24 delilah spamd[32048]: 69.6.40.26: connected (1/1), lists:
spamhaus spews1 spews2
Dec 12 00:30:08 delilah spamd[32048]: 69.6.40.26: disconnected after 1964
seconds. lists: spamhaus spews1 spews2
这台特定的机器在 12 月 9 日至 12 月 12 日之间进行过 13 次投递尝试时,已经被列入了多个黑名单。最后一次尝试持续了 32 分钟 44 秒,仍未完成投递。相对聪明的垃圾邮件发送者通常会在前几秒钟内就断开连接,就像第一个日志片段中的那些一样。其他一些则在大约 400 秒后放弃。有些则坚持几个小时。(我们记录的最极端情况是坚持了 42,673 秒,接近 12 小时。)
跟踪你实际的邮件连接:spamlogd
在幕后,鲜为人知且几乎没有文档记录的,是spamd最重要的辅助程序之一:spamlogd白名单更新程序。顾名思义,spamlogd在后台安静地工作,记录进出你的邮件服务器的连接,以保持白名单的更新。其目的是确保在你与常联系的主机之间发送的有效邮件能够顺利通过,且不出现太多麻烦。
注意
如果你已经跟随讨论到此为止,spamlogd可能已经被自动启动了。然而,如果你的初始spamd配置没有包含灰名单功能,spamlogd可能没有启动,你可能会遇到一些奇怪的现象,比如灰名单和白名单没有被正确更新。在启用灰名单后重新启动spamd应该能确保spamlogd被加载并可用。
为了正确执行其工作,spamlogd需要你记录进出邮件服务器的 SMTP 连接,就像我们在第五章的示例规则集中所做的那样:
emailserver = "192.0.2.225"
pass log proto tcp to $emailserver port $email
pass log proto tcp from $emailserver to port smtp
在 OpenBSD 4.1 及更高版本(以及等效版本)上,你可以创建多个pflog接口,并指定规则应该记录到哪里。以下是如何将spamlogd需要读取的数据与其他 PF 日志分开的方法:
-
使用
ifconfig pflog1 create创建一个单独的pflog1接口,或者创建一个仅包含up这一行的hostname.pflog1文件。 -
将规则更改为以下内容:
pass log (to pflog1) proto tcp to $emailserver port $email pass log (to pflog1) proto tcp from $emailserver to port smtp -
将
-l pflog1添加到spamlogd的启动参数中。
这将spamd相关的日志记录与其他日志分开。(有关日志记录的更多信息,请参见第九章。)
在前面的规则生效后,spamlogd将把接收你发送的邮件的 IP 地址添加到白名单中。这并不是一个铁定保证回复邮件会立即通过,但在大多数配置下,它能显著加速邮件处理过程。
灰色拦截
我们知道,垃圾邮件发送者很少使用完全合规的 SMTP 实现来发送他们的邮件,这也是灰名单技术有效的原因之一。我们还知道,垃圾邮件发送者很少检查他们传给被劫持机器的地址是否实际上是可投递的。结合这两个事实,你会发现,如果一个被灰名单列入的机器尝试向你域中的无效地址发送邮件,那么该邮件很有可能是垃圾邮件或恶意软件。
这一认识促使spamd开发的下一个进化步骤——一种被称为灰色拦截的技术。当一个被灰名单列入的主机尝试向我们域中的已知无效地址发送邮件时,该主机将被添加到本地维护的黑名单spamd-greytrap中。spamd-greytrap列表中的成员将像其他黑名单成员一样,遭遇每秒 1 字节的延迟。
spamd中实现的灰色陷阱简单而优雅。你需要的主要启动条件是spamd处于灰名单模式。另一个关键组件是你服务器处理邮件的域中所有地址的列表,但只能包含那些你确定永远不会接收合法邮件的地址。列表中的地址数量不重要,但必须至少有一个,最多的限制主要由你希望添加的地址数量决定。
接下来,你使用spamdb将你的列表提供给灰色陷阱功能,并坐下来观看。首先,发送者尝试向你灰名单中的地址发送邮件,并被简单地加入灰名单,就像任何你之前没有交换过邮件的发送者一样。如果同一台机器再次尝试,无论是发送到同一个无效地址,还是另一个在灰名单中的地址,灰色陷阱将被触发,违规者会被放入spamd-greytrap,持续 24 小时。在接下来的 24 小时内,来自被灰色陷阱地址的任何 SMTP 流量将被延迟,每次只回复 1 个字节。
那个 24 小时的时间段足够短,不会对合法流量造成严重干扰,因为真实的 SMTP 实现会至少持续几天尝试发送。大规模实施该技术的经验表明,它很少产生误报。在 24 小时后继续发送垃圾邮件的机器很快就会重新进入陷阱。
要设置你的陷阱列表,请使用spamdb的-T选项。在我的例子中,之前在实践中的灰名单中提到的那个奇怪地址^([37])是一个自然的候选地址:
$ **sudo spamdb -T -a wkitp98zpu.fsf@datadok.no**
我实际输入的命令是$ sudo spamdb -T -a "<wkitp98zpu.fsf@datadok.no>"。在 OpenBSD 4.1 及更高版本中,spamdb不需要尖括号或引号,但它仍然可以接受这些符号。
你可以添加任意多的地址。我通常通过在灰名单和邮件服务器日志中查找尝试发送失败的报告到我域中不存在的地址来找到新的垃圾邮件陷阱地址(是的,听起来确实像疯了一样)。
警告
确保你添加到垃圾邮件陷阱列表中的地址是无效的,并且将始终保持无效。没有什么比发现你将一个有效地址变成垃圾邮件陷阱更让人尴尬的了,哪怕它是暂时的。
以下日志片段显示了一台发送垃圾邮件的机器如何在第一次联系时被加入灰名单,然后它试图笨拙地向我添加到陷阱列表中的地址发送邮件,几分钟后最终被加入spamd-greytrap黑名单。我们知道它接下来大约 20 个小时会做什么。
Nov 6 09:50:25 delilah spamd[23576]: 210.214.12.57: connected (1/0)
Nov 6 09:50:32 delilah spamd[23576]: 210.214.12.57: connected (2/0)
Nov 6 09:50:40 delilah spamd[23576]: (GREY) 210.214.12.57: <gilbert@keyholes.net> ->
<wkitp98zpu.fsf@datadok.no>
Nov 6 09:50:40 delilah spamd[23576]: 210.214.12.57: disconnected after 15 seconds.
Nov 6 09:50:42 delilah spamd[23576]: 210.214.12.57: connected (2/0)
Nov 6 09:50:45 delilah spamd[23576]: (GREY) 210.214.12.57: <bounce-3C7E40A4B3@branch15.summerbargainz.
com> -> <adm@dataped.no>
Nov 6 09:50:45 delilah spamd[23576]: 210.214.12.57: disconnected after 13 seconds.
Nov 6 09:50:50 delilah spamd[23576]: 210.214.12.57: connected (2/0)
Nov 6 09:51:00 delilah spamd[23576]: (GREY) 210.214.12.57: <gilbert@keyholes.net> ->
<wkitp98zpu.fsf@datadok.no>
Nov 6 09:51:00 delilah spamd[23576]: 210.214.12.57: disconnected after 18 seconds.
Nov 6 09:51:02 delilah spamd[23576]: 210.214.12.57: connected (2/0)
Nov 6 09:51:02 delilah spamd[23576]: 210.214.12.57: disconnected after 12 seconds.
Nov 6 09:51:02 delilah spamd[23576]: 210.214.12.57: connected (2/0)
Nov 6 09:51:18 delilah spamd[23576]: (GREY) 210.214.12.57: <gilbert@keyholes.net> ->
<wkitp98zpu.fsf@datadok.no>
Nov 6 09:51:18 delilah spamd[23576]: 210.214.12.57: disconnected after 16 seconds.
Nov 6 09:51:18 delilah spamd[23576]: (GREY) 210.214.12.57: <bounce-3C7E40A4B3@branch15.summerbargainz.
com> -> <adm@dataped.no>
Nov 6 09:51:18 delilah spamd[23576]: 210.214.12.57: disconnected after 16 seconds.
Nov 6 09:51:20 delilah spamd[23576]: 210.214.12.57: connected (1/1), lists: spamd-greytrap
Nov 6 09:51:23 delilah spamd[23576]: 210.214.12.57: connected (2/2), lists: spamd-greytrap
Nov 6 09:55:33 delilah spamd[23576]: (BLACK) 210.214.12.57: <gilbert@keyholes.net> ->
<wkitp98zpu.fsf@datadok.no>
Nov 6 09:55:34 delilah spamd[23576]: (BLACK) 210.214.12.57: <bounce-3C7E40A4B3@branch15.
summer-bargainz.com> -> <adm@dataped.no>
顺便说一下,尽管垃圾邮件发送者换了机器发送邮件,但From:和To:地址保持不变。事实证明,他仍然尝试发送到一个从未能成功传递的地址,这强烈表明这个垃圾邮件发送者并没有频繁检查自己的列表。
使用 spamdb 管理列表
有时你可能需要查看或更改黑名单、白名单和灰名单的内容。这些记录位于/var/db/spamdb数据库中,管理员管理这些列表的主要接口是spamdb。
早期版本的spamdb只是提供了将白名单条目添加到数据库或更新现有条目的选项(spamdb -a nn.mm.nn.mm)。你可以删除白名单条目(spamdb -d nn.mm.nn.mm),以弥补黑名单的缺陷或灰名单算法的影响。近期版本的spamdb提供了一些有趣的功能来支持灰陷阱。
更新列表
如果你运行spamdb而不带任何参数,它将列出你的spamdb数据库的内容,并允许你添加或删除垃圾邮件陷阱地址和陷阱列表条目。你还可以动态地添加白名单条目。
如果你想将主机添加到白名单中,而不将其添加到永久的nospamd文件中,并且不重新加载规则集或表格,你可以直接通过命令行执行此操作,方法如下:
$ sudo spamdb -a 213.187.179.198
如果一个垃圾邮件发送者尽管你做出了最大努力,仍然成功地发送了消息,你可以通过将该垃圾邮件发送者添加到spamd-greytrap列表来纠正这一情况,方法如下:
$ sudo spamdb -a -t 192.168.2.128
添加新的陷阱地址同样简单:
$ sudo spamdb -a -T _-medvetsky@ehtrib.org
如果你想撤销其中任何一个决定,你只需将这两个命令中的-a选项替换为-d选项即可。
保持 spamdb 灰名单同步
从 OpenBSD 4.1 开始,spamd可以在任意数量的协作灰名单网关之间保持灰名单数据库的同步。其实现是通过一组spamd命令行选项:
-
-Y选项指定一个同步目标,即你希望通知其灰名单信息更新的其他spamd运行网关的 IP 地址。 -
在接收端,
-y选项指定一个同步监听器,即该spamd实例准备接收来自其他主机的灰名单更新的地址或接口。
例如,我们的主要spamd网关mainoffice-gw.example.com可能会在其启动命令行中添加以下选项,分别用于建立同步目标和同步监听器:
-Y minorbranch-gw.example.com -y mainoffice-gw.example.com
相反,位于分支办公室的minorbranch-gw.example.com的主机名会是反转的:
-Y mainoffice-gw.example.com -y minorbranch-gw.example.com
spamd守护进程还支持同步伙伴之间的共享密钥认证。具体来说,如果你创建了文件/etc/mail/spamd.key并将其分发给所有同步伙伴,它将用于计算认证所需的校验和。spamd.key文件本身可以是任何类型的数据,比如从/dev/arandom中采集的随机数据,正如spamd的手册页所建议的那样。
注意
在无法直接同步与spamd相关的数据的情况下,或者如果你只是想与他人共享你的spamd-greytrap,将本地陷阱垃圾邮件发送者的列表导出到文本文件中可能是可取的。spamd-setup期望的列表格式是每行一个地址,评论行可以选择性地以一个或多个#字符开头。以可用格式导出当前被拦截地址的列表,可以像拼凑一个spamdb、grep和一些想象力的单行命令一样简单。
检测无序 MX 使用
OpenBSD 4.1 为spamd增加了检测无序 MX 使用的功能。首先联系辅助邮件交换服务器而不是主邮件交换服务器是垃圾邮件发送者常用的技巧,且这种做法与我们期望的普通电子邮件传输代理行为相违背。换句话说,如果有人以错误的顺序尝试邮件交换服务器,我们可以相当确定他们是在尝试发送垃圾邮件。
对于我们的example.com域,主邮件服务器为 192.0.2.225,备份邮件服务器为 192.0.2.224,在spamd的启动选项中添加-M 192.0.2.224意味着任何在联系主邮件服务器 192.0.2.225 之前尝试通过 SMTP 联系 192.0.2.224 的主机将在接下来的 24 小时内被加入到本地的spamdgreytrap列表中。
处理与灰名单不兼容的站点
不幸的是,有些情况你需要为其他站点的电子邮件设置的特殊性进行补偿。
从任何没有与你联系过的站点发送的第一封电子邮件,将会被延迟一段随机时间,这段时间主要取决于发件人的重试间隔。有时候,即使是最小的延迟也不可接受。例如,如果你有一些不常联系的客户,他们要求你在联系时立即并紧急地处理他们的事务,那么初次交付的延迟可能长达几个小时,这样的延迟可能并不理想。此外,你还可能遇到配置错误的邮件服务器,它们可能根本不重试,或者重试得太快,甚至可能在只尝试一次后就停止重试。
此外,一些站点足够大,拥有多个外发 SMTP 服务器,它们与灰名单不兼容,因为它们不能保证会从之前尝试交付时使用的相同 IP 地址重试交付。尽管这些站点遵守了重试要求,但显然这是灰名单的几个剩余缺点之一。
补偿这种情况的一种方法是定义一个本地白名单表,从文件中加载,以应对重启情况。为了确保来自表中地址的 SMTP 流量不被传递到spamd,可以添加一个pass规则,允许该流量通过:
table <nospamd> persist file "/etc/mail/nospamd"
pass in log on egress proto tcp from <nospamd> to port smtp
在 OpenBSD 4.7 之前的语法中,需在重定向块的顶部添加一个no rdr规则,并添加一个匹配的pass规则,以便让来自nospamd表中主机的 SMTP 流量通过,如下所示:
no rdr proto tcp from <nospamd> to $mailservers port smtp
pass in log on egress proto tcp from <nospamd> to port smtp
在对规则集进行这些更改后,将需要保护免受重定向的地址输入到/etc/mail/nospamd文件中。然后使用pfctl -f /etc/pf.conf重新加载规则集。接下来,你可以对<nospamd>表使用所有预期的表技巧,包括在编辑nospamd文件后替换其内容。事实上,这种方法在spamd的 man 页面和样本配置文件中都有强烈提示。
至少一些具有多个外发 SMTP 服务器的网站会发布有关哪些主机被允许通过发送者策略框架(SPF)记录为其域发送电子邮件的信息,作为域 DNS 信息的一部分。^([38]) 要检索example.com域的 SPF 记录,可以使用host命令的-ttxt选项,如下所示:
$ **host -ttxt example.com**
此命令将产生类似以下的答案:
example.com descriptive text "v=spf1 ip4:192.0.2.128/25 -all"
这里,引用中的文本是example.com域的 SPF 记录。如果你希望来自example.com的电子邮件快速到达,并且你相信那里的人员不会发送或转发垃圾邮件,可以选择 SPF 记录中的地址范围,将其添加到nospamd文件中,并从更新的文件重新加载<nospamd>表内容。
反垃圾邮件技巧
当选择性地使用时,结合spamd的黑名单是强大、精确且高效的反垃圾邮件工具。spamd机器的负载非常小。另一方面,spamd的表现永远不会好于其最弱的数据源,这意味着你需要监控日志,并在必要时使用白名单。
也可以在纯灰名单模式下运行spamd,不使用黑名单。事实上,一些用户报告称,纯灰名单的spamd配置与使用黑名单的配置在反垃圾邮件方面同样有效,有时比内容过滤更加有效。一项发布在openbsd-misc上的报告称,纯灰名单配置立即使该公司消除了大约 95%的垃圾邮件负载。(这份报告可以通过marc.info/等地方访问;搜索主题“Followup – spamd greylisting results。”)
我推荐两个非常好的黑名单。一个是基于“过去的新闻组帖子幽灵”的 Bob Beck 的陷阱列表。由阿尔伯塔大学运行spamd的计算机自动生成,Bob 的设置是一个定期自动删除被困地址的spamd系统,这意味着您会获得极低数量的误报。主机数量差异很大,最高曾达到 67 万。虽然仍处于测试阶段,但该列表于 2006 年 1 月公开。该列表可从www.openbsd.org/spamd/traplist.gz获取。它是最近示例spamd.conf文件中的uatraps黑名单的一部分。
我推荐的另一个列表是heise.de的nixspam,它具有 12 小时的自动过期时间和极高的准确性。它也在示例spamd.conf文件中。有关此列表的详细信息可从www.heise.de/ix/nixspam/dnsbl_en/获取。
一旦您对您的设置感到满意,请尝试引入本地灰色陷阱。这可能会捕捉到更多不受欢迎的人,并且这是一种有趣、干净的乐趣。一些有限的实验—在撰写本章时进行(在bsdly.blogspot.com/中以bsdly.blogspot.com/2007/07/hey-spammer-heres-list-for-you.html开头的条目中记录)—甚至表明,从您的邮件服务器日志、spamd日志或直接从您的灰名单中收集垃圾邮件使用的无效地址放入您的陷阱列表是非常有效的。将列表发布在一个适度可见的网页上似乎可以确保您放在那里的地址将一遍又一遍地被地址收集机器人记录,并且将为您提供更好的灰色陷阱材料,因为它们更有可能被保留在垃圾邮件发送者已知良好地址列表上。
^([30]) 在撰写本章时,这仅仅是理论性的;我还没有收到任何可靠的报告表明这种情况正在发生。这在 2012 年发生了变化,当可靠的消息来源开始报告在奇怪的端口出现暴力序列。更多信息请参见bsdly.blogspot.com/2013/02/theres-no-protection-in-high-ports.html。
^([31]) 要了解“Hail Mary Cloud”暴力尝试序列的概述,请参阅文章“Hail Mary Cloud and the Lessons Learned” bsdly.blogspot.com/2013/10/the-hail-mary-cloud-and-lessons-learned.html。更多资源在那里和附录 A 中有引用。
^([32]) 从长期来看,整个网络和更大范围的 IP 地址被重新分配给新所有者是相当正常的,通常是响应于物理世界或商业领域的事件。
^([33]) 在 pfctl 获取过期表项的能力之前,表项过期通常由专用工具 expiretable 处理。如果你的 pfctl 没有 expire 选项,你应该认真考虑升级到更新的系统。如果升级由于某种原因不切实际,可以在你的包管理系统中寻找 expiretable。
^([34]) 原始的 Harris 论文以及其他一些有用的文章和资源可以在 www.greylisting.org/ 找到。
^([35]) RFC 5321 的相关部分与 RFC 2821 的相应部分相同,后者已废弃。我们中的一些人对于 IETF 没有澄清这些文本内容而感到有些失望,尽管该标准现已向前推进。我的反应(实际上,这是一次相当激烈的抱怨)可以在 bsdly.blogspot.com/2008/10/ietf-failed-to-account-for-greylisting.html 中看到。
^([36]) 相关的 RFC 主要是 RFC 1123 和 RFC 5321,它们废弃了早期的 RFC 2821。请记住,临时拒绝是 SMTP 容错特性的一部分。
^([37]) 当然,这个地址完全是假的。它看起来像是 GNUS 邮件和新闻客户端生成的消息 ID,可能是从新闻存档或某个不幸的恶意软件受害者的邮箱中窃取的。
^([38]) SPF 记录以 TXT 记录的形式存储在 DNS 区域中。详情请参见 www.openspf.org/。
第七章 使用队列和优先级进行流量整形

在本章中,我们将探讨如何使用流量整形来高效地分配带宽资源,并根据特定策略进行调整。如果“流量整形”这个术语对你来说有些陌生,请放心,它的意思正如你所理解的那样:你将修改网络分配资源的方式,以满足用户及其应用程序的需求。通过正确理解你的网络流量以及产生这些流量的应用程序和用户,你实际上可以通过优化网络来实现“更大、更好、更快、更强”的目标,尽管只针对网络中实际需要传输的流量进行优化。
有一套小巧而强大的流量整形工具可供你使用;所有这些工具通过在你的网络设置中引入非默认行为来调整网络现实,以符合你的需求。针对 PF 上下文的流量整形目前有两种方式:一种是曾经实验性的ALTQ(alternate queuing,即“替代队列”)框架,经过约 15 年的使用后,现在被视为旧式方法;另一种是 OpenBSD 5.5 引入的较新的 OpenBSD 优先级和队列系统。
本章的第一部分通过观察新的 OpenBSD 优先级和队列系统的特点来介绍流量整形。如果你准备在 OpenBSD 5.5 或更高版本上进行设置,可以直接进入下一部分,始终开启的优先级和队列用于流量整形。在这一部分,我们将介绍流量整形的主要概念,并通过示例进行说明。
在 OpenBSD 5.4 及更早版本,以及其他一些 BSD 系统中,PF 代码尚未与 OpenBSD 5.5 保持同步,因此流量整形由 ALTQ 系统负责。在 OpenBSD 中,ALTQ 在经过一次过渡发布后被移除,从 OpenBSD 5.6 开始,只有较新的流量整形系统得以保留。如果你有兴趣将现有的 ALTQ 设置迁移到新系统,你可能会发现从 ALTQ 到优先级和队列的过渡部分很有用;这一节重点介绍了旧版 ALTQ 系统与新系统之间的差异。
如果你正在使用一个尚未支持 OpenBSD 5.5 引入的队列系统的操作系统,你需要研究 ALTQ 流量整形子系统,相关内容可参见使用 ALTQ 引导流量。如果你正在学习流量整形的概念并希望将其应用于 ALTQ 设置,请在深入 ALTQ 特定配置细节之前阅读本章的第一部分。
始终开启的优先级和队列用于流量整形
管理带宽与平衡支票簿或处理其他有限资源有很多相似之处。这些资源持续供应,且有严格的上限,你需要根据政策或规格中设定的优先级,以最大效率分配资源。
OpenBSD 5.5 及更新版本通过 PF 规则集中的分类机制提供了几种不同的带宽资源管理选项。我们将首先看看如何仅使用流量优先级来管理流量,然后再讨论如何通过为你的流量分配定义的子集到队列来细分带宽资源。
注意
始终启用的优先级是作为 OpenBSD 5.0 中的一种预告功能引入的。经过几年的开发和测试,新的队列系统最终在 OpenBSD 5.5 中正式提交,并于 2014 年 5 月 1 日发布。如果你从 OpenBSD 5.5 或更新版本开始流量整形,或者打算这么做,本节是一个合适的起点。如果你从早期版本的 OpenBSD 升级或从其他 ALTQ 系统迁移到最新版本的 OpenBSD,你很可能会发现接下来的部分,从 ALTQ 到优先级和队列的过渡,非常有用。
通过设置流量优先级进行整形
如果你主要是希望将某些类型的流量推到其他流量之前,你可以通过简单地设置优先级来实现:为一些项目分配更高的优先级,使它们在其他流量之前获得处理。
prio 优先级方案
从 OpenBSD 5.0 开始,可以基于每条规则对网络流量进行分类的优先级方案。优先级范围从 0 到 7,其中 0 为最低优先级。被分配优先级 7 的项目会优先于其他所有流量,默认值 3 会自动分配给大多数类型的流量。该优先级方案通常称为prio,在 PF 语法关键字之后最常见,你可以通过match或pass规则设置优先级来调整你的流量。
例如,为了最大化你的出站 SSH 流量,你可以在配置中添加如下规则:
pass proto tcp to port ssh set prio 7
然后你的 SSH 流量将优先处理。
然后你可以检查其余的规则集,决定哪些流量更重要,哪些流量你希望始终能够到达目的地,哪些部分流量你认为不那么重要。
为了将 Web 流量推到所有流量之前,并提升网络时间和名称服务的优先级,你可以在配置中添加类似这样的规则:
pass proto tcp to port { www https } set prio 7
pass proto { udp tcp } to port { domain ntp } set prio 6
或者,如果你已经有一个包含符合除端口以外其他标准的规则集,你可以通过将优先级流量整形写成match规则来实现类似的效果:
match proto tcp to port { www https } set prio 7
match proto { udp tcp } to port { domain ntp } set prio 6
在某些网络中,时间敏感型流量,如语音网络协议(VoIP),可能需要特殊处理。对于 VoIP,像这样的优先级设置可能改善电话通话质量:
voip_ports="{ 2027 4569 5036 5060 10000:20000 }"
match proto udp to port $voip_ports set prio 7
但请检查你的 VoIP 应用程序的文档,了解它使用的具体端口。无论如何,像这样的match规则不仅能对你的配置产生积极影响:你可以像这里的例子那样使用match规则,将过滤决策——如通过、阻塞或重定向——与流量整形决策分开,并通过这种分离,你很可能会得到一个更易读和易维护的配置。
同样值得注意的是,OpenBSD 网络栈的某些部分为开发者认为对网络功能至关重要的特定类型流量设置了默认优先级。如果你没有设置任何优先级,那么任何带有proto carp和其他一些管理协议及数据包类型的流量将按照优先级 6 处理,而所有没有通过set prio规则获得具体分类的流量,将有一个默认优先级 3。
双优先级加速技巧
在刚才展示的例子中,我们为不同类型的流量设置了不同的优先级,并成功地使特定类型的流量(如 VoIP 和 SSH)比其他流量更快地传输。但得益于 TCP 的设计,尽管它承载了大部分流量,即使是一个简单的优先级整形方案,通过对规则集进行少量调整,也能提供更多的优化空间。
正如 RFC 的读者和一些实践者所发现的那样,TCP 的面向连接设计意味着,对于每个发送的数据包,发送方都期望在预设的时间内或在定义的“窗口”序列号范围内收到一个确认(ACK)数据包。如果发送方在预期的限制时间内没有收到确认,它会假设数据包在传输过程中丢失,并安排重新发送数据。
另一个需要考虑的重要因素是,默认情况下,数据包按到达的顺序处理。这被称为先进先出(FIFO),这意味着基本没有数据的 ACK 数据包会在较大的数据包之间等待它们的顺序。在繁忙或拥塞的链路上,这正是流量整形变得有趣的地方,等待 ACK 并执行重传会明显消耗有效带宽,减慢所有传输的速度。事实上,双向并发传输可能会相互显著减慢,超过它们预计数据大小的值。^([39])
幸运的是,这个问题有一个简单且相当流行的解决方案:你可以使用优先级确保那些较小的数据包提前通过。如果你在match或pass规则中分配两个优先级,例如这样:
match out on egress set prio (5, 6)
第一优先级将分配给常规流量,而 ACK 数据包和其他低延迟类型的服务(ToS)数据包将分配第二优先级,并将比常规数据包更快地处理。
当一个数据包到达时,PF 会检测 ACK 数据包并将其放入更高优先级的队列。PF 还会检查到达数据包的 ToS 字段。那些设置为低延迟的 ToS(表示发送方希望更快速地传输数据)的数据包也会获得更高优先级的处理。当多个优先级被指示时,如前面的规则所示,PF 会相应地分配优先级。其他 ToS 值的数据包将按到达顺序处理,但由于 ACK 数据包更快到达,发送方将花更少的时间等待 ACK 并重新发送假定丢失的数据。最终结果是可用带宽得到了更高效的利用。(这里引用的 match 规则是我写的第一条规则,用来熟悉新的 prio 功能——当然是在测试系统上——这个功能是在 OpenBSD 5.0 开发周期中提交的。如果你将这条 match 规则放到现有的规则集上,你可能会看到链接能够承载更多的流量和更多的并发连接,直到出现明显的拥塞症状。)
在你将两级优先级技巧引入流量整形之前,看看你是否能找到一种方法来测量引入前后的流量情况,并记录下差异,再继续进行更复杂的流量整形选项。
引入带宽分配队列
我们已经看到,仅使用优先级的流量整形可以非常有效,但也会有一些情况,单纯依赖优先级的方案无法达到你的目标。一个这样的场景是,当你面临一些需求时,最有用的解决方案是为某些类型的流量分配更高的优先级,也许还需要更大的带宽份额,例如电子邮件和其他高价值的服务,同时对其他类型的流量分配较少的带宽。另一个这样的场景是,当你只是想将可用的带宽分配给不同大小的块以供特定服务使用,可能还需要为某些类型的流量设置硬性的上限,同时确保你关心的所有流量至少能获得其公平的带宽份额。在这种情况下,你将放弃纯优先级方案,至少不会把它作为主要工具,而是开始使用队列进行实际的流量整形。
与优先级级别不同,优先级级别是始终可用的,可以在任何规则中使用而无需进一步准备,而队列代表你可用带宽的特定部分,只有在你根据可用容量定义它们之后,才可以在规则中使用。队列是网络数据包的缓冲区。队列被定义为具有特定带宽量,或作为可用带宽的特定部分,你可以将每个队列带宽份额的一部分分配给子队列,或将队列嵌套在其他队列中,共享父队列的资源。数据包会被保存在队列中,直到它们根据队列的标准被丢弃或发送,并受到队列可用带宽的限制。队列附加到特定接口,带宽是按接口管理的,每个接口的可用带宽会细分到你定义的队列中。
定义队列的基本语法遵循以下模式:
queue *name* on *interface* bandwidth *number [ ,K,M,G]*
queue *name1* parent *name* bandwidth *number[ ,K,M,G]* default
queue *name2* parent *name* bandwidth *number[ ,K,M,G]*
queue *name3* parent *name* bandwidth *number[ ,K,M,G]*
带宽数字后面的字母表示测量单位:K表示千比特;M表示兆比特;G表示千兆比特。当你只写带宽数字时,它会被解释为每秒比特数。你还可以在这个基本语法上添加其他选项,正如我们在后面的示例中将看到的。
注意
子队列定义会命名它们的父队列,并且必须有一个队列作为默认队列,用于接收任何未特别分配到其他队列的流量。
一旦队列定义完成,你可以通过重写pass或match规则,将流量分配到特定的队列中,从而将流量整形集成到规则集中。
你的总可用带宽是多少?
一旦我们开始处理总带宽的定义部分,而不是依靠某种方式共享整个带宽的优先级,确定你总可用带宽的准确值就变得非常有趣。确定特定接口的实际可用带宽用于队列可能比较困难。如果你没有指定总带宽,那么将使用可用的总带宽来计算分配值,但某些类型的接口无法可靠地报告实际带宽值。一个常见的例子是,如果你的网关的外部接口是一个 100 兆比特(Mb)以太网接口,并连接到一条仅提供 8Mb 下载和 1Mb 上传速度的 DSL 线路。^([40]) 这个以太网接口将自信地报告 100Mb 带宽,而不是面向互联网连接的实际值。
因此,通常最好将总带宽设置为一个固定值。不幸的是,使用的值可能与带宽供应商告诉你的可用带宽值不完全相同,因为各种技术和实现总会有一些开销。例如,在典型的 TCP/IP 有线以太网中,开销可以低至个位数的百分比,但在 ATM 上的 TCP/IP,开销已经接近 20%。如果你的带宽供应商没有提供开销信息,你需要对起始值做一个合理的猜测。无论如何,请记住,总带宽永远不会大于网络路径中最弱链路的带宽。
队列仅支持相对于执行队列操作的系统的出站连接。在规划带宽管理时,考虑到实际可用带宽应该等于连接路径中最弱(带宽最小)的链路,即使你的队列设置在不同的接口上。
HFSC 算法
在 OpenBSD 5.5 及以后版本中,任何你定义的队列系统的基础都是 分层公平服务曲线(HFSC) 算法。HFSC 的设计目的是在一个层级结构中公平地分配资源。它的一个有趣特性是,直到某部分流量接近预设限制时,它才开始整形流量。该算法会在流量接近剥夺其他队列的最小保证份额的点之前开始整形。
注意
我们在本书中展示的所有示例配置都将流量分配到出站队列中,因为你只能现实地控制本地生成的流量,并且一旦达到限制,任何流量整形系统最终都会通过丢包来迫使端点回退。正如我们在前面的示例中看到的,所有表现良好的 TCP 堆栈会通过较慢的包速率响应丢失的 ACK。
现在你至少了解了 OpenBSD 队列系统背后的基本理论,让我们来看看队列是如何工作的。
将带宽分成固定大小的块
你会经常发现某些流量应该比其他流量具有更高的优先级。例如,你可能希望重要的流量,如邮件和其他关键服务,始终保持一定的带宽,而其他服务,如点对点文件共享,则不应允许消耗超过某个量的带宽。为了解决这些问题,队列提供了比纯优先级方案更广泛的选择。
第一个队列示例基于前面章节中的规则集。场景是我们有一个小型本地网络,我们希望让本地网络的用户连接到一个预定义的外部服务集,同时也允许外部网络的用户访问本地网络上的 Web 服务器和 FTP 服务器。
队列定义
在以下示例中,所有队列都设置在根队列上,根队列叫做main,并且位于面向外部、连接互联网的接口上。这个方法主要是因为外部链路的带宽比本地网络更容易受到限制。然而,原则上,分配队列并进行流量整形可以在任何网络接口上进行。
这个设置包括一个总带宽为 20Mb 的队列,并且有六个子队列。
queue main on $ext_if bandwidth 20M
queue defq parent main bandwidth 3600K default
queue ftp parent main bandwidth 2000K
queue udp parent main bandwidth 6000K
queue web parent main bandwidth 4000K
queue ssh parent main bandwidth 4000K
queue ssh_interactive parent ssh bandwidth 800K
queue ssh_bulk parent ssh bandwidth 3200K
queue icmp parent main bandwidth 400K
前面示例中显示的子队列defq具有 3600K 的带宽分配,即占总带宽的 18%,并被指定为默认队列。这意味着任何匹配pass规则但没有明确分配到其他队列的流量都会进入此队列。
其他队列大致遵循相同的模式,直到子队列ssh,它本身有两个子队列(下方的两个缩进行)。在这里,我们看到了使用两个不同优先级加速 ACK 包的技巧的变化,正如我们稍后会看到的,分配流量到这两个 SSH 子队列的规则分配了不同的优先级。大容量 SSH 传输,通常是 SCP 文件传输,使用指示吞吐量的 ToS,而交互式 SSH 流量则设置为低延迟的 ToS 标志,并且会优先于大容量传输。交互式流量可能消耗的带宽较少,因此获得较小的带宽份额,但由于它被分配了更高的优先级,因此得到优先处理。这个方案还帮助了 SCP 文件传输的速度,因为 SCP 传输的 ACK 包将被分配更高的优先级。
最后,我们有icmp队列,它为剩余的 400K 带宽(即 2%的带宽)保留。这确保了我们希望传递的 ICMP 流量能够获得最小的带宽保证,即使它不符合分配到其他队列的标准。
规则集
为了将队列与规则集绑定,我们使用pass规则来指示哪些流量被分配到哪些队列及其相应标准。
set skip on { lo, $int_if }
pass log quick on $ext_if proto tcp to port ssh \
queue (ssh_bulk, ssh_interactive) set prio (5,7)
pass in quick on $ext_if proto tcp to port ftp queue ftp
pass in quick on $ext_if proto tcp to port www queue http
pass out on $ext_if proto udp queue udp
pass out on $ext_if proto icmp queue icmp
pass out on $ext_if proto tcp from $localnet to port $client_out ➊
ssh、ftp、www、udp和icmp的规则将流量分配到它们各自的队列,并且我们再次注意到,ssh队列的子队列分配了两种不同的优先级的流量。最后的兜底规则➊将所有其他来自本地网络的外发流量通过,并将其放入默认的defq队列。
你也可以让一组match规则来执行队列分配,从而使配置更加灵活。有了匹配规则,你可以将过滤决策移到其他地方的规则集来进行阻塞、通过,甚至重定向。
match log quick on $ext_if proto tcp to port ssh \
queue (ssh_bulk, ssh_interactive) set prio (5,7)
match in quick on $ext_if proto tcp to port ftp queue ftp
match in quick on $ext_if proto tcp to port www queue http
match out on $ext_if proto udp queue udp
match out on $ext_if proto icmp queue icmp
请注意,使用match规则进行队列分配时,无需一个最终的兜底规则将不匹配其他规则的流量放入默认队列。任何不匹配这些规则并且允许通过的流量都会进入默认队列。
上限和下限与突发流量
固定带宽分配是不错的选择,但对于那些有流量整形需求的网络管理员来说,在初步尝试后,他们往往希望有更多的灵活性。假如有一个带宽分配机制,既能提供每个队列的带宽下限和上限保证,又能随时间变化进行灵活分配,并且只有在真正需要时才开始进行流量整形,那该有多好呢?
好消息是,OpenBSD 队列正好能够做到这一点,得益于我们之前讨论过的 HFSC 算法。HFSC 使得可以设置带有保证的最小分配和硬性上限的排队机制,甚至可以设置包含burst值的分配,让可用容量随时间变化。
队列定义
从我们在前几章中逐步修改的典型网关配置出发,我们在pf.conf文件的开头插入这个队列定义:
queue rootq on $ext_if bandwidth 20M
queue main parent rootq bandwidth 20479K min 1M max 20479K qlimit 100
queue qdef parent main bandwidth 9600K min 6000K max 18M default
queue qweb parent main bandwidth 9600K min 6000K max 18M
queue qpri parent main bandwidth 700K min 100K max 1200K
queue qdns parent main bandwidth 200K min 12K burst 600K for 3000ms
queue spamd parent rootq bandwidth 1K min 0K max 1K qlimit 300
这个定义与引入带宽分配队列中的定义有一些显著不同。我们从这个相对较小的层次结构开始,将顶层队列rootq分成两个。接下来,我们将main队列细分成几个子队列,所有子队列都有一个设置了的min值——即分配给队列的最小带宽保证。(max值会设置队列分配的硬上限。)bandwidth参数还设置了当队列出现积压时可用的带宽分配——即当队列开始消耗其qlimit(队列限制)分配时。
队列限制参数的工作原理如下:在发生拥塞时,每个队列默认有一个 50 个槽位的池(即队列限制),用来保存那些无法立即传输的数据包。在这里,顶层队列main和spamd都通过它们的qlimit设置,分别设定了大于默认值的池:main为 100,spamd为 300。增大这些qlimit值意味着我们在流量接近设置的限制时丢包的可能性较小,但也意味着当流量整形启动时,我们会看到进入这些较大池中的连接的延迟增加。
规则集
下一步是将新创建的队列与规则集关联起来。如果你已经有了过滤机制,那么关联就很简单——只需要添加几条match规则:
match out on $ext_if proto tcp to port { www https } \
set queue (qweb, qpri) set prio (5,6)
match out on $ext_if proto { tcp udp } to port domain \
set queue (qdns, qpri) set prio (6,7)
match out on $ext_if proto icmp \
set queue (qdns, qpri) set prio (6,7)
在这里,match规则再次通过高优先级和低优先级队列分配来加速 ACK 包,就像我们在基于纯优先级的系统中看到的那样。唯一的例外是当我们将流量分配到最低优先级队列时(对现有的pass规则稍作修改),此时我们确实不希望加速。
pass in log on egress proto tcp to port smtp \
rdr-to 127.0.0.1 port spamd set queue spamd set prio 0
将spamd流量分配到一个最小大小的队列,并将其优先级设置为 0,目的是在垃圾邮件发送者到达我们的spamd之前减缓他们的速度。(有关spamd及相关问题的更多信息,请参见第六章。)
在队列分配和优先级设置到位之后,应该能清楚地看到,队列层次结构使用了两种熟悉的技巧来高效利用可用带宽。首先,它使用了一种高优先级和低优先级混合的变种,这种方法在之前的纯优先级示例中已经展示过。其次,我们通过为名称服务查找分配一个小而有保证的带宽部分,来加速几乎所有其他流量,特别是 Web 流量。对于qdns队列,我们设置了带有时间限制的burst值:在3000毫秒后,分配将降低到最小值12K,以适应总200K配额。像这样的短时burst值对于加速在早期阶段传输大部分负载的连接非常有用。
从这个示例中可能不太明显,但 HFSC 要求流量只能分配给叶子队列,即没有子队列的队列。这意味着,可以将流量分配给main的子队列——qpri、qdef、qweb和qdns——以及rootq的子队列——spamd——正如我们刚才通过match和pass规则所做的那样,但不能直接分配给rootq或main本身。所有队列分配到位后,我们可以使用systat队列命令来显示队列及其流量:
6 users Load 0.31 0.28 0.34 Tue May 19 21:31:54 2015
QUEUE BW SCH PR PKTS BYTES DROP_P DROP_B QLEN BORR SUSP P/S B/S
rootq 20M 0 0 0 0 0
main 20M 0 0 0 0 0
qdef 9M 48887 15M 0 0 0
qweb 9M 18553 8135K 0 0 0
qpri 600K 37549 2407K 0 0 0
qdns 200K 15716 1568K 0 0 0
spamd 1K 10590 661K 126 8772 47
队列以缩进方式显示,表示它们的层级结构,从根队列到叶子队列。main队列及其子队列——qpri、qdef、qweb和qdns——显示了它们的带宽分配以及通过的字节和包数量。如果在此阶段我们被迫丢包,DROP_P和DROP_B列会显示丢弃的包和字节数。QLEN表示等待处理的包数量,而最后两列显示每秒包和字节的实时更新。
要查看更详细的视图,请使用pfctl -vvsq显示队列及其流量:
queue rootq on xl0 bandwidth 20M qlimit 50
[ pkts: 0 bytes: 0 dropped pkts: 0 bytes: 0 ]
[ qlength: 0/ 50 ]
[ measured: 0.0 packets/s, 0 b/s ]
queue main parent rootq on xl0 bandwidth 20M, min 1M, max 20M qlimit 100
[ pkts: 0 bytes: 0 dropped pkts: 0 bytes: 0 ]
[ qlength: 0/100 ]
[ measured: 0.0 packets/s, 0 b/s ]
queue qdef parent main on xl0 bandwidth 9M, min 6M, max 18M default qlimit 50
[ pkts: 1051 bytes: 302813 dropped pkts: 0 bytes: 0 ]
[ qlength: 0/ 50 ]
[ measured: 2.6 packets/s, 5.64Kb/s ]
queue qweb parent main on xl0 bandwidth 9M, min 6M, max 18M qlimit 50
[ pkts: 1937 bytes: 1214950 dropped pkts: 0 bytes: 0 ]
[ qlength: 0/ 50 ]
[ measured: 3.6 packets/s, 13.65Kb/s ]
queue qpri parent main on xl0 bandwidth 600K, max 1M qlimit 50
[ pkts: 2169 bytes: 143302 dropped pkts: 0 bytes: 0 ]
[ qlength: 0/ 50 ]
[ measured: 6.6 packets/s, 3.55Kb/s ]
queue qdns parent main on xl0 bandwidth 200K, min 12K burst 600K for 3000ms qlimit 50
[ pkts: 604 bytes: 65091 dropped pkts: 0 bytes: 0 ]
[ qlength: 0/ 50 ]
[ measured: 1.6 packets/s, 1.31Kb/s ]
queue spamd parent rootq on xl0 bandwidth 1K, max 1K qlimit 300
[ pkts: 884 bytes: 57388 dropped pkts: 0 bytes: 0 ]
[ qlength: 176/300 ]
[ measured: 1.9 packets/s, 1Kb/s ]
这个视图显示了队列接收流量的情况,基本与站点的典型工作负载一致。请注意,在规则集重新加载后不久,spamd队列就已经接近其qlimit设置的一半,似乎表明队列的维度设置与实际流量相符。
注意
请注意每个队列的丢包计数器(dropped pkts:)。如果丢包数量较高或持续增加,那么可能意味着需要调整某个带宽分配参数,或者需要调查其他网络问题。
DMZ 网络,现在启用了流量整形
在第五章中,我们设置了一个拥有单一网关的网络,并将所有外部可见的服务配置在一个独立的 DMZ(非军事区)网络中,以便从互联网和内部网络到服务器的所有流量都必须通过该网关。该网络示意图在第五章中已展示,这里在图 7-1 中再次展示。以第五章中的规则集为起点,我们将添加一些队列,以优化我们的网络资源。网络的物理和逻辑布局不会改变。
图 7-1. 带有 DMZ 的网络
该网络最可能的瓶颈是网关外部接口与互联网之间的带宽。尽管我们设置中的其他地方的带宽当然不是无限的,但本地网络上任何接口的可用带宽通常会比与外部世界通信的实际带宽更不受限制。为了使服务以最佳性能可用,我们需要设置队列,以便站点上可用的带宽能够分配给我们希望允许的流量。DMZ 接口上的接口带宽可能是 100Mb 或 1Gb,而实际可用带宽则大大小于从本地网络外部连接的带宽。这一考虑体现在我们的队列定义中,其中,外部流量的实际带宽可用性是队列设置中的主要限制因素。
queue ext on $ext_if bandwidth 2M
queue ext_main parent ext bandwidth 500K default
queue ext_web parent ext bandwidth 500K
queue ext_udp parent ext bandwidth 400K
queue ext_mail parent ext bandwidth 600K
queue dmz on $dmz_if bandwidth 100M
queue ext_dmz parent dmz bandwidth 2M
queue ext_dmz_web parent ext_dmz bandwidth 800K default
queue ext_dmz_udp parent ext_dmz bandwidth 200K
queue ext_dmz_mail parent ext_dmz bandwidth 1M
queue dmz_main parent dmz bandwidth 25M
queue dmz_web parent dmz bandwidth 25M
queue dmz_udp parent dmz bandwidth 20M
queue dmz_mail parent dmz bandwidth 20M
注意,对于每个接口,都有一个根队列,其带宽限制决定了分配给该接口所有队列的带宽。为了使用新的队列基础设施,我们还需要对过滤规则进行一些修改。
注
由于任何未明确分配到特定队列的流量都会被分配到接口的默认队列,因此务必根据网络中的实际流量调整过滤规则和队列定义。
添加队列后,过滤规则的主要部分可能如下所示:
pass in on $ext_if proto { tcp, udp } to $nameservers port domain \
set queue ext_udp set prio (6,5)
pass in on $int_if proto { tcp, udp } from $localnet to $nameservers \
port domain
pass out on $dmz_if proto { tcp, udp } to $nameservers port domain \
set queue ext_dmz_udp set prio (6,5)
pass out on $dmz_if proto { tcp, udp } from $localnet to $nameservers \
port domain set queue dmz_udp
pass in on $ext_if proto tcp to $webserver port $webports set queue ext_web
pass in on $int_if proto tcp from $localnet to $webserver port $webports
pass out on $dmz_if proto tcp to $webserver port $webports \
set queue ext_dmz_web
pass out on $dmz_if proto tcp from $localnet to $webserver port $webports \
set queue dmz_web
pass in log on $ext_if proto tcp to $mailserver port smtp
pass in log on $ext_if proto tcp from $localnet to $mailserver port smtp
pass in log on $int_if proto tcp from $localnet to $mailserver port $email
pass out log on $dmz_if proto tcp to $mailserver port smtp set queue ext_mail
pass in on $dmz_if proto tcp from $mailserver to port smtp set queue dmz_mail
pass out log on $ext_if proto tcp from $mailserver to port smtp \
set queue ext_dmz_mail
注意,只有通过 DMZ 或外部接口的流量才会被分配到队列。在这种配置下,如果内部网络没有外部可访问的服务,那么在内部接口上进行队列管理就没有太大意义,因为那可能是带宽限制最少的网络部分。此外,正如之前的示例所示,可以通过将match规则块专门用于队列分配,从而将队列分配与规则集的过滤部分分开。
使用队列处理不需要的流量
到目前为止,我们专注于队列化作为确保特定类型流量高效通过的一种方式。现在,我们将查看两个示例,它们展示了一种稍微不同的方法,使用各种与队列相关的技巧来识别和处理不需要的流量,以确保不法分子被控制。
超载到一个小队列
在《抵御暴力攻击》中,我们使用了状态跟踪选项和overload规则的组合,将特定地址填充到一个特殊处理的表格中。我们在第六章中演示的特殊处理方法是切断所有连接,但同样也可以将overload流量分配到特定队列。例如,考虑下面这个我们第一个队列示例中的规则。
pass log quick on $ext_if proto tcp to port ssh flags S/SA \
keep state queue (ssh_bulk, ssh_interactive) set prio (5,7)
要创建第六章中超载表格技巧的变种,可以添加状态跟踪选项,像这样:
pass log quick on $ext_if proto tcp to port ssh flags S/SA \
keep state (max-src-conn 15, max-src-conn-rate 5/3, \
overload <bruteforce> flush global) queue (ssh_bulk, ssh_interactive) \
set prio (5,7)
然后,将其中一个队列略微缩小:
queue smallpipe parent main bandwidth 512
并通过这个规则将不法分子的流量分配到小带宽队列:
pass inet proto tcp from <bruteforce> to port $tcp_services queue smallpipe
结果是,来自暴力破解者的流量将被通过,但每秒传输速率有一个严格的上限为 512 比特。(值得注意的是,由于网络栈的计时器精度问题,可能很难在高速链接上强制执行小带宽分配。如果分配的带宽相对于链路容量过小,超出每秒最大分配的包可能仍然会被传输,直到带宽限制生效。)此外,像这样的规则可能还需要补充表项过期设置,如在使用 pfctl 整理表格中所描述的那样。
基于操作系统指纹的队列分配
第六章介绍了几种使用spamd减少垃圾邮件的方法。如果在你的环境中无法运行spamd,你可以基于以下知识使用队列和规则集:发送垃圾邮件的机器很可能运行某个特定的操作系统。(我们将这个操作系统称为 Windows。)
PF 具有一个相当可靠的操作系统指纹识别机制,可以根据连接建立时初始 SYN 数据包的特征来检测网络连接另一端的操作系统。如果你已经确定从运行该特定操作系统的系统中不太可能传送合法邮件,那么以下内容可以作为spamd的简单替代方案。
pass quick proto tcp from any os "Windows" to $ext_if \
port smtp set queue smallpipe
在这里,来自运行特定操作系统的主机的电子邮件流量不会超过你带宽的 512 比特每秒。
从 ALTQ 过渡到优先级和队列
如果你已经有使用 ALTQ 进行流量整形的配置,并计划切换到 OpenBSD 5.5 或更新版本,本节内容将提供一些如何管理过渡的提示。主要要点如下:
过渡后的规则可能更简单。 OpenBSD 5.5 及更新版本的流量整形系统已经摒弃了之前较为复杂的 ALTQ 语法和其队列算法的选择,且明确区分了队列和纯优先级的排序。在大多数情况下,转换为新流量整形系统后,你的配置将变得更具可读性和可维护性。
对于简单的配置,set prio 已经足够。 ALTQ 中最简单的队列纪律是 priq,即优先级队列。最常见的简单用例是丹尼尔·哈特迈耶在前述文章中首次展示的两优先级加速技巧。基本的两优先级配置如下所示:
ext_if="kue0"
altq on $ext_if priq bandwidth 100Kb queue { q_pri, q_def }
queue q_pri priority 7
queue q_def priority 1 priq(default)
pass out on $ext_if proto tcp from $ext_if queue (q_def, q_pri)
pass in on $ext_if proto tcp to $ext_if queue (q_def, q_pri)
在 OpenBSD 5.5 及更新版本中,可以通过不定义队列来实现相同的效果。相反,你只需在 match 或 pass 规则中指定两个优先级,如下所示:
match out on egress set prio (5, 6)
在这里,第一个优先级将分配给常规流量,而 ACK 和其他具有低延迟 ToS 的数据包将分配第二个优先级,并比常规数据包更快地得到处理。其效果与我们刚刚引用的 ALTQ 示例相同,唯一的不同是定义了带宽限制和流量整形对入站流量的某些可疑影响。
大部分情况下,优先级队列可以被 set prio 构造所替代。 对于纯优先级的区分,按 pass 或 match 规则应用 set prio 比定义队列并分配流量要简单,并且只影响数据包的优先级。ALTQ 允许你定义 CBQ 或 HFSC 队列,并且队列定义中包含优先级值。在新的队列系统下,优先级的分配仅发生在 match 或 pass 规则中,但如果你的应用需要在同一规则中同时设置优先级和队列分配,新的语法也允许这样做:
pass log quick on $ext_if proto tcp to port ssh \
queue (ssh_bulk, ssh_interactive) set prio (5,7)
这种效果与之前在将带宽分割成固定大小的块中展示的行为类似,这种变体在过渡期间可能特别有帮助。
优先级现在始终很重要。请记住,默认值是 3。 需要注意的是,从 OpenBSD 5.0 开始,流量优先级始终是启用的,即使你没有主动分配优先级,也需要考虑优先级。在使用两级优先级技巧来加速 ACK 和所有流量的旧式配置中,唯一重要的是有两个不同的优先级。低延迟的数据包会被分配到高优先级队列中,最终的效果是流量可能通过得更快,带宽使用比默认的 FIFO 队列更有效。现在默认优先级是 3,并且如果将队列的优先级设置为 0(如一些旧示例所做的),那么只有在没有更高优先级的流量需要处理时,分配到该优先级的流量才会被视为准备通过。
对于实际的带宽塑形,HFSC 在幕后工作。 一旦你确定你的规格要求将可用带宽分割成多个块,底层算法始终是 HFSC。不同类型队列的语法已经不再使用。HFSC 被选中是因为它的灵活性,以及它只有在流量接近你队列配置设置的某个限制时,才会主动开始塑形流量。此外,限制队列定义为仅包含带宽声明的配置,可以实现类似 CBQ 的配置。将你的带宽划分为固定大小的块(前文提到)展示了一种静态配置,作为 HFSC 的子集实现 CBQ。
你可以通过旧队列机制从 ALTQ 过渡。 OpenBSD 5.5 支持传统的 ALTQ 配置,仅需要对配置进行一次小改动:queue关键字作为新队列系统的保留字,因此 ALTQ 队列需要声明为oldqueue。只要做出这个更改(这实际上是一个纯粹的搜索替换操作,甚至可以在操作系统升级前执行),配置就会按预期工作。
如果你的设置足够复杂,请返回规格并重新实现。 本章中的示例有些风格化且相对简单。如果你有经过多次增量建设的现有配置,并且已经达到比这里描述的复杂度大几个数量级的水平,新语法可能提供了一个机会,帮助你定义你的设置用途,并创建一个更清晰、更易维护的配置规范进行重新实现。
采用 oldqueue 路线并从那里进行调整会在一定程度上有效,但通过在测试环境中根据修订后的规范进行干净的重新实现可能更容易过渡,在这个环境中你可以测试你的假设是否在新的流量整形系统背景下成立。无论你选择哪种过渡方式,切换到 OpenBSD 5.5 或更高版本后,你几乎可以确定最终会得到一个更加易读和可维护的配置。
使用 ALTQ 定向流量
ALTQ 是一个非常灵活的旧有机制,用于网络流量整形,它在 OpenBSD^([41]) 3.3 版本时被 Henning Brauer 集成到 PF 中,他也是 OpenBSD 5.5 引入的优先级和队列系统(在本章前面的部分已描述)的主要开发者。从 OpenBSD 3.3 开始,所有 ALTQ 配置都被移入 pf.conf,以便于流量整形和过滤的集成。PF 移植到其他 BSD 系统时,快速地采用了至少一些可选的 ALTQ 集成功能。
注意
OpenBSD 5.5 引入了一个全新的队列系统,用于流量整形,语法与之前有着根本性的不同(而且更易读),它补充了在 OpenBSD 5.0 中引入的始终开启的优先级系统。这个新系统旨在经过一次过渡发布后完全取代 ALTQ。本章的其余部分只有在你有兴趣学习如何设置或维护基于 ALTQ 的系统时才有用。
基本的 ALTQ 概念
正如其名,ALTQ 配置完全以队列为中心。与更新后的流量整形系统一样,ALTQ 队列是通过带宽来定义的,并附加到接口上。队列可以分配优先级,在某些情况下,它们可以有子队列,子队列可以接收父队列带宽的一部分。
ALTQ 队列的一般语法如下:
altq on *interface type* [options ... ] *main_queue* { *sub_q1*, *sub_q2* ..}
queue *sub_q1* [ options ... ]
queue *sub_q2* [ options ... ] { *subA*, *subB*, ... }
[...]
pass [ ... ] queue *sub_q1*
pass [ ... ] queue *sub_q2*
注意
在 OpenBSD 5.5 及更新版本中,由于与新队列子系统之间无法解决的语法冲突,ALTQ 队列用 oldqueue 代替了 queue。
一旦队列定义就位,你可以通过重写 pass 或 match 规则,将流量分配到特定队列,从而将流量整形集成到你的规则集中。任何未显式分配到特定队列的流量都会与其他所有流量一起放入默认队列。
队列调度器,也称为队列纪律
在默认的网络配置中,如果没有队列设置,TCP/IP 栈及其过滤子系统会按照 FIFO 纪律处理数据包。
ALTQ 提供了三种队列调度算法,或称为 纪律,它们可以稍微改变这个行为。类型有 priq、cbq 和 hfsc。其中,cbq 和 hfsc 队列可以有多个子队列。priq 队列本质上是扁平的,只有一个队列级别。每种纪律都有其独特的语法规则,我们将在接下来的部分中介绍这些规则。
priq
基于优先级的队列完全根据总带宽中声明的优先级来定义。对于priq队列,允许的优先级范围是 0 到 15,其中较高的值会获得优先处理。符合较高优先级队列标准的数据包会在符合较低优先级队列标准的数据包之前被处理。
cbq
基于类别的队列定义为常量大小的带宽分配,作为总可用带宽的百分比,或以千比特、兆比特或千兆比特每秒为单位。cbq 队列可以细分为多个子队列,这些子队列也被分配了 0 到 7 范围内的优先级,优先级较高意味着优先处理。
hfsc
hfsc 规则使用 HFSC 算法确保队列层次中的带宽分配“公平”。HFSC 允许设置具有最小保留分配和硬性上限的队列策略。分配甚至可以随时间变化,且你还可以拥有从 0 到 7 范围的细粒度优先级。
因为算法和与 ALTQ 配置相关的设置都相当复杂,且有多个可调参数,大多数 ALTQ 用户倾向于使用更简单的队列类型。然而,那些声称理解 HFSC 的人都对它推崇备至。
设置 ALTQ
启用 ALTQ 可能需要一些额外步骤,具体取决于你选择的操作系统。
OpenBSD 上的 ALTQ
在 OpenBSD 5.5 上,所有支持的队列规则都已经编译到 GENERIC 和 GENERIC.MP 内核中。请检查你的 OpenBSD 版本是否仍然支持 ALTQ。如果是,唯一需要配置的就是编辑你的 pf.conf 文件。
FreeBSD 上的 ALTQ
在 FreeBSD 上,确保你的内核已经编译了 ALTQ 和 ALTQ 队列规则选项。默认的 FreeBSD GENERIC 内核没有启用 ALTQ 选项,正如你在运行 /etc/rc.d/pf 脚本启用 PF 时看到的消息所示。相关选项如下:
options ALTQ
options ALTQ_CBQ # Class Bases Queuing (CBQ)
options ALTQ_RED # Random Early Detection (RED)
options ALTQ_RIO # RED In/Out
options ALTQ_HFSC # Hierarchical Packet Scheduler (HFSC)
options ALTQ_PRIQ # Priority Queuing (PRIQ)
options ALTQ_NOPCC # Required for SMP build
ALTQ 选项是启用 ALTQ 内核功能所必需的,但在 SMP 系统中,你还需要启用 ALTQ_NOPCC 选项。根据你将使用的队列类型,你至少需要启用以下其中一个选项:ALTQ_CBQ、ALTQ_PRIQ 或 ALTQ_HFSC。最后,你可以通过启用 ALTQ_RED 和 ALTQ_RIO 选项来启用拥塞避免技术 随机早期检测(RED) 和 RED 进/出。 (有关如何编译和安装带有这些选项的自定义内核的信息,请参阅 FreeBSD 手册。)
NetBSD 上的 ALTQ
ALTQ 已集成到 NetBSD 4.0 PF 实现中,并支持 NetBSD 4.0 及之后的版本。NetBSD 的默认 GENERIC 内核配置不包括 ALTQ 相关选项,但 GENERIC 配置文件中将所有相关选项注释掉,方便用户加入。主要的内核选项如下:
options ALTQ # Manipulate network interfaces' output queues
options ALTQ_CBQ # Class-Based queuing
options ALTQ_HFSC # Hierarchical Fair Service Curve
options ALTQ_PRIQ # Priority queuing
options ALTQ_RED # Random Early Detection
ALTQ选项用于启用内核中的 ALTQ。根据你将使用的队列类型,必须至少启用以下之一:ALTQ_CBQ、ALTQ_PRIQ或ALTQ_HFSC。
使用 ALTQ 要求将 PF 编译进内核,因为 PF 加载模块不支持 ALTQ 功能。(有关最新信息,请参阅 NetBSD PF 文档,* www.netbsd.org/Documentation/network/pf.html *)
基于优先级的队列
基于优先级的队列(priq)的基本概念相当简单。在分配给主队列的总带宽内,只有流量优先级才是重要的。你为队列分配一个介于 0 到 15 之间的优先级值,其中较高的值意味着该队列的流量请求会更早被处理。
使用 ALTQ 优先级队列来提高性能
Daniel Hartmeier 发现了一种简单而有效的方式,通过使用 ALTQ 优先级队列来改善家庭网络的吞吐量。像许多人一样,他的家庭网络是基于非对称连接,总带宽较低,以至于他希望能更好地利用带宽。此外,当线路接近或达到最大容量时,奇怪的现象开始出现。特别有一个症状似乎表明有改进的空间:每当出站流量开始时,进入的流量(下载、邮件等)会不成比例地变慢——这一现象无法仅通过测量传输的数据量来解释。所有这一切都回到了 TCP 的一个基本特性。
当一个 TCP 数据包被发送时,发送方期望接收方的确认(ACK 数据包),并会在指定时间内等待其到达。如果 ACK 在规定时间内没有到达,发送方假设数据包没有被接收并重新发送。由于在默认设置中,数据包是按到达顺序依次由接口处理的,因此 ACK 数据包几乎没有数据负载,结果会在较大的数据包传输时排队等待。
如果 ACK 数据包能够在较大的数据包之间穿插,那么结果将是更高效地利用可用带宽。使用 ALTQ 实现这样一个系统的最简单实际方法是设置两个具有不同优先级的队列,并将它们集成到规则集中。以下是规则集的相关部分。
ext_if="kue0"
altq on $ext_if priq bandwidth 100Kb queue { q_pri, q_def }
queue q_pri priority 7
queue q_def priority 1 priq(default)
pass out on $ext_if proto tcp from $ext_if queue (q_def, q_pri)
pass in on $ext_if proto tcp to $ext_if queue (q_def, q_pri)
在这里,基于优先级的队列在外部接口上设置,并且有两个子队列。第一个子队列q_pri具有优先级值 7;另一个子队列q_def的优先级值为 1,明显较低。
这个看似简单的规则集通过利用 ALTQ 如何处理不同优先级队列的方式来工作。一旦连接建立,ALTQ 检查每个数据包的 ToS 字段。ACK 数据包的 ToS 延迟位设置为低,这表示发送方希望尽可能快速地传输。当 ALTQ 看到低延迟的数据包且有不同优先级的队列可用时,它会将该数据包分配给优先级更高的队列。这意味着 ACK 数据包会跳过低优先级队列并更快地传输,从而使得数据包也能更快地得到服务。最终结果是,使用相同硬件和带宽的情况下,比纯 FIFO 配置的性能更好。(丹尼尔·哈特迈尔关于他这种配置的文章中有更详细的分析。)
使用队列分配的匹配规则
在之前的示例中,规则集是传统方式构建的,队列分配作为pass规则的一部分。然而,这并不是唯一的队列分配方式。当你使用match规则(在 OpenBSD 4.6 及以后版本中可用)时,简单的优先级排队机制可以非常容易地应用到现有规则集上。
如果你已经完成了第三章和第四章中的示例,那么你的规则集可能已经有一个match规则,用于处理出站流量的nat-to。要在规则集中引入基于优先级的排队,你需要首先添加队列定义,并对出站的match规则做一些小调整。
从前面示例中的队列定义开始,根据本地情况调整总带宽,如下所示。
altq on $ext_if priq bandwidth $ext_bw queue { q_pri, q_def }
queue q_pri priority 7
queue q_def priority 1 priq(default)
这会根据你使用ext_bw宏定义的带宽分配队列。
将队列集成到规则集中的最简单快捷方式是编辑你的出站match规则,使其类似于这样:
match out on $ext_if from $int_if:network nat-to ($ext_if) queue (q_def, q_pri)
重新加载你的规则集,优先级排队机制会应用到所有从本地网络发起的流量。
你可以使用systat命令实时查看流量如何分配到你的队列中。
**$ sudo systat queues**
这会给你一个实时显示,类似于以下内容:
2 users Load 0.39 0.27 0.30 Fri Apr 1 16:33:44 2015
QUEUE BW SCH PR PKTS BYTES DROP_P DROP_B QLEN BORRO SUSPE P/S B/S
q_pri priq 7 21705 1392K 0 0 0 12 803
q_def priq 12138 6759K 0 0 0 9 4620
查看PKTS(数据包)和BYTES(字节)列中的数字,你会明显看到排队机制按预期工作。
q_pri队列处理了相当多的包与数据量的比率,正如我们预期的那样。ACK 数据包占用的空间不大。另一方面,分配给q_def队列的流量在每个数据包中包含更多数据,数字显示了与q_pri队列相比的反向包与数据大小比率。
注意
systat 是一个在所有 BSD 系统上都非常有用的程序,OpenBSD 版本提供了与 PF 相关的多个视图,而这些视图在其他系统的 systat 版本中并没有,直到写这本书时仍然如此。我们将在下一章再次讨论 systat。在此期间,阅读手册并多尝试使用这个程序,它是了解系统的一个非常有用的工具。
面向小型网络的基于类别的带宽分配
最大化网络性能通常感觉很好。然而,你可能会发现网络还有其他需求。例如,某些流量(如邮件和其他重要服务)可能需要始终保持一定的带宽基准,而其他服务(比如点对点文件共享)则不应允许消耗超过某个限制的带宽。为了应对这些需求或问题,ALTQ 提供了带有更多选项的基于类别的队列(cbq)规则。
为了说明如何使用cbq,我们将在一个小型本地网络中基于前几章的规则集进行构建。我们希望允许本地网络中的用户连接到自己网络之外的预定义服务集,并允许外部网络的用户访问本地网络中的某个 Web 服务器和 FTP 服务器。
队列定义
所有队列都设置在面向外部的互联网接口上。这样做的主要原因是外部链路上的带宽往往比本地网络更容易受到限制。然而,原则上,队列的分配和流量整形可以在任何网络接口上进行。这里展示的示例设置包括一个总带宽为 2Mb 的 cbq 队列,带有六个子队列。
altq on $ext_if cbq bandwidth 2Mb queue { main, ftp, udp, web, ssh, icmp }
queue main bandwidth 18% cbq(default borrow red)
queue ftp bandwidth 10% cbq(borrow red)
queue udp bandwidth 30% cbq(borrow red)
queue web bandwidth 20% cbq(borrow red)
queue ssh bandwidth 20% cbq(borrow red) { ssh_interactive, ssh_bulk }
queue ssh_interactive priority 7 bandwidth 20%
queue ssh_bulk priority 5 bandwidth 80%
queue icmp bandwidth 2% cbq
子队列 main 占有 18% 的带宽,并被指定为默认队列。这意味着任何符合 pass 规则但没有明确分配到其他队列的流量都会进入此队列。borrow 和 red 关键字表示该队列可以从其父队列“借用”带宽,而系统会尝试通过应用 RED 算法来避免拥塞。
其他队列基本遵循相同的模式,直到子队列 ssh,该子队列本身有两个子队列,并具有独立的优先级。在这里,我们看到了一个基于 ACK 优先级的变种示例。大批量 SSH 传输,通常是 SCP 文件传输,使用标明吞吐量的 ToS 进行传输,而交互式 SSH 流量则将 ToS 标志设置为低延迟,并优先于大批量传输。交互式流量通常消耗的带宽较少,因此得到较少的带宽份额,但由于赋予其更高的优先级,它会得到优待。这种方案也有助于提升 SCP 文件传输的速度,因为 SCP 传输的 ACK 包将被分配到更高优先级的子队列。
最后,我们有 icmp 队列,它被保留用于来自顶级的剩余 2% 带宽。这保证了一个最低的带宽分配给 ICMP 流量,确保其可以传递,但这些流量不符合其他队列的分配标准。
规则集
为了使这一切得以实现,我们使用这些 pass 规则,指示哪些流量被分配到相应的队列中,以及它们的分配标准:
set skip on { lo, $int_if }
pass log quick on $ext_if proto tcp to port ssh queue (ssh_bulk, ssh_
interactive)
pass in quick on $ext_if proto tcp to port ftp queue ftp
pass in quick on $ext_if proto tcp to port www queue http
pass out on $ext_if proto udp queue udp
pass out on $ext_if proto icmp queue icmp
pass out on $ext_if proto tcp from $localnet to port $client_out
ssh、ftp、www、udp 和 icmp 的规则将流量分配到各自的队列中。最后一个捕获规则将来自本地网络的所有其他流量传递到默认的 main 队列。
一个基础的 HFSC 流量整形器
迄今为止我们所看到的简单调度器可以提供高效的配置,但有流量整形需求的网络管理员通常会寻找比纯优先级队列或简单的基于类队列更具灵活性的方案。HFSC 排队算法(在 pf.conf 术语中为 hfsc)提供了灵活的带宽分配、为每个队列提供带宽的上下限保证以及随时间变化的动态分配,并且只有在确实需要时才开始进行流量整形。然而,增加的灵活性是有代价的:其设置比其他 ALTQ 类型稍微复杂一些,调整配置以获得最佳结果可能是一个相当有趣的过程。
队列定义
首先,在我们稍早更改过的相同配置基础上,我们将此队列定义提早放入 pf.conf 文件中:
altq on $ext_if bandwidth $ext_bw hfsc queue { main, spamd }
queue main bandwidth 99% priority 7 qlimit 100 hfsc (realtime 20%, linkshare 99%) \
{ q_pri, q_def, q_web, q_dns }
queue q_pri bandwidth 3% priority 7 hfsc (realtime 0, linkshare 3% red )
queue q_def bandwidth 47% priority 1 hfsc (default realtime 30% linkshare 47% red)
queue q_web bandwidth 47% priority 1 hfsc (realtime 30% linkshare 47% red)
queue q_dns bandwidth 3% priority 7 qlimit 100 hfsc (realtime (30Kb 3000 12Kb), \
linkshare 3%)
queue spamd bandwidth 0% priority 0 qlimit 300 hfsc (realtime 0, upperlimit 1%, \
linkshare 1%)
hfsc 队列定义使用的参数与简单调度器略有不同。我们从这个相对较小的层级开始,将顶级队列拆分成两个。在下一级,我们将 main 队列细分成几个子队列,每个子队列都有一个定义的优先级。所有子队列都设置了一个 realtime 值——分配给队列的最低带宽保证。可选的 upperlimit 设置队列分配的硬性上限。linkshare 参数设置当队列积压时,队列可用的分配带宽——即当队列开始占用其 qlimit 分配时。
在发生拥塞的情况下,每个队列默认有一个 50 个槽位的池(队列限制 qlimit),用于在数据包无法立即传输时将其保留。在此示例中,顶级队列 main 和 spamd 都通过各自的 qlimit 设置设定了大于默认值的池:main 为 100,spamd 为 300。增加队列大小意味着当流量接近设置的限制时,我们丢包的可能性较小,但这也意味着当流量整形启动时,进入这些大于默认池的连接会出现更高的延迟。
此队列层级使用了两种常见的技巧来有效利用可用带宽:
-
它使用了先前纯优先级示例中展示的高优先级与低优先级混合的变种。
-
我们通过为名称服务查找分配一个小而保证的带宽部分,加速几乎所有其他流量(特别是这里看似最优先的 Web 流量)。对于
q_dns队列,我们设置了realtime值,并设定了时间限制——在3000毫秒后,realtime分配降至12Kb。这对于加速在初期阶段传输大部分负载的连接非常有用。
规则集
接下来,我们将新创建的队列添加到规则集中。如果你已经有一个过滤机制(假设你已经有了),那么连接起来变得非常简单,只需要添加一些match规则。
match out on $ext_if from $air_if:network nat-to ($ext_if) \
queue (q_def, q_pri)
match out on $ext_if from $int_if:network nat-to ($ext_if) \
queue (q_def, q_pri)
match out on $ext_if proto tcp to port { www https } queue (q_web, q_pri)
match out on $ext_if proto { tcp udp } to port domain queue (q_dns, q_pri)
match out on $ext_if proto icmp queue (q_dns, q_pri)
在这里,match规则再次通过高优先级和低优先级队列分配的方式加速 ACK 数据包,正如你在纯优先级系统中看到的那样。唯一的例外是当我们将流量分配给最低优先级队列时,在这种情况下,我们真的不在乎是否加速。
pass in log on egress proto tcp to port smtp rdr-to 127.0.0.1 port spamd queue spamd
这个规则的目的是让垃圾邮件发送者在到达我们的spamd时稍微慢一点。通过设置分层队列系统,systat queues也会将队列及其流量以层次结构的形式显示出来。
2 users Load 0.22 0.25 0.25 Fri Apr 3 16:43:37 2015
QUEUE BW SCH PRIO PKTS BYTES DROP_P DROP_B QLEN BORROW SUSPEN P/S B/S
root_nfe0 20M hfsc 0 0 0 0 0 0 0 0
main 19M hfsc 7 0 0 0 0 0 0 0
q_pri 594K hfsc 7 1360 82284 0 0 0 11 770
q_def 9306K hfsc 158 15816 0 0 0 0.2 11
q_web 9306K hfsc 914 709845 0 0 0 50 61010
q_dns 594K hfsc 7 196 17494 0 0 0 3 277
spamd 0 hfsc 0 431 24159 0 0 0 2 174
根队列显示为附加到物理接口——在本例中为nfe0和root_nfe0。main及其子队列——q_pri、q_def、q_web和q_dns——显示了它们的带宽分配及通过的字节和数据包数量。DROP_P和DROP_B列会显示如果我们在此阶段被迫丢弃数据包时,丢弃的数据包数和字节数。最后两列显示了每秒数据包数和每秒字节数的实时更新。
在 DMZ 中排队等待服务器
在第五章中,我们配置了一个带有单一网关的网络,但所有对外可见的服务都配置在一个单独的 DMZ 网络中。这样,从互联网和内部网络到服务器的所有流量都必须通过网关(见图 7-1)。
以第五章中的规则集为起点,我们将添加一些排队策略,以优化我们的网络资源。网络的物理和逻辑布局不会改变。这个网络最可能的瓶颈是网关外部接口与互联网之间的连接带宽。虽然我们设置中的其他带宽并非无限,但任何本地网络接口上的可用带宽通常不会像与外部世界通信时实际可用的带宽那样成为限制因素。为了确保服务在最佳性能下可用,我们需要设置队列,使站点上的可用带宽分配给我们希望允许的流量。
在我们的例子中,DMZ 接口的接口带宽可能是 100Mb 或 1Gb,而来自本地网络外部的连接的实际可用带宽要小得多。这个考虑因素在我们的队列定义中体现得非常明显,你可以清楚地看到可用于外部流量的带宽是队列设置中的主要限制。
total_ext = 2Mb
total_dmz = 100Mb
altq on $ext_if cbq bandwidth $total_ext queue { ext_main, ext_web, ext_udp, \
ext_mail, ext_ssh }
queue ext_main bandwidth 25% cbq(default borrow red) { ext_hi, ext_lo }
queue ext_hi priority 7 bandwidth 20%
queue ext_lo priority 0 bandwidth 80%
queue ext_web bandwidth 25% cbq(borrow red)
queue ext_udp bandwidth 20% cbq(borrow red)
queue ext_mail bandwidth 30% cbq(borrow red)
altq on $dmz_if cbq bandwidth $total_dmz queue { ext_dmz, dmz_main, dmz_web, \
dmz_udp, dmz_mail }
queue ext_dmz bandwidth $total_ext cbq(borrow red) queue { ext_dmz_web, \
ext_dmz_udp, ext_dmz_mail }
queue ext_dmz_web bandwidth 40% priority 5
queue ext_dmz_udp bandwidth 10% priority 7
queue ext_dmz_mail bandwidth 50% priority 3
queue dmz_main bandwidth 25Mb cbq(default borrow red) queue { dmz_main_hi, \
dmz_main_lo }
queue dmz_main_hi priority 7 bandwidth 20%
queue dmz_main_lo priority 0 bandwidth 80%
queue dmz_web bandwidth 25Mb cbq(borrow red)
queue dmz_udp bandwidth 20Mb cbq(borrow red)
queue dmz_mail bandwidth 20Mb cbq(borrow red)
请注意,total_ext带宽限制决定了所有可以进行外部连接的队列的分配。为了使用新的队列基础设施,我们也需要对过滤规则进行一些修改。请记住,任何没有明确分配到特定队列的流量都会被分配到接口的默认队列。因此,调整过滤规则和队列定义,以适应网络中的实际流量是非常重要的。
使用队列分配,过滤规则的主要部分可能最终如下所示:
pass in on $ext_if proto { tcp, udp } to $nameservers port domain \
queue ext_udp
pass in on $int_if proto { tcp, udp } from $localnet to $nameservers \
port domain
pass out on $dmz_if proto { tcp, udp } to $nameservers port domain \
queue ext_dmz_udp
pass out on $dmz_if proto { tcp, udp } from $localnet to $nameservers \
port domain queue dmz_udp
pass in on $ext_if proto tcp to $webserver port $webports queue ext_web
pass in on $int_if proto tcp from $localnet to $webserver port $webports
pass out on $dmz_if proto tcp to $webserver port $webports queue ext_dmz_web
pass out on $dmz_if proto tcp from $localnet to $webserver port $webports \
queue dmz_web
pass in log on $ext_if proto tcp to $mailserver port smtp
pass in log on $ext_if proto tcp from $localnet to $mailserver port smtp
pass in log on $int_if proto tcp from $localnet to $mailserver port $email
pass out log on $dmz_if proto tcp to $mailserver port smtp queue ext_mail
pass in on $dmz_if from $mailserver to port smtp queue dmz_mail
pass out log on $ext_if proto tcp from $mailserver to port smtp \
queue ext_dmz_mail
请注意,只有通过 DMZ 接口或外部接口的流量才会被分配到队列中。在这种配置下,如果内部网络没有外部可访问的服务,那么在内部接口上进行排队就没有太大意义,因为它很可能是我们网络中对带宽限制最少的部分。
使用 ALTQ 来处理不需要的流量
到目前为止,我们已经专注于将队列作为一种方法,确保在现有网络条件下,以尽可能高效的方式通过特定类型的流量。接下来,我们将看两个示例,展示稍微不同的方法来识别和处理不需要的流量,并演示一些与队列相关的技巧,帮助你保持恶意流量的控制。
超载到一个小型队列
回想一下拒绝野蛮人,我们使用了状态跟踪选项和overload规则的组合,填充了一个地址表以便进行特殊处理。我们在第六章中展示的特殊处理是切断所有连接,但同样也可以将overload流量分配到一个特定的队列中。
请参考我们在基于类别的带宽分配示例中的规则。
pass log quick on $ext_if proto tcp to port ssh flags S/SA \
keep state queue (ssh_bulk, ssh_interactive)
我们可以添加状态跟踪选项,如这里所示。
pass log quick on $ext_if proto tcp to port ssh flags S/SA \
keep state (max-src-conn 15, max-src-conn-rate 5/3, \
overload <bruteforce> flush global) queue (ssh_bulk, ssh_interactive)
然后,我们可以将其中一个队列稍微设置得小一些。
queue smallpipe bandwidth 1kb cbq
接下来,我们可以使用以下规则将恶意流量分配到这个小带宽队列。
pass inet proto tcp from <bruteforce> to port $tcp_services queue smallpipe
补充类似这些规则的表项过期机制也可能很有用,正如在使用 pfctl 整理表项中所描述的那样。
基于操作系统指纹的队列分配
第六章介绍了几种使用spamd减少垃圾邮件的方法。如果在你的环境中不能运行spamd,你可以使用基于常识的队列和规则集,因为发送垃圾邮件的机器很可能运行某些特定的操作系统。
PF 有一个相当可靠的操作系统指纹识别机制,可以根据连接建立时初始 SYN 包的特征,检测网络连接另一端的操作系统。如果你已经确定合法邮件不太可能来自运行该特定操作系统的系统,以下方法可能是spamd的简单替代方案。
pass quick proto tcp from any os "Windows" to $ext_if port smtp queue smallpipe
在这里,来自运行特定操作系统的主机的电子邮件流量不会超过 1KB 带宽,并且没有带宽借用。
结论:流量整形的乐趣,甚至可能带来利润
本章讨论了可以让流量传输更快的流量整形技术,或者至少可以按照你的规格,使优先流量更高效地传输。到现在为止,你应该至少对流量整形的基本概念以及如何在系统上使用流量整形工具有了初步的了解。
我希望本章中那些稍显风格化(但功能性的)示例,能让你对流量整形的可能性感到一些兴趣,并激发你自己思考如何在网络中使用流量整形工具。如果你注意观察网络流量和它所表达的潜在需求(更多内容请参见第九章和第十章,了解如何详细研究网络流量),你可以利用流量整形工具来提升你的网络为用户提供服务的方式。幸运的话,用户会感谢你的努力,甚至你自己也可能会享受这个过程。
^([39]) 丹尼尔·哈特迈尔(Daniel Hartmeier),PF 的原始开发者之一,写了一篇关于这个问题的精彩文章,文章可以在 www.benzedrine.cx/ackpri.html 查阅。丹尼尔的解释使用了旧版 ALTQ 优先级队列语法,但包含的数据清楚地展示了为帮助 ACK 传递分配两个不同优先级的效果。
^([40]) 我知道这让书籍显得有些过时。几年后,这些数字看起来会显得有些古老。
^([41]) ALTQ 的原始研究在 USENIX 1999 年会议上以论文形式发表。你可以在线阅读 Kenjiro Cho 的论文《使用 ALTQ 管理流量》,地址为 www.usenix.org/publications/library/proceedings/usenix99/cho.html。该代码随后通过 Cho 和 Chris Cappucio 的努力出现在 OpenBSD 中。
第八章. 冗余与资源可用性

高可用性和持续服务一直以来既是市场营销的流行词,也是现实世界中 IT 专业人员和网络管理员所追求的目标。为了满足这一需求并解决一些相关问题,CARP 和 pfsync 被作为两项备受期待的功能添加到 OpenBSD 3.5 中。借助这些工具,OpenBSD 及其他采用这些工具的操作系统在提供类似于其他操作系统所称的通用 集群 功能方面迈出了重要步伐。OpenBSD 及其姐妹 BSD 系统使用的术语与其他产品不同,但正如您将在本章中看到的那样,CARP、pfsync 和相关工具提供的高可用性功能相当于许多专有系统通常仅作为昂贵的可选功能提供的功能。
本章介绍如何使用基础系统中的这些工具来管理资源可用性——换句话说,如何利用它们确保在不利条件下,您负责的资源和服务保持可用。
冗余与故障转移:CARP 和 pfsync
通用地址冗余协议(CARP)被开发出来作为一种不受专利限制的替代方案,用于虚拟路由器冗余协议(VRRP),后者已经接近成为 IETF 批准的标准,尽管潜在的专利问题尚未解决。^([42]) CARP 的主要目的是确保即使由于错误或计划中的维护活动(如升级)导致防火墙或其他服务停机,网络也能继续正常运行。OpenBSD 开发人员不仅仅满足于复制受专利限制的协议,而是决定在多个方面做得更好。CARP 具有经过认证的冗余功能——它与地址族无关,并且配有状态同步功能。作为 CARP 的补充,pfsync 协议设计用于处理冗余数据包过滤节点或网关之间的 PF 状态同步。这两个协议的目的是确保关键网络功能的冗余,并实现自动故障转移。
CARP 基于将一组机器设置为一个主机和一个或多个冗余备份的方式,所有这些机器都能够处理一个公共的 IP 地址。如果主机出现故障,其中一个备份将继承该 IP 地址。CARP 主机之间的交接可以通过认证进行,基本上是通过设置共享密钥(实际上类似于密码)来完成。
在 PF 防火墙的情况下,可以设置 pfsync 来处理同步。如果通过 pfsync 的同步已经正确设置,活跃连接将在不中断的情况下进行切换。本质上,pfsync 是一种虚拟网络接口,专门设计用于在 PF 防火墙之间同步状态信息。它的接口通过 ifconfig 分配给物理接口。
即使从技术上讲,可以将 pfsync 流量与其他流量一起放在常规接口上,强烈建议你在单独的网络或甚至 VLAN 上设置 pfsync。pfsync 不对同步伙伴进行身份验证,因此保证正确同步的唯一方法是为 pfsync 流量使用专用接口。
项目规格:一对冗余网关
为了演示使用 CARP 和 pfsync 的有效故障转移设置,我们将检查一个有一个网关连接到外部网络的网络。重新配置网络的目标如下:
-
网络应该像我们引入冗余之前一样正常运行。
-
我们应该在没有明显停机时间的情况下实现更好的可用性。
-
网络应该经历平滑的故障转移,活跃连接不受影响。
我们将从第三章中相对简单的网络开始,如图 8-1 所示。
图 8-1. 单网关的网络
我们用一对冗余的网关替代了单个网关,这对网关共享一个私有网络,通过 pfsync 更新状态信息。结果如图 8-2 所示。
图 8-2. 冗余网关的网络
CARP 地址是虚拟地址,除非你能访问 CARP 群组中所有机器的控制台,否则几乎总是应该为物理接口分配 IP 地址。为每个物理接口分配唯一的 IP 地址,你将能够与主机通信,并确保知道你在与哪台机器交互。如果物理接口没有分配 IP 地址,你可能会遇到一个问题,即备份网关无法通信(除非在物理接口已分配地址的网络中与主机通信),直到它们成为冗余组中的主机,并接管虚拟 IP 地址。
合理的假设是,分配给物理接口的 IP 地址应该属于与虚拟共享 IP 地址相同的子网。同样需要注意的是,这实际上并不是一个硬性要求——甚至可以配置 CARP,其中物理接口没有分配地址。如果你没有为 CARP 接口指定特定的物理接口,内核会尝试将 CARP 地址分配给一个已经配置有与 CARP 地址同一子网的物理接口。即使在更简单的配置中这可能不是必须的,通常还是建议通过 ifconfig 命令中的 carpdev 选项明确选择接口来设置 CARP 接口。
警告
如果在重新配置网络时,默认网关地址从固定的变为特定接口,从主机变为虚拟地址,那么几乎无法避免临时连接中断。
设置 CARP
大多数 CARP 设置工作都集中在布线(根据你的网络示意图)、设置 sysctl 值以及发出 ifconfig 命令。此外,在一些系统中,你需要确保你的内核已经编译了所需的设备。
检查内核选项
在 OpenBSD 中,CARP 和 pfsync 设备都包含在默认的 GENERIC 和 GENERIC.MP 内核配置中。除非你正在运行没有这些选项的自定义内核,否则无需进行内核重新配置。如果你正在运行 FreeBSD,确保内核已编译 CARP 和 pfsync 设备,因为默认的 GENERIC 内核没有这些选项。(请参阅 FreeBSD 手册 了解如何编译和安装带有这些选项的自定义内核。)
NetBSD 应该检查内核是否已编译伪设备 CARP,因为 NetBSD 默认的 GENERIC 内核配置中没有这个设备。(你会在 GENERIC 配置文件中找到相关行被注释掉。)截至本文写作时,NetBSD 由于协议编号问题,尚不支持 pfsync。
设置 sysctl 值
在所有支持 CARP 的系统中,基本功能由一些 sysctl 变量控制。主要的变量是 net.inet.carp.allow,默认情况下它是启用的。在典型的 OpenBSD 系统中,你会看到:
$ **sysctl net.inet.carp.allow**
net.inet.carp.allow=1
这意味着你的系统已配备 CARP。
如果你的内核没有配置 CARP 设备,在 FreeBSD 上运行此命令时应该会产生如下类似的输出:
sysctl: unknown oid 'net.inet.carp.allow'
或者它可能在 NetBSD 上产生类似这样的输出:
sysctl: third level name 'carp' in 'net.inet.carp.allow' is invalid
使用此 sysctl 命令查看所有与 CARP 相关的变量:
$ **sysctl net.inet.carp**
net.inet.carp.allow=1
net.inet.carp.preempt=0
net.inet.carp.log=2
注意
在 FreeBSD 中,你还会遇到只读状态变量 net.inet.carp.suppress_preempt,它表示是否可以进行抢占。在基于 OpenBSD 4.2 或更早版本的 CARP 代码的系统中,你还会看到 net.inet.carp.arpbalance,它用于启用 CARP ARP 平衡,为本地网络上的主机提供有限的负载均衡。
为了在我们的设置中实现网关之间的优雅故障转移,我们需要设置net.inet.carp.preempt变量,以便在具有多个网络接口的主机(如我们的网关)上,所有 CARP 接口将同时在主状态和备份状态之间切换。此设置必须在 CARP 组中的所有主机上保持一致,并且在设置过程中应在所有主机上重复执行。
$ **sudo sysctl net.inet.carp.preempt=1**
net.inet.carp.log变量设置 CARP 日志的调试级别,范围为 0 到 7。默认值为 2,意味着仅记录 CARP 状态变化。
使用 ifconfig 设置网络接口
请注意,在图 8-2 中显示的网络图中,本地网络使用 192.168.12.0 网络中的地址,而面向互联网的接口则位于 192.0.2.0 网络中。考虑到这些地址范围和 CARP 接口的默认行为,设置虚拟接口的命令实际上非常直接。
除了常规的网络参数外,CARP 接口还需要一个额外的参数:虚拟主机 ID(vhid),它唯一标识将共享虚拟 IP 地址的接口。
警告
vhid是一个 8 位值,必须在网络的广播域内唯一设置。将vhid设置为错误的值可能会导致难以调试的网络问题,甚至有传闻称,与其他无关系统的 ID 冲突可能会发生,并导致基于 VRRP 的冗余和负载均衡系统出现中断,VRRP 使用的虚拟节点标识方案类似于 CARP。
在将作为该组初始主机的机器上运行这些命令:
$ **sudo ifconfig carp0 192.0.2.19 vhid 1**
$ **sudo ifconfig carp1 192.168.1.1 vhid 2**
我们不需要显式设置物理接口,因为carp0和carp1虚拟接口将自动绑定到已配置有地址且与分配的 CARP 地址在同一子网中的物理接口。
注意
在提供carpdev选项的ifconfig系统上,建议对所有 CARP 接口设置使用carpdev选项,即使它并非严格要求。在某些情况下,选择物理网络设备用于 CARP 接口可能并不明显,向ifconfig命令中添加carpdev interface字符串可以使设置从无功能到正常工作之间产生差异。这在某些非直观配置和相关网络中的空闲 IP 地址非常有限时尤其有用。FreeBSD 的 CARP 端口从 FreeBSD 10.0 开始提供carpdev选项。
使用ifconfig确保每个 CARP 接口已正确配置,并特别注意carp:这一行,它表示MASTER状态,如下所示:
$ **ifconfig carp0**
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:00:5e:00:01:01
carp: MASTER carpdev ep0 vhid 1 advbase 1 advskew 0
groups: carp
inet 192.0.2.19 netmask 0xffffff00 broadcast 192.0.2.255
inet6 fe80::200:5eff:fe00:101%carp0 prefixlen 64 scopeid 0x5
备份上的设置几乎与主服务器相同,不同之处在于您添加了 advskew 参数,它表示指定机器接管的优先级相对于当前主机有多少较低的优先级。
$ **sudo ifconfig carp0 192.0.2.19 vhid 1 advskew 100**
$ s**udo ifconfig carp1 192.168.1.1 vhid 2 advskew 100**
advskew 参数及其配套值 advbase 用于计算当前主机在接管之后宣布其主机身份的间隔时间。advbase 的默认值为 1,advskew 的默认值为 0。在上述示例中,主机会每秒宣布一次(1 + 0/256),而备份主机会等到 1 + 100/256 秒后才会宣布。
在所有故障转移组主机上启用 net.inet.carp.preempt=1 后,当主服务器停止宣布或宣布自己不可用时,备份服务器会接管,新的主机开始以配置的速率进行宣布。较小的 advskew 值意味着更短的宣布间隔,并且主机成为新主机的可能性更大。如果更多主机具有相同的 advskew 值,则已为主机的主机将保持其主机身份。
在 OpenBSD 4.1 及更高版本中,另一个决定哪个主机接管 CARP 主职责的因素是 降级计数器,这是每个 CARP 主机为其接口组宣布的一个值,用来衡量其 CARP 接口的就绪状态。当降级计数器值为 0 时,主机完全就绪;更高的值表示状态有所下降。您可以使用 ifconfig -g 从命令行设置降级计数器,但通常该值由系统自动设置,通常在启动过程中会有较高的值。所有其他条件相等时,降级计数器值最低的主机会赢得接管 CARP 主机职责的竞争。
注意
截至本文写作时,FreeBSD 10 之前的 CARP 版本不支持设置降级计数器。
在备份服务器上,再次使用 ifconfig 检查每个 CARP 接口是否正确配置:
$ **ifconfig carp0**
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 00:00:5e:00:01:01
carp: BACKUP carpdev ep0 vhid 1 advbase 1 advskew 100
groups: carp
inet 192.0.2.19 netmask 0xffffff00 broadcast 192.0.2.255
inet6 fe80::200:5eff:fe00:101%carp0 prefixlen 64 scopeid 0x5
这里的输出与您刚刚在主服务器上看到的只有一点点不同。请注意,carp: 行表示 BACKUP 状态,并且包含 advbase 和 advskew 参数。
对于实际生产环境,您应当通过配置 CARP 组成员使用共享的秘密密码短语,来增加防止未经授权的 CARP 活动的安全措施,例如以下内容:^([43])
$ **sudo ifconfig carp0 pass mekmitasdigoat 192.0.2.19 vhid 1**
$ **sudo ifconfig carp1 pass mekmitasdigoat 192.168.1.1 vhid 2**
注意
与其他密码一样,密码短语将成为您设置中所有 CARP 流量的必需组成部分。请确保在故障转移组中配置所有 CARP 接口使用相同的密码短语(或无密码)。
一旦确定了适当的设置,通过将它们放入 /etc 中的正确文件,确保它们在未来的系统重启中得到保留:
-
在 OpenBSD 上,将适当的
ifconfig参数放入 hostname.carp0 和 hostname.carp1 中。 -
在 FreeBSD 和 NetBSD 上,将相关行放入 rc.conf 文件中,作为
ifconfig_carp0=和ifconfig_carp1=变量的内容。
保持状态同步:添加 pfsync
作为最终的配置步骤,设置冗余组内主机之间的状态表同步,以防止故障转移期间流量中断。这个操作是通过一组 pfsync 接口实现的。(如前所述,截至本文写作时,NetBSD 不支持 pfsync。)
配置 pfsync 接口需要一些规划和 ifconfig 命令。你可以在任何已配置的网络接口上设置 pfsync,但最好为同步设置一个独立的网络。图 8-2 中的示例配置展示了一个专门为此目的设置的小型网络。交叉电缆连接了两个以太网接口,但在具有超过两个主机的故障转移组中,你可能需要一个带有独立交换机、集线器或 VLAN 的设置。用于同步的接口分别被分配了 IP 地址 10.0.12.16 和 10.0.12.17。
完成基本的 TCP/IP 配置后,每个同步伙伴接口的完整 pfsync 设置是
$ **sudo ifconfig pfsync0 syncdev ep2**
pfsync 协议本身提供的安全功能有限:它没有认证机制,并且默认通过 IP 多播流量进行通信。然而,在无法实现物理独立网络的情况下,你可以通过将 pfsync 设置为仅与指定的 syncpeer 同步来增强其安全性:
$ **sudo ifconfig pfsync0 syncpeer 10.0.12.16 syncdev ep2**
这将生成一个配置的接口,在 ifconfig 输出中显示如下:
pfsync0: flags=41<UP,RUNNING> mtu 1500
priority: 0
pfsync: syncdev: ep2 syncpeer: 10.0.12.16 maxupd: 128 defer: off
groups: carp pfsync
另一种选择是设置一个 IPsec 隧道,并使用它来保护同步流量。在这种情况下,ifconfig 命令是
$ **sudo ifconfig pfsync0 syncpeer 10.0.12.16 syncdev enc0**
这意味着 syncdev 设备变成了 enc0 封装接口,而不是物理接口。
注意
如果可能,设置物理独立的专用网络或独立 VLAN 进行同步,因为任何丢失的 pfsync 更新可能导致故障转移时状态不清晰。
检查 PF 状态同步是否正常运行的一个非常有用的方法是使用 systat states 在每台机器上查看同步主机的状态表。该命令为你提供一个实时显示的状态,展示同步目标上的批量更新。在同步之间,所有主机的状态应该一致显示。(流量计数器——如传输的包数和字节数——是例外;它们只在处理实际连接的主机上显示更新。)
这标志着基于 CARP 的故障转移的基本网络配置的结束。在接下来的部分中,我们将讨论在编写冗余配置规则集时需要注意的事项。
构建规则集
在我们经历了所有这些曲折的配置以实现基本的网络功能后,你可能会想知道,将当前pf.conf中的规则迁移到新设置需要做哪些工作。幸运的是,所需的修改并不多。我们引入的主要变化对外界基本上是不可见的,针对单一网关配置设计的规则集通常也能很好地适用于冗余配置。
尽管如此,我们引入了两种额外的协议(CARP 和 pfsync),你可能需要对规则集做一些相对较小的调整,以便故障转移能够正常工作。基本上,你需要将 CARP 和 pfsync 流量传递到合适的接口。处理 CARP 流量的最简单方法是为你的carpdevs引入一个宏定义,包含所有将处理 CARP 流量的物理接口。你还需要引入一个配套的pass规则,例如以下规则,以便在适当的接口上传递 CARP 流量:
pass on $carpdevs proto carp
同样地,对于 pfsync 流量,你可以为syncdev引入宏定义,并添加一个配套的pass规则:
pass on $syncdev proto pfsync
完全跳过 pfsync 接口进行过滤,在性能上比过滤和转发要便宜。要完全把 pfsync 设备排除在过滤范围外,可以使用如下规则:
set skip on $syncdev
你还应该考虑虚拟 CARP 接口及其地址与物理接口的角色。就 PF 而言,所有流量都会通过物理接口,但流量的源地址或目标地址可能会是 CARP 接口的 IP 地址。
你可能会发现你的配置中有一些规则,你不想在故障转移时费心去同步,比如与运行在网关本身上的服务的连接。一个典型的例子是允许管理员 SSH 连接的规则:
pass in on $int_if from $ssh_allowed to self
对于这样的规则,你可以使用状态选项no-sync来防止同步在故障转移发生后实际上并不相关的连接状态变化:
pass in on $int_if from $ssh_allowed to self keep state (no-sync)
使用这种配置,你将能够在 CARPed 系统组的成员中安排操作系统升级和以前会造成停机的活动,选择在最合适的时机进行,而不会对服务用户造成明显的停机。
Ifstated,接口状态守护进程
在正确配置的 CARP 环境中,基本的网络功能已经得到很好的支持,但你的设置可能包含一些在主机网络配置发生变化时需要特别关注的元素。例如,当某个特定接口停止工作或重新启动时,某些服务可能需要启动或停止,或者你可能希望在接口状态变化时运行特定的命令或脚本。如果这听起来很熟悉,那么ifstated就是为你准备的。
ifstated工具是在 OpenBSD 3.5 中引入的,用于基于网络接口状态变化触发动作。您可以在 OpenBSD 的基础系统中找到它,或者在 FreeBSD 的端口系统中找到net/ifstated。在 OpenBSD 中,文件/etc/ifstated.conf(或者在 FreeBSD 中安装端口后的/usr/local/etc/ifstated.conf)包含一个几乎可以直接运行的配置文件,并提供了一些关于如何为 CARP 环境设置ifstated的提示。
主要的控制对象是接口及其状态——例如,carp0.link.up是carp0接口变为主控的状态——您将在状态变化时执行相应的操作。
每当接口状态发生变化时需要执行的状态和动作都在一个直观的脚本语言中进行指定,该语言具备基本的特性,如变量、宏和简单的逻辑条件。(请参见man ifstated和man ifstated.conf,以及在您的基础系统安装中提供的默认ifstated.conf示例文件,以了解更多相关内容,以及如何在您的环境中实现基于 CARP 的集群功能。)
CARP 负载均衡
通过故障切换实现冗余是不错的选择,但有时候,硬件闲置以防故障并不那么吸引人,反而更好的是创建一个配置,将网络负载分散到多个主机上。
除了 ARP 负载均衡(它通过基于传入连接的源 MAC 地址计算哈希来工作),OpenBSD 4.3 及更高版本中的 CARP 支持多种基于 IP 的负载均衡方法,其中流量根据从连接的源和目的 IP 地址计算的哈希进行分配。由于 ARP 负载均衡是基于源 MAC 地址的,它仅适用于直接连接的网络段中的主机。另一方面,基于 IP 的方法适用于负载均衡到互联网及其访问的连接。
您的应用程序选择方法将取决于您需要配合的其他网络设备的规格。基本的ip负载均衡模式使用组播 MAC 地址,让直接连接的交换机将流量转发到负载均衡集群中的所有主机。
不幸的是,一些系统不支持单播 IP 地址和组播 MAC 地址的组合。在这种情况下,您可能需要将负载均衡配置为ip-unicast模式,该模式使用单播 MAC 地址,并配置交换机将流量转发到适当的主机。或者,您可能需要将负载均衡配置为ip-stealth模式,该模式根本不使用组播 MAC 地址。像往常一样,问题出在细节上,答案可以在 man 页和其他文档中找到,通常还需要通过一些实验来验证。
注意
传统上,relayd被用作智能负载均衡的前端,用于提供对外服务的服务器。在 OpenBSD 4.7 中,relayd获得了跟踪可用上行链路并根据链路健康状况修改系统路由表的能力,这一功能通过与router关键字捆绑在一起实现。对于具有多个可能上行链路或不同路由表的设置,可以配置relayd选择上行链路,或者借助sysctl变量net.inet.ip.multipath和net.inet6.ip6.multipath,在可用路由和上行链路之间执行负载均衡。具体的配置方式会根据网络环境的不同而有所不同。relayd.conf手册页提供了一个完整的示例,帮助你入门。
CARP 负载均衡模式
在负载均衡模式下,CARP 概念得到了扩展,每个 CARP 接口可以成为多个故障转移组的成员,并且可以成为与共享虚拟地址的物理主机数量相等的负载均衡组的成员。与故障转移模式相比,负载均衡模式下每个节点必须是其所在组的主节点,以便能够接收流量。哪个组(以及相应的物理主机)最终处理某个连接,由 CARP 通过哈希值计算决定。此计算基于连接的源 MAC 地址(在 ARP 平衡模式下)以及源和目标 IP 地址(在 IP 平衡模式下)和实际可用性。该方案的缺点是每个组会消耗一个虚拟主机 ID,因此在负载均衡配置中,这些 ID 会比仅使用故障转移时更快地用完。实际上,基于 CARP 的负载均衡集群的数量上限是 32 个虚拟主机 ID。
advskew参数在负载均衡配置中的作用与在故障转移配置中相似,但 CARP 负载均衡的ifconfig(以及hostname.carpN)语法与故障转移模式下略有不同。
设置 CARP 负载均衡
将前面部分中建立的 CARP 故障转移组更改为负载均衡集群只需编辑配置文件并重新加载。在以下示例中,我们选择一个 IP 负载均衡方案。如果选择其他方案,配置文件中的区别仅在于模式选择的关键字。
在第一台主机上,我们将/etc/hostname.carp0更改为
pass mekmitasdigoat 192.0.2.19 balancing ip carpnodes 5:100,6:0
这表示在该主机上,carp0接口是vhid 5组的成员(advskew为100),并且也是vhid 6组的成员,在该组中它是成为初始主节点的主要候选者(advskew设置为0)。
接下来,我们将/etc/hostname.carp1更改为:
pass mekmitasdigoat 192.168.12.1 balancing ip carpnodes 3:100,4:0
对于carp1,其成员是vhid为3和4的虚拟主机,advskew值分别为100和0。
对于另一个主机,advskew 值是相反的,但配置其他方面大致相似。这里,/etc/hostname.carp0 文件内容如下:
pass mekmitasdigoat 192.0.2.19 balancing ip carpnodes 5:0,6:100
这意味着 carp0 接口是 vhid 5 的成员,advskew 0,并且是 vhid 6 的成员,advskew 100。与之互补的是 /etc/hostname.carp1 文件,其内容如下:
pass mekmitasdigoat 192.168.12.1 balancing ip carpnodes 3:0,4:100
再次强调,carp1 是 vhid 3 和 4 的成员,advskew 0 位于第一个组,100 位于第二个组。
第一个主机上 carp 接口组的 ifconfig 输出如下所示:
$ **ifconfig carp**
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 01:00:5e:00:01:05
priority: 0
carp: carpdev vr0 advbase 1 balancing ip
state MASTER vhid 5 advskew 0
state BACKUP vhid 6 advskew 100
groups: carp
inet 192.0.2.19 netmask 0xffffff00 broadcast 192.0.2.255
inet6 fe80::200:24ff:fecb:1c10%carp0 prefixlen 64 scopeid 0x7
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 01:00:5e:00:01:03
priority: 0
carp: carpdev vr1 advbase 1 balancing ip
state MASTER vhid 3 advskew 0
state BACKUP vhid 4 advskew 100
groups: carp
inet 192.168.12.1 netmask 0xffffff00 broadcast 192.168.12.255
inet6 fe80::200:24ff:fecb:1c10%carp1 prefixlen 64 scopeid 0x8
pfsync0: flags=41<UP,RUNNING> mtu 1500
priority: 0
pfsync: syncdev: vr2 syncpeer: 10.0.12.17 maxupd: 128 defer: off
groups: carp pfsync
另一个主机的 ifconfig 输出如下:
$ **ifconfig carp**
carp0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 01:00:5e:00:01:05
priority: 0
carp: carpdev vr0 advbase 1 balancing ip
state BACKUP vhid 5 advskew 100
state MASTER vhid 6 advskew 0
groups: carp
inet 192.0.2.19 netmask 0xffffff00 broadcast 192.0.2.255
inet6 fe80::200:24ff:fecb:1c18%carp0 prefixlen 64 scopeid 0x7
carp1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
lladdr 01:00:5e:00:01:03
priority: 0
carp: carpdev vr1 advbase 1 balancing ip
state BACKUP vhid 3 advskew 100
state MASTER vhid 4 advskew 0
groups: carp
inet 192.168.12.1 netmask 0xffffff00 broadcast 192.168.12.255
inet6 fe80::200:24ff:fecb:1c18%carp1 prefixlen 64 scopeid 0x8
pfsync0: flags=41<UP,RUNNING> mtu 1500
priority: 0
pfsync: syncdev: vr2 syncpeer: 10.0.12.16 maxupd: 128 defer: off
groups: carp pfsync
如果我们在负载均衡方案中有三个节点,每个 carp 接口都需要加入一个额外的组,总共是三个组。简而言之,对于每个在负载均衡组中引入的物理主机,每个 carp 接口都成为一个额外组的成员。
一旦设置好负载均衡集群,运行 systat states 命令,观察每个主机的连接流量,持续几分钟,确保系统按预期运行,并验证你所做的努力是值得的。
^([42]) VRRP 在 RFC 2281 和 RFC 3768 中有详细描述。相关专利由 Cisco、IBM 和 Nokia 持有。详情请参阅 RFC 文档。
^([43]) 这个特定的密码短语有着非常明确的含义。通过 Web 搜索可以发现它的意义,以及为什么它是现代网络文档中理所当然的标准。最终的答案可以通过 openbsd-misc 邮件列表的归档找到。
第九章:日志记录、监控与统计

控制网络——无论是为了家庭网络需求,还是在专业环境中的应用——可能是阅读本书的每个人的主要目标之一。保持控制的一个必要元素是能够访问关于网络中发生的所有相关信息。幸运的是,PF —— 就像大多数类 Unix 系统的组件 —— 能够生成网络活动的日志数据。
PF 提供了丰富的选项来设置日志详细程度、处理日志文件以及提取特定类型的数据。你已经可以使用基础系统中的工具做很多事情,并且通过你的包管理系统还可以获得其他一些工具,用于以多种有用的方式收集、研究和查看日志数据。在本章中,我们将更详细地探讨 PF 日志的一般情况以及你可以用来提取和展示信息的一些工具。
PF 日志:基础
PF 记录的信息和日志详细程度由你决定,取决于你的规则集。基础的日志记录很简单:对于每个你希望记录数据的规则,添加 log 关键字。当你加载带有 log 的规则集时,任何开始连接的匹配日志规则的数据包(被阻止、通过或匹配)都会被复制到 pflog 设备。数据包一旦被 PF 看到并且日志规则被评估时,它就会立即被记录。
注意
在复杂的规则集中,由于 match 或 pass 规则,数据包可能会经历多次转换,当数据包进入主机时匹配的条件可能在转换后不再匹配。
PF 还会存储一些附加数据,例如时间戳、接口、原始的源和目的 IP 地址、数据包是否被阻止或通过,以及从加载的规则集中关联的规则编号。
PF 日志数据由 pflogd 日志守护进程收集,该进程默认在系统启动时启用 PF 时启动。默认的日志数据存储位置是 /var/log/pflog。日志以二进制格式写入,通常称为 数据包捕获格式(pcap),旨在通过 tcpdump 进行读取和处理。稍后我们将讨论一些提取和展示日志文件信息的附加工具。日志文件格式是一种文档化完善且广泛支持的二进制格式。
开始时,这是一个基本的日志示例。首先从你希望记录的规则开始,并添加 log 关键字:
block log
pass log quick proto { tcp, udp } to port ssh
重新加载规则集后,你应该会看到 /var/log/pflog 文件的时间戳发生变化,文件开始增长。要查看存储了什么内容,可以使用 tcpdump 配合 -r 选项读取该文件。
如果日志记录已经进行了一段时间,输入以下命令可能会产生大量输出:
$ **sudo tcpdump -n -ttt -r /var/log/pflog**
例如,以下仅是一个长文件的前几行,几乎所有行都足够长,需要换行显示:
$ **sudo tcpdump -n -ttt -r /var/log/pflog**
tcpdump: WARNING: snaplen raised from 116 to 160
Sep 13 13:00:30.556038 rule 10/(match) pass in on epic0: 194.54.107.19.34834 >
194.54.103.66.113: S 3097635127:3097635127(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale
0,[|tcp]> (DF)
Sep 13 13:00:30.556063 rule 10/(match) pass out on fxp0: 194.54.107.19.34834 >
194.54.103.66.113: S 3097635127:3097635127(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale
0,[|tcp]> (DF)
Sep 13 13:01:07.796096 rule 10/(match) pass in on epic0: 194.54.107.19.29572 >
194.54.103.66.113: S 2345499144:2345499144(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale
0,[|tcp]> (DF)
Sep 13 13:01:07.796120 rule 10/(match) pass out on fxp0: 194.54.107.19.29572 >
194.54.103.66.113: S 2345499144:2345499144(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale
0,[|tcp]> (DF)
Sep 13 13:01:15.096643 rule 10/(match) pass in on epic0: 194.54.107.19.29774 >
194.54.103.65.53: 49442 [1au][|domain]
Sep 13 13:01:15.607619 rule 12/(match) pass in on epic0: 194.54.107.19.29774 >
194.54.107.18.53: 34932 [1au][|domain]
tcpdump程序非常灵活,特别是在输出方面,它提供了多种显示选项。此示例中的格式来自我们提供给tcpdump的选项。该程序几乎总是显示数据包到达的日期和时间(-ttt选项指定了这种长格式)。接下来,tcpdump列出了加载的规则集中的规则编号、数据包出现的接口、源地址和目标地址及端口(-n选项告诉tcpdump显示 IP 地址而不是主机名),以及各种数据包属性。
注意
日志文件中的规则编号指的是 加载到内存中的 规则集。你的规则集在加载过程中会经历一些自动步骤,例如宏扩展和优化,这使得日志中存储的规则编号可能与从pf.conf文件顶部开始计算得到的规则编号不完全匹配。如果你不确定是哪个规则匹配了,可以使用pfctl -vvs rules命令并研究输出。
在我们的tcpdump输出示例中,我们看到加载的规则集中的第十条规则(rule 10)似乎是一个捕获规则,匹配 IDENT 请求和域名查询。这种输出在调试时非常有价值,要保持网络的掌控,必须具备这种类型的数据。通过一些努力和仔细阅读tcpdump手册页面,你应该能够从日志数据中提取出有用的信息。
若要实时显示你记录的流量,可以使用tcpdump直接从日志设备读取日志信息。为此,使用-i选项指定你希望tcpdump读取的接口,如下所示。(-l选项可启用输出的行缓冲,如果你想查看正在捕获的内容,这个选项很有用。)
$ **sudo tcpdump -lnettti pflog0**
Apr 29 22:07:36.097434 rule 16/(match) pass in on xl0: 85.19.150.98.53 > 213.187.179.198.41101:
63267*- 1/0/2 (68)
Apr 29 22:07:36.097760 rule def/(match) pass out on em0: 213.187.179.198.22 >
192.168.103.44.30827: P 1825500807:1825500883(76) ack 884130750 win 17520 [tos 0x10]
Apr 29 22:07:36.098386 rule def/(match) pass in on em0: 192.168.103.44.30827 >
213.187.179.198.22: . ack 76 win 16308 (DF) [tos 0x10]
Apr 29 22:07:36.099544 rule 442/(match) pass in on xl0: 93.57.15.161.4487 > 213.187.179.198.80:
P ack 3570451894 win 65535 <nop,nop,timestamp 4170023961 0>
Apr 29 22:07:36.108037 rule 25/(match) pass out on xl0: 213.187.179.198.25 >
213.236.166.45.65106: P 2038177705:2038177719(14) ack 149019161 win 17424 (DF)
Apr 29 22:07:36.108240 rule def/(match) pass out on em0: 213.187.179.198.22 >
192.168.103.44.30827: P 76:232(156) ack 1 win 17520 [tos 0x10]
该序列以域名查询响应开始,接着是两个来自打开 SSH 连接的数据包,这告诉我们该站点的管理员可能在匹配规则上启用了log (all)(参见记录所有数据包:log (all)")). 第四个数据包属于一个网站连接,第五个是一个外发 SMTP 连接的一部分,最后是另一个 SSH 数据包。如果你让这个命令一直运行,显示的行最终会滚动出屏幕,但你可以将数据重定向到文件或其他程序以进一步处理。
注意
有时你可能主要对特定主机之间的流量或符合特定条件的流量感兴趣。在这种情况下,tcpdump的过滤功能可能会非常有用。详情请参见man tcpdump。
记录数据包在规则集中的路径:log(匹配)
PF 日志代码的早期版本没有提供一种简单的方法来跟踪数据包在规则集遍历过程中将匹配的所有规则。当 OpenBSD 4.6 引入match规则并为 PF 用户提供一种更方便且稍微简化的方式来对数据包和连接进行转换(例如地址重写)时,这个问题变得更加明显。match规则允许您独立于最终的pass或block决定,对数据包或连接执行操作。指定的操作——如nat-to、rdr-to等——会立即执行。这可能导致一种情况:数据包已被match规则转换,且不再匹配规则集中后续出现的过滤规则,如果没有发生转换,它本应匹配这些规则。一个相对基本的例子是,在规则集中放置一个应用了nat-to的match规则,该规则位于任何pass规则之前。一旦nat-to操作被应用,任何原本会匹配数据包源地址的过滤条件将不再匹配该数据包。
这种更大的灵活性使得某些规则集更难以调试(通常是那些包含多个执行转换的match规则的规则集),因此明显需要一种新的日志选项。
PF 开发者一直在关注日志代码的重写,直到 OpenBSD 4.9 发布时,日志系统被重写,重构后的代码使得引入日志选项matches变得容易,从而帮助调试规则集并跟踪数据包在规则集中的路径,其中多个match或pass规则可能会转化数据包。
将log (matches)添加到规则中会强制记录所有匹配该规则的规则,一旦数据包匹配到包含log (matches)子句的规则。一旦发生这种匹配,所有后续规则也会被记录。因此,您可以使用有针对性的log (matches)语句来追踪数据包在加载规则集中的路径,使得解开复杂规则集变得更加容易。
例如,考虑这个简单的包含 NAT 的规则集。log (matches)规则如下:
match in log (matches) on $int_if from $testhost tag localnet
我们的测试主机是一台位于本地网络中的工作站,IP 地址为 192.168.103.44。当测试主机访问互联网上的一个网站时,日志信息如下所示:
Apr 29 21:08:24.386474 rule 3/(match) match in on em0: 192.168.103.44.14054 > 81.93.163.115.80:
S 1381487359:1381487359(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale 3,nop,nop,timestamp
735353043[|tcp]> (DF) ➊
Apr 29 21:08:24.386487 rule 11/(match) block in on em0: 192.168.103.44.14054 >
81.93.163.115.80: S 1381487359:1381487359(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale
3,nop,nop,timestamp 735353043[|tcp]> (DF) ➋
Apr 29 21:08:24.386497 rule 17/(match) pass in on em0: 192.168.103.44.14054 > 81.93.163.115.80:
S 1381487359:1381487359(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale 3,nop,nop,timestamp
735353043[|tcp]> (DF) ➌
Apr 29 21:08:24.386513 rule 17/(match) pass in on em0: 192.168.103.44.14054 > 81.93.163.115.80:
S 1381487359:1381487359(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale 3,nop,nop,timestamp
735353043[|tcp]> (DF)
Apr 29 21:08:24.386553 rule 5/(match) match out on xl0: 213.187.179.198.14054 >
81.93.163.115.80: S 1381487359:1381487359(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale
3,nop,nop,timestamp 735353043[|tcp]> (DF) ➍
Apr 29 21:08:24.386568 rule 16/(match) pass out on xl0: 213.187.179.198.14054 >
81.93.163.115.80: S 1381487359:1381487359(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale
3,nop,nop,timestamp 735353043[|tcp]> (DF) ➎
初始数据包首先匹配规则 3,即上述日志片段➊中的match log (matches)规则。下一个匹配的是我们加载的规则集中的规则 11 ➋,即最初的block all,但是数据包也匹配了规则 17,这允许它通过pass in on em0 ➌。接下来匹配的规则 5 ➍显然是一个match规则,它应用了nat-to(注意源地址已发生变化)。最后,数据包通过规则 16 ➎,这是一条匹配的pass规则,允许其通过out on xl0。
这个示例实际上只有一个转换(nat-to),但log (matches)功能使我们能够跟踪连接的初始数据包,通过规则集中的所有匹配规则,包括源地址的替换。
记录所有数据包:log (all)
对于大多数调试和轻量级监控目的,记录连接中的第一个数据包就提供了足够的信息。然而,有时你可能希望记录所有匹配某些规则的数据包。为此,在你想要监控的规则中使用(all)日志选项。对我们的最小规则集做出这个更改后,我们得到了以下内容:
block log (all)
pass log (all) quick proto tcp to port ssh keep state
这个选项使得日志变得更加冗长。为了说明log (all)生成的日志数据有多少,我们将使用以下规则集片段,它包含了域名查找和网络时间同步的内容:
udp_services = "{ domain, ntp }"
pass log (all) inet proto udp to port $udp_services
在这些规则设置好后,以下是一个示例,展示当一个俄罗斯域名服务器向我们网络中的服务器发送域名请求时会发生什么:
$ **sudo tcpdump -lnttti pflog0 port domain**
tcpdump: WARNING: snaplen raised from 116 to 160
tcpdump: listening on pflog0, link-type PFLOG
Sep 30 14:27:41.260190 212.5.66.14.53 > 194.54.107.19.53:[|domain]
Sep 30 14:27:41.260253 212.5.66.14.53 > 194.54.107.19.53:[|domain]
Sep 30 14:27:41.260267 212.5.66.14.53 > 194.54.107.19.53:[|domain]
Sep 30 14:27:41.260638 194.54.107.19.53 > 212.5.66.14.53:[|domain]
Sep 30 14:27:41.260798 194.54.107.19.53 > 212.5.66.14.53:[|domain]
Sep 30 14:27:41.260923 194.54.107.19.53 > 212.5.66.14.53:[|domain]
现在我们有六条记录,而不仅仅是一条。
即使通过tcpdump过滤掉所有除port domain外的数据,向一个或多个规则添加log (all)也会显著增加日志中的数据量。如果你需要记录所有流量,但网关的存储容量有限,你可能会发现自己需要购买额外的存储设备,而增加的 I/O 活动实际上可能对性能产生负面影响。此外,以这种细节级别记录和存储流量日志很可能会带来法律影响。
负责任地记录日志!
创建任何类型的日志都可能带来意想不到的后果,包括一些法律影响。一旦你开始存储由网络流量生成的日志数据,你就在创建关于用户的信息存储库。存储日志数据长时间的技术和商业理由可能是合理的,但仅记录足够的数据并将其存储在适当的时间内,是一门精细的艺术。
你可能已经对生成日志数据的实际问题有了一些了解,比如安排足够的存储空间,以便保留足够多的日志数据并且能够保持足够长的时间以便使用。法律影响会根据你所在的地区有所不同。某些国家和地区有关于如何处理日志数据的具体要求,并且对这些数据如何使用和保留日志的时间有着限制。其他地方要求服务提供商在特定的时间段内保留流量日志,在某些情况下还要求在执法部门提出请求时提供相关数据。在建立日志基础设施之前,确保你了解法律问题。
记录到多个 pflog 接口
在 OpenBSD 4.1 之后的 PF 版本中,你可以将日志数据定向到多个pflog接口。在 OpenBSD 4.1 中,pflog接口成为了一个可克隆设备,这意味着你可以使用ifconfig命令创建多个pflog接口,除了默认的pflog0之外。这使得将不同规则集部分的日志数据记录到不同的pflog接口成为可能,如果需要,还可以更方便地单独处理这些数据。
从默认的单一pflog0接口转移到多个pflog接口需要对你的设置进行一些微妙但有效的修改。为了将日志记录到多个接口,请确保你规则集中使用的所有日志接口都已创建。你不需要在加载规则集之前创建这些设备;如果你的规则集记录到一个不存在的接口,日志数据会被简单地丢弃。
当调整你的设置以使用多个pflog接口时,你很可能会通过命令行添加所需的接口,如下所示:
$ **sudo ifconfig create pflog1**
在你的规则集中添加log关键字时,指定日志设备,如下所示:
pass log (to pflog1) proto tcp to $emailserver port $email
pass log (to pflog1) proto tcp from $emailserver to port smtp
对于 OpenBSD 上更永久的配置,创建一个包含仅有up的hostname.pflog1文件,以及针对任何额外日志接口的类似hostname.pflogN文件。
在 FreeBSD 上,克隆的pflog接口的配置应包含在你的rc.conf文件中,格式如下:
ifconfig_pflog1="up"
截至本文写作时,NetBSD 上的pflog接口克隆尚不可用。
正如你在第六章中看到的,将日志信息定向到规则集的不同部分并分配到不同的接口使得可以将 PF 生成的日志数据的不同部分输送到不同的应用程序。这使得像spamlogd这样的程序仅处理相关信息,而你可以将 PF 日志数据的其他部分传输给其他日志处理程序。
将日志记录到 syslog,本地或远程
避免将 PF 日志数据存储在网关本身的一种方式是指示网关将日志记录到另一台机器。如果你已经有了集中式日志基础设施,尽管 PF 的普通日志机制并未专门设计为传统的syslog风格的日志记录,但这仍然是一个相当合乎逻辑的做法。
正如任何老牌 BSD 用户所告诉你的,传统的 syslog 系统日志设施在处理它从其他主机通过 UDP 接收到的数据时有些天真,经常提到的一个危险是磁盘填满的拒绝服务攻击。还有一个永远存在的风险是,在高负载下,无论是个别系统还是网络,都可能丢失日志信息。因此,考虑在所有相关主机通过一个安全且适当规划的网络进行通信时,仅设置远程日志记录。在大多数 BSD 系统中,默认情况下,syslogd 并没有配置为接受来自其他主机的日志数据。(如果你计划使用远程 syslog 日志记录,请参见 syslogd 手册页,了解如何启用监听来自远程主机的日志数据。)
如果你仍然希望通过 syslog 来进行 PF 日志记录,以下是一个简短的教程,介绍如何实现这一目标。在普通的 PF 配置中,pflogd 会将日志数据复制到日志文件中。当你希望将日志数据存储在远程系统上时,你应该通过修改 rc.conf.local 文件(在 OpenBSD 上)来禁用 pflog 的数据收集,像这样:
pflogd_flags="NO"
在 FreeBSD 和 NetBSD 上,修改 rc.conf 文件中的 pflog_flats= 设置行。然后终止 pflogd 进程。接下来,确保不再由 pflogd 收集的日志数据以有意义的方式传输到你的日志处理系统中。这个步骤分为两部分:首先,配置系统日志程序将数据传输到日志处理系统,然后使用 tcpdump 和 logger 将数据转换并注入到 syslog 系统中。
要设置 syslogd 来处理数据,选择你的 日志设施、日志级别 和 动作,然后将生成的行添加到 /etc/syslog.conf 文件中。这些概念在 man syslog.conf 中有很好的解释,如果你想理解系统日志,这是必读的。动作 部分通常是本地文件系统中的一个文件。例如,如果你已经设置好系统日志程序在 loghost.example.com 接收数据,那么选择日志设施 local2 和日志级别 info,并输入以下行:
local2.info @loghost.example.com
完成此更改后,重新启动 syslogd 以使其读取新的设置。
接下来,设置 tcpdump 将日志数据从 pflog 设备转换并传递给 logger,然后 logger 会将其发送到系统日志程序。在这里,我们重复使用本章早些时候示例中的 tcpdump 命令,并添加了一些有用的内容:
$ **sudo nohup tcpdump -lnettti pflog0 | logger -t pf -p local2.info &**
nohup 命令确保即使进程没有控制终端或者被放到后台(正如我们在这里使用 & 处理的那样),它仍会继续运行。tcpdump 命令的 -l 选项指定行缓冲输出,这对于重定向到其他程序非常有用。logger 选项为数据流添加标签 pf,以标识 PF 数据,并使用 -p 选项将日志优先级设置为 local2.info。结果会被记录到你在日志主机上指定的文件中,日志条目将类似于以下内容:
pf: Sep 21 14:05:11.492590 rule 93/(match) pass in on ath0:
10.168.103.11.15842 > 82.117.50.17.80: [|tcp] (DF)
pf: Sep 21 14:05:11.492648 rule 93/(match) pass out on xl0:
194.54.107.19.15842 > 82.117.50.17.80: [|tcp] (DF)
pf: Sep 21 14:05:11.506289 rule 93/(match) pass in on ath0:
10.168.103.11.27984 > 82.117.50.17.80: [|tcp] (DF)
pf: Sep 21 14:05:11.506330 rule 93/(match) pass out on xl0:
194.54.107.19.27984 > 82.117.50.17.80: [|tcp] (DF)
pf: Sep 21 14:05:11.573561 rule 136/(match) pass in on ath0:
10.168.103.11.6430 > 10.168.103.1.53:[|domain]
pf: Sep 21 14:05:11.574276 rule 136/(match) pass out on xl0:
194.54.107.19.26281 > 209.62.178.21.53:[|domain]
这段日志片段主要显示了来自 NAT 本地网络中客户端的 Web 浏览活动,从网关的视角来看,并伴随有域名查找。
使用标签跟踪每个规则的统计信息
从获取日志数据时得到的顺序信息基本上是跟踪数据包随时间的移动。在其他情况下,连接的序列或历史记录不如总数重要,例如自从计数器最后一次重置以来,匹配规则的数据包或字节的数量。
在第二章的结尾,你看到如何使用pfctl -s info查看全局汇总计数器以及其他数据。要更详细地分析数据,按规则逐个跟踪流量总数,使用稍微不同形式的pfctl命令,比如pfctl -vs rules,以显示带规则的统计信息,如下所示:
$ **pfctl -vs rules**
pass inet proto tcp from any to 192.0.2.225 port = smtp flags S/SA keep state label "mail-in"
[ Evaluations: 1664158 Packets: 1601986 Bytes: 763762591 States: 0 ]
[ Inserted: uid 0 pid 24490 ]
pass inet proto tcp from 192.0.2.225 to any port = smtp flags S/SA keep state label "mail-out"
[ Evaluations: 2814933 Packets: 2711211 Bytes: 492510664 States: 0 ]
[ Inserted: uid 0 pid 24490 ]
该输出的格式易于阅读,显然是为那些希望快速了解发生了什么的场景设计的。如果你指定更详细的输出,使用pfctl -vvs rules,你会看到基本相同的显示,只不过添加了规则编号。另一方面,这个命令的输出并不非常适合用作脚本或其他程序进一步处理的数据。要以脚本友好的格式提取这些统计数据和其他一些项,并根据自己的需求决定哪些规则值得追踪——请使用规则标签。
标签不仅仅是为了标识处理特定类型流量的规则;它们还使得提取流量统计信息变得更容易。通过将标签附加到规则,你可以存储一些关于规则集部分的额外数据。例如,你可以使用标签来测量带宽使用情况以供计费目的。
在以下示例中,我们将标签mail-in和mail-out分别附加到入站和出站邮件流量的pass规则上。
pass log proto { tcp, udp } to $emailserver port smtp label "mail-in"
pass log proto { tcp, udp } from $emailserver to port smtp label "mail-out"
一旦你加载了带标签的规则集,使用pfctl -vsl检查数据:
$ sudo pfctl -vsl
➊ ➋ ➌ ➍ ➎ ➏ ➐ ➑
mail-in 1664158 1601986 763762591 887895 682427415 714091 81335176
mail-out 2814933 2711211 492510664 1407278 239776267 1303933 252734397
该输出包含以下信息:
-
➊ 标签
-
➋ 规则被评估的次数
-
➌ 传递的数据包总数
-
➍ 传递的总字节数
-
➎ 传递的数据包数量
-
➏ 传入的字节数
-
➐ 传递出去的数据包数量
-
➒ 传递出去的字节数
该列表的格式非常适合脚本和应用程序进行解析。
标签会从规则集加载时起累积数据,直到它们的计数器被重置。在许多情况下,设置一个cron作业,定期读取标签值并将这些值存入永久存储是有意义的。
如果你选择定期运行数据收集,考虑使用pfctl -vsl -z来收集数据。z选项在pfctl读取计数器后重置它们,这样你的数据收集器就会获取周期性数据,即自从命令或脚本上次运行以来的累积数据。
注意
使用宏和列表的规则会扩展为几个不同的规则。如果你的规则集中包含带标签的列表和宏,则内存中的结果将是多个规则,每个规则都附带一个相同名称的标签。虽然这可能导致sudo pfctl -vsl的输出令人困惑,但只要接收数据的应用程序或脚本能够通过汇总相同标签的总数正确解释数据,就不应该成为问题。
如果这种数据收集方式对你有用,那么值得注意的是,最近的 PF 版本提供了将流量元数据收集为 NetFlow 或 IPFIX 数据的选项。有关详细信息,请参见使用 pflow(4)收集 NetFlow 数据收集 NetFlow 数据")。
PF 日志和统计信息的附加工具
保持对网络的控制还需要能够实时查看系统的状态。在本节中,我们将介绍一些你可能会觉得有用的监控工具。这里介绍的所有工具要么在基础系统中可用,要么可以通过 OpenBSD 和 FreeBSD 的包管理系统获得(在 NetBSD 中也有,但有一些例外)。
使用 systat 监控系统
如果你有兴趣查看当前通过你系统传递的流量的即时快照,OpenBSD 上的systat程序提供了几个有用的视图。在第七章中,我们简要查看了systat queues,以了解流量是如何在我们的流量整形规则集中分配到队列的。这里,我们将回顾一些其他有用的选项。
systat程序在所有 BSD 操作系统中都有,版本略有不同。在所有系统中,systat都提供系统统计视图,语法和输出有一些小的差异。例如,queues视图是最近 OpenBSD 版本中的多个systat视图之一,但在本文写作时,FreeBSD 和 NetBSD 中并没有这个视图。
如果你需要比queues提供的当前状态表更全面的视图,可以尝试systat states,它提供了一个与top(1)进程列表非常相似的输出。以下是典型的systat states输出示例:
2 users Load 0.24 0.28 0.27 (1-16 of 895) Wed Apr 1 14:00:04 2015
PR D SRC DEST STATE AGE EXP PKTS BYTES RATE PEAK AVG RU G
udp O 192.168.103.1:56729 192.168.103.9:12345 1:0 8340m 25 372K 542M 1492 4774 1137 *
tcp I 10.168.103.15:47185 213.187.179.198:22 4:4 62377 86398 2954 613K 13264 23654 10 18
tcp I 10.168.103.15:2796 213.187.179.198:22 4:4 62368 86219 4014 679K 0 0 11 18
tcp I 10.168.103.15:15599 129.240.64.10:6667 4:4 61998 86375 9266 849K 0 58 14 *
tcp O 213.187.179.198:1559 129.240.64.10:6667 4:4 61998 86375 9266 849K 0 58 14 * 1
tcp I 10.168.103.15:8923 140.211.166.4:6667 4:4 61843 86385 15677 4794K 0 299 79 *
tcp O 213.187.179.198:8923 140.211.166.4:6667 4:4 61843 86385 15677 4794K 0 299 79 * 1
tcp I 10.168.103.15:47047 217.17.33.10:6667 4:4 61808 86385 7093 556K 0 88 9 *
tcp O 213.187.179.198:4704 217.17.33.10:6667 4:4 61808 86385 7093 556K 0 88 9 * 1
tcp I 10.168.103.15:30006 203.27.221.42:6667 4:4 61744 86375 6000 487K 0 49 8 *
tcp O 213.187.179.198:3000 203.27.221.42:6667 4:4 61744 86375 6000 487K 0 49 8 * 1
tcp I 10.168.103.15:31709 209.250.145.51:6667 4:4 61744 86385 6646 613K 0 114 10 *
tcp O 213.187.179.198:3170 209.250.145.51:6667 4:4 61744 86385 6646 613K 0 114 10 * 1
tcp I 192.168.103.254:5386 69.90.74.197:80 4:4 56718 29844 10 3282 0 0 0 *
tcp O 213.187.179.198:5386 69.90.74.197:80 4:4 56718 29844 10 3282 0 0 0 * 1
tcp I 10.168.103.15:33241 192.168.103.84:22 4:4 46916 82678 7555 897K 0 0 19 *
如果你的状态列表无法完全显示在一个屏幕上,只需通过实时显示翻页查看。
类似地,systat rules会显示加载的规则集的实时数据,包括数据包、字节和其他统计信息,如以下示例所示:
2 users Load 1.25 0.87 0.52 (1-16 of 239) Fri Apr 3 14:01:59 2015
RUL ANCHOR A DIR L Q IF PR K PKTS BYTES STATE MAX INFO
0 M In 26M 12G 4946K all max-mss 1440
1 M Out nfe0 4853K 3162M 94858 inet from 10.0.0.0/8 to any queue(q_def
2 M Out nfe0 3318K 2430M 61672 inet from 192.168.103.0/24 to any queue
3 M Out nfe0 tcp 6404K 4341M 134K from any to any port = www queue(q_web,
4 M Out nfe0 tcp 84298 43M 1594 from any to any port = https queue(q_we
5 M Out nfe0 tcp 502 34677 63 from any to any port = domain queue(q_d
6 M Out nfe0 udp 512K 64M 257K from any to any port = domain queue(q_d
7 M Out nfe0 icmp 11 1008 3 all queue(q_dns, q_pri)
8 B Any L 14638 1346K 0 return all
9 B Any Q 95 5628 0 return from <bruteforce> to any
10 P Any 1139K 1005M 757 all flags any
11 P In Q tcp K 18538 1350K 708 inet from any to any port = ftp
12 P Out tcp K 0 0 0 inet from 127.0.0.1/32 to any port = ftp
13 P Any 1421 128K 134 all flags any
14 P In L egres tcp K 1830K 87M 18933 inet from any to any port = smtp queue
15 P In L egres tcp K 31 5240 2 from <nospamd> to any port = smtp
systat rules视图特别有用,因为它提供了对完全解析并加载的规则集的实时查看。例如,如果你的规则集表现异常,rules视图可以帮助你找出问题所在,并显示数据包的流动情况。
systat 程序还提供了一个视图,呈现你通过命令行使用 pfctl -s status 获取的相同数据。下面的示例显示了 systat pf 的一部分输出。systat pf 视图提供了比大多数屏幕显示更多的信息,但你可以分页浏览数据的实时显示。
2 users Load 0.34 0.64 0.47 (1-16 of 51) Fri Apr 3 14:04:04 2015
TYPE NAME VALUE RATE NOTES
pf Status Enabled
pf Since 139:05:08
pf Debug err
pf Hostid 0x82aea702
nfe0 Bytes In 6217042900 IPv4
nfe0 Bytes In 0 IPv6
nfe0 Bytes Out 5993394114 IPv4
nfe0 Bytes Out 64 IPv6
nfe0 Packets In 12782504 IPv4, Passed
nfe0 Packets In 0 IPv6, Passed
nfe0 Packets In 11096 IPv4, Blocked
nfe0 Packets In 0 IPv6, Blocked
nfe0 Packets Out 12551463 IPv4, Passed
nfe0 Packets Out 1 IPv6, Passed
nfe0 Packets Out 167 IPv4, Blocked
systat 程序提供了许多其他视图,包括与网络相关的视图,例如 netstat、用于虚拟内存统计的 vmstat 以及用于按设备显示输入/输出统计的 iostat。你可以通过左右光标键切换所有 systat 视图。(请参阅 man systat 获取详细信息。)
使用 pftop 监控事物
如果你的系统没有包含带有 PF 相关视图的 systat 版本,你仍然可以使用 Can Erkin Acar 的 pftop 实时监控进出网络的流量。这个命令显示的是流量的运行快照。pftop 并不包含在基础系统中,但作为一个包可以使用——在 OpenBSD 和 FreeBSD 的 ports 系统中为 sysutils/pftop^([44]),在 NetBSD 通过 pkgsrc 提供 sysutils/pftop。以下是其输出的一个示例:
pfTop: Up State 1-17/771, View: default, Order: none, Cache: 10000 14:05:42
PR DIR SRC DEST STATE AGE EXP PKTS BYTES
udp Out 192.168.103.1:56729 192.168.103.9:12345 SINGLE:NO_TRAFFIC 8346m 22 373K 543M
tcp In 10.168.103.15:47185 213.187.179.198:22 ESTABLISHED:ESTABLISHED 62715 86395 3232 667K
tcp In 10.168.103.15:2796 213.187.179.198:22 ESTABLISHED:ESTABLISHED 62706 86369 4071 686K
tcp In 10.168.103.15:15599 129.240.64.10:6667 ESTABLISHED:ESTABLISHED 62336 86379 9318 854K
tcp Out 213.187.179.198:15599 129.240.64.10:6667 ESTABLISHED:ESTABLISHED 62336 86379 9318 854K
tcp In 10.168.103.15:8923 140.211.166.4:6667 ESTABLISHED:ESTABLISHED 62181 86380 15755 4821K
tcp Out 213.187.179.198:8923 140.211.166.4:6667 ESTABLISHED:ESTABLISHED 62181 86380 15755 4821K
tcp In 10.168.103.15:47047 217.17.33.10:6667 ESTABLISHED:ESTABLISHED 62146 86379 7132 559K
tcp Out 213.187.179.198:47047 217.17.33.10:6667 ESTABLISHED:ESTABLISHED 62146 86379 7132 559K
tcp In 10.168.103.15:30006 203.27.221.42:6667 ESTABLISHED:ESTABLISHED 62082 86380 6034 489K
tcp Out 213.187.179.198:30006 203.27.221.42:6667 ESTABLISHED:ESTABLISHED 62082 86380 6034 489K
tcp In 10.168.103.15:31709 209.250.145.51:6667 ESTABLISHED:ESTABLISHED 62082 86379 6685 617K
tcp Out 213.187.179.198:31709 209.250.145.51:6667 ESTABLISHED:ESTABLISHED 62082 86379 6685 617K
tcp In 192.168.103.254:53863 69.90.74.197:80 ESTABLISHED:ESTABLISHED 57056 29506 10 3282
tcp Out 213.187.179.198:53863 69.90.74.197:80 ESTABLISHED:ESTABLISHED 57056 29506 10 3282
tcp In 10.168.103.15:33241 192.168.103.84:22 ESTABLISHED:ESTABLISHED 47254 82340 7555 897K
tcp Out 10.168.103.15:33241 192.168.103.84:22 ESTABLISHED:ESTABLISHED 47254 82340 7555 897K
你可以使用 pftop 按多个不同的标准对连接进行排序,包括按 PF 规则、流量、连接时间、源地址和目标地址等。
使用 pfstat 绘制流量图
一旦你的系统启动并运行并开始生成数据,图形化表示流量数据是查看和分析数据的有用方式。绘制 PF 数据图表的一种方法是使用 pfstat,这是由 Daniel Hartmeier 开发的一个工具,用于提取并呈现 PF 自动生成的统计数据。pfstat 工具可以通过 OpenBSD 的包系统或作为端口 net/pfstat,通过 FreeBSD 的 ports 系统作为 sysutils/pfstat,以及通过 NetBSD pkgsrc 提供作为 sysutils/pfstat 来获得。
pfstat 程序收集你在配置文件中指定的数据,并以 JPG 或 PNG 图形文件的形式呈现这些数据。数据源可以是通过 /dev/pf 设备在本地系统上运行的 PF,或者是通过运行配套的 pfstatd 守护进程的远程计算机收集的数据。
要设置 pfstat,你只需决定哪些 PF 数据需要绘制成图,并且如何绘制,然后编写配置文件并启动 cron 作业以收集数据并生成图表。该程序附带一个注释良好的示例配置文件和一个有用的手册页。示例配置文件是编写自己配置文件的一个有用起点。例如,下面的 pfstat.conf 片段非常接近你在示例配置中会看到的内容:^([45])
collect 8 = global states inserts diff
collect 9 = global states removals diff
collect 10 = global states searches diff
image "/var/www/users/peter/bsdly.net/pfstat-states.jpg" {
from 1 days to now
width 980 height 300
left
graph 8 "inserts" "states/s" color 0 192 0 filled,
graph 9 "removals" "states/s" color 0 0 255
right
graph 10 "searches" "states/s" color 255 0 0
}
这里的配置从三个collect语句开始,每个数据系列都分配了一个唯一的数字标识符。在这里,我们捕获状态表中的插入、删除和查找次数。接下来是image定义,它指定要绘制的数据。from行指定要显示的时间段(from 1 days to now表示只显示过去 24 小时收集的数据)。width和height指定图形的大小,单位为每个方向的像素数。graph语句指定如何显示数据系列以及图例。每分钟收集一次状态插入、删除和查找数据,然后将收集的数据绘制成一天的数据图,产生的图形大致与图 9-1 中的图形相似。
图 9-1。状态表统计,24 小时时间尺度
可以调整图形以提供相同数据的更详细视图。例如,要以稍高的分辨率查看过去一小时的数据,可以将时间段更改为from 1 hours to now,并将尺寸设置为width 600 height 300。结果类似于图 9-2 中的图形。
图 9-2。状态表统计,1 小时时间尺度
pfstat主页位于www.benzedrine.cx/pfstat.html,其中包含了多个示例,并展示了来自benzedrine.cx域网关的实时数据图。通过阅读这些示例并结合你自己对流量的了解,你应该能够创建适合你站点需求的pfstat配置。
注意
除了pfstat,其他系统监控工具包至少提供了一些 PF 监控功能。其中一个工具包是流行的symon实用程序,它通常与symon数据收集器一起配置,在所有监控的系统上运行,至少有一个主机安装了symux和可选的syweb Web 接口。symon基于循环数据库工具(RRDtool),它提供了一个用于记录 PF 数据的有用接口,并通过syweb Web 接口展示 PF 统计信息的图形化界面。symon可以作为端口或包在 OpenBSD 和 FreeBSD 上使用,作为sysutils/symon,syweb Web 接口则作为www/syweb提供。
使用 pflow(4)收集 NetFlow 数据
NetFlow 是一种网络数据收集与分析方法,已催生出许多支持工具,用于记录和分析有关 TCP/IP 连接的数据。NetFlow 起源于思科,随着时间的推移,已成为各种网络设备中的一个重要功能,作为网络管理和分析工具。
NetFlow 数据模型将网络 流量 定义为一个单向的数据包序列,具有相同的源和目标 IP 地址及协议。例如,一个 TCP 连接在 NetFlow 数据中会表现为两个流:每个方向一个。
PF 数据可以通过在 OpenBSD 4.5 中引入的 pflow(4) 虚拟接口以及 pflow 状态选项提供给 NetFlow 工具。基本上,你期望在 NetFlow 样式的流记录中找到的所有信息,都可以从 PF 在状态表中保存的数据中轻松推导出来,而 pflow 接口提供了一种直接的方式,以这种易于处理且文档齐全的格式导出 PF 状态表数据。与其他日志记录一样,你可以在 PF 规则集中逐条规则启用 NetFlow 数据收集。
完整的基于 NetFlow 的网络监控系统由多个不同的部分组成。NetFlow 数据来自一个或多个 传感器,这些传感器生成有关网络流量的数据。传感器将流量数据转发到 收集器,该收集器存储接收到的数据。最后,一个 报告 或 分析 系统使你能够提取并处理这些数据。^([46])
设置 NetFlow 传感器
NetFlow 传感器需要两个组件:一个或多个配置好的 pflow(4) 设备,以及至少一条在规则集中启用 pflow 状态选项的 pass 规则。pflow 接口需要两个必需的参数:流量源 IP 地址和流量目标的 IP 地址与端口。以下是用于 /etc/hostname.pflow0 文件的 ifconfig 命令示例:
flowsrc 192.0.2.1 flowdst 192.0.2.105:3001
通过命令行,使用以下命令:
$ **sudo ifconfig pflow0 create flowsrc 192.0.2.1 flowdst 192.0.2.105:3001**
在这两种情况下,命令都将设置主机,以将 NetFlow 数据从源地址 192.0.2.1 发送到应该监听 NetFlow 数据的收集器 192.0.2.105,UDP 端口 3001。
注意
可以设置多个 pflow 设备,并为每个设备指定不同的流量目标。然而,目前无法在每条规则的基础上指定应该接收生成数据的 pflow 设备。
启用 pflow 设备后,在 /etc/pf.conf 中指定哪些 pass 规则应将 NetFlow 数据提供给传感器。例如,如果你主要关注收集客户的电子邮件流量到 IPv4 主机的数据,则该规则会设置必要的传感器:
pass out log inet proto tcp from <client> to port $email \
label client-email keep state (pflow)
当pflow首次引入 PF 时,早期用户的即时反应是,他们很可能希望将pflow选项添加到其规则集中大多数pass规则中。这促使 PF 开发者 Henning Brauer 引入了另一个有用的 PF 功能——设置状态默认值,该默认值适用于所有规则,除非另有指定。例如,如果在规则集的开始添加以下行,配置中的所有pass规则都将生成 NetFlow 数据,并通过pflow设备进行导出。
set state-defaults pflow
配置了至少一个pflow设备,并且在你的pf.conf中至少有一条规则生成用于通过pflow设备导出的数据后,你几乎完成了传感器的设置。然而,你可能仍然需要添加一条规则,允许 UDP 数据从你指定的流量数据源的 IP 地址流向收集器的 IP 地址和目标端口。一旦完成这最后一步,你应该可以开始集中精力收集数据,以便进一步处理。
NetFlow 数据收集、报告和分析
如果你的网站已经建立了基于 NetFlow 的收集和分析基础设施,可能已经添加了必要的配置,将 PF 生成的数据输入到数据收集和分析系统中。如果你还没有设置流量分析环境,仍然有许多可用的选项。
OpenBSD 的软件包系统提供了三个 NetFlow 收集和分析包:flow-tools、flowd和nfdump。^([47])这三个系统都有专门的、有能力的开发者和用户社区,并且提供各种附加功能,包括图形 Web 界面。flow-tools是许多站点流量分析设置中的主要组件。nfdump的爱好者则推崇nfsen分析包,它将nfdump工具集成到一个强大且灵活的基于 Web 的分析前端中,除此之外,它还会显示你在 GUI 中选择的命令行等效内容。当你需要深入挖掘数据,而 GUI 的选择无法满足时,命令行显示会非常有用。你可以复制 GUI 中显示的命令,并在shell会话或脚本中的nfdump命令行上做进一步调整,以提取你所需要的确切数据。
选择收集器
收集器的选择在某种程度上与分析包的选择相关。也许因为收集器倾向于以自己独特的格式存储流量数据,导致大多数报告和分析后台都是根据其中一个或另一个收集器的特性开发的。
无论你选择哪个 NetFlow 收集器,熟悉的日志记录警告仍然适用:详细的流量日志信息将需要存储空间。对于 NetFlow 来说,每个流将生成一个相对固定大小的记录,根据一些经验数据,即使是忙碌站点的适度收集配置,也能每天生成数 GB 的 NetFlow 数据。你所需的存储空间直接与连接数量以及你保留原始 NetFlow 数据的时间长度成正比。最后,记录和存储如此详细的流量日志可能会带来法律上的影响。
收集器通常提供过滤功能,可以让你丢弃特定主机或网络的数据,甚至丢弃某些 NetFlow 记录的部分内容,这些过滤可以是全局性的,也可以针对特定主机或网络的数据。
为了说明一些基本的 NetFlow 收集以及如何提取已收集数据的子集以便进一步分析,我们将使用 flowd,这是由长期 OpenBSD 开发者 Damien Miller 开发的,并且可以通过软件包系统获取(在 OpenBSD 中为 net/flowd,在 FreeBSD 中为 net-mgmt/flowd)。
我之所以选择使用 flowd,主要是因为它是为了小巧、简单和安全而开发的。正如你所看到的,flowd 依然能够非常有用且灵活。使用其他工具进行流数据操作在某些细节上会有所不同,但基本原理是相同的。
与其他 NetFlow 收集器套件相比,flowd 非常紧凑,只有两个可执行程序——收集器守护进程 flowd 和流过滤与展示程序 flowd-reader——以及支持库和控制配置文件。文档内容足够,尽管有些简略,示例的 /etc/flowd.conf 文件包含了大量注释。根据手册页和示例配置文件中的注释,你应该能很快创建一个有用的收集器配置。
在去掉所有注释行后——使用 grep -v \# /etc/flowd.conf 或类似命令——一个非常基础的 flowd 配置可能如下所示:
logfile "/var/log/flowd"
listen on 192.0.2.105:3001
flow source 192.0.2.1
store ALL
尽管该配置几乎没有比前面设置传感器时提到的 pflow 接口配置多出多少信息,但它确实包括了两个重要项:
-
logfile行告诉我们收集的数据将存储的位置(并揭示了flowd倾向于将所有数据存储在一个单独的文件中)。 -
最后一行告诉我们
flowd将存储从指定流源接收到的所有字段数据。
配置完成后,启动 flowd 守护进程,几乎立刻你就会看到 /var/log/flowd 文件在网络流量通过网关并收集流记录时不断增长。过一段时间后,你应该能够使用 flowd 的伴随程序 flowd-reader 查看这些数据。例如,当所有字段都被存储时,从 NAT 局域网内某个主机进行的一个名称查找在 flowd-reader 的默认视图中看起来是这样的:
$ **sudo flowd-reader /var/log/flowd**
FLOW recv_time 2011-04-01T21:15:53.607179 proto 17 tcpflags 00 tos 00 agent
[213.187.179.198] src [192.0.2.254]:55108 dst [192.0.2.1]:53 packets 1 octets
62
FLOW recv_time 2011-04-01T21:15:53.607179 proto 17 tcpflags 00 tos 00 agent
[213.187.179.198] src [192.0.2.1]:53 dst [192.0.2.254]:55108 packets 1 octets
129
请注意,查找生成了两个流:每个方向一个。
第一个流主要由接收时间标识,后面是使用的协议(协议 17 是 UDP,如 etc/protocols 中所示)。该连接的 TCP 和 TOS 标志都未设置,收集器从我们的网关 192.0.2.1 收到数据。流的源地址是 192.0.2.254,源端口是 55108,目的地址是 192.0.2.1,源端口是 53,通常是 DNS 端口。该流包含 1 个数据包,负载为 62 字节。返回流在同一时间由收集器接收,我们可以看到该流的源和目的地址交换,负载略大,为 129 字节。flowd-reader 的输出格式适合通过正则表达式进行解析,以便在报告工具或绘图软件中进行后处理。
你可能认为这些数据是任何人想要了解特定网络流集的全部内容,但其实可以提取更详细的信息。例如,使用 flowd-reader -v 选项来获取详细输出,你可能会看到如下内容:
FLOW recv_time 2011-04-01T21:15:53.607179 proto 17 tcpflags 00 tos 00
agent [213.187.179.198] src [192.0.2.254]:55108 dst [192.0.2.1]:53 gateway
[0.0.0.0] packets 1 octets 62 in_if 0 out_if 0 sys_uptime_ms 1w5d19m59s.000
time_sec 2011-04-01T21:15:53 time_nanosec 103798508 netflow ver 5 flow_start
1w5d19m24s.000 flow_finish 1w5d19m29s.000 src_AS 0 src_masklen 0 dst_AS 0
dst_masklen 0 engine_type 10752 engine_id 10752 seq 5184351 source 0 crc32
759adcbd
FLOW recv_time 2011-04-01T21:15:53.607179 proto 17 tcpflags 00 tos 00
agent [213.187.179.198] src [192.0.2.1]:53 dst [192.0.2.254]:55108 gateway
[0.0.0.0] packets 1 octets 129 in_if 0 out_if 0 sys_uptime_ms 1w5d19m59s.000
time_sec 2011-04-01T21:15:53 time_nanosec 103798508 netflow ver 5 flow_start
1w5d19m24s.000 flow_finish 1w5d19m29s.000 src_AS 0 src_masklen 0 dst_AS 0
dst_masklen 0 engine_type 10752 engine_id 10752 seq 5184351 source 0 crc32
f43cbb22
gateway 字段表示传感器本身作为此连接的网关。你可以看到涉及的接口列表(in_if 和 out_if 值)、传感器的系统正常运行时间(sys_uptime_ms)以及许多其他参数——如 AS 数字(src_AS 和 dst_AS)——它们可能在不同的上下文中对统计或过滤有用。再次强调,输出非常适合通过正则表达式进行过滤。
你不需要依赖外部软件来对从 pflow 传感器收集的数据进行初步过滤。flowd 本身提供了一系列过滤功能,使你可以只存储所需的数据。一个方法是将过滤表达式放入 flowd.conf 中,如下所示(已去除注释以节省空间):
logfile "/var/log/flowd.compact"
listen on 192.0.2.105:3001
flow source 192.0.2.1
store SRC_ADDR
store DST_ADDR
store SRCDST_PORT
store PACKETS
store OCTETS
internalnet = "192.0.2.0/24"
unwired = "10.168.103.0/24"
discard src $internalnet
discard dst $internalnet
discard src $unwired
discard dst $unwired
你可以选择仅存储流记录中的某些字段。例如,在只有一个收集器或代理的配置中,agent 字段没有任何实用价值,因此不需要存储。在这种配置中,我们选择只存储源地址和目的地址及端口、数据包数和字节数。
你可以进一步限制存储的数据。宏 internalnet 和 unwired 扩展为两个 NATed 本地网络,宏定义后的四行 discard 表示 flowd 丢弃任何关于源地址或目的地址在这两个本地网络中的流量数据。结果是一个更紧凑的数据集,专门针对你的特定需求,你只会看到可路由的地址和传感器网关外部接口的地址:
$ **sudo flowd-reader /var/log/flowd.compact | head**
FLOW src [193.213.112.71]:38468 dst [192.0.2.1]:53 packets 1 octets 79
FLOW src [192.0.2.1]:53 dst [193.213.112.71]:38468 packets 1 octets 126
FLOW src [200.91.75.5]:33773 dst [192.0.2.1]:53 packets 1 octets 66
FLOW src [192.0.2.1]:53 dst [200.91.75.5]:33773 packets 1 octets 245
FLOW src [200.91.75.5]:3310 dst [192.0.2.1]:53 packets 1 octets 75
FLOW src [192.0.2.1]:53 dst [200.91.75.5]:3310 packets 1 octets 199
FLOW src [200.91.75.5]:2874 dst [192.0.2.1]:53 packets 1 octets 75
FLOW src [192.0.2.1]:53 dst [200.91.75.5]:2874 packets 1 octets 122
FLOW src [192.0.2.1]:15393 dst [158.37.91.134]:123 packets 1 octets 76
FLOW src [158.37.91.134]:123 dst [192.0.2.1]:15393 packets 1 octets 76
即使使用详细选项,flowd-reader 的显示也仅显示你在过滤配置中明确指定的内容:
$ **sudo flowd-reader -v /var/log/flowd.compact | head**
LOGFILE /var/log/flowd.compact
FLOW src [193.213.112.71]:38468 dst [192.0.2.1]:53 packets 1 octets 79
FLOW src [192.0.2.1]:53 dst [193.213.112.71]:38468 packets 1 octets 126
FLOW src [200.91.75.5]:33773 dst [192.0.2.1]:53 packets 1 octets 66
FLOW src [192.0.2.1]:53 dst [200.91.75.5]:33773 packets 1 octets 245
FLOW src [200.91.75.5]:3310 dst [192.0.2.1]:53 packets 1 octets 75
FLOW src [192.0.2.1]:53 dst [200.91.75.5]:3310 packets 1 octets 199
FLOW src [200.91.75.5]:2874 dst [192.0.2.1]:53 packets 1 octets 75
FLOW src [192.0.2.1]:53 dst [200.91.75.5]:2874 packets 1 octets 122
FLOW src [192.0.2.1]:15393 dst [158.37.91.134]:123 packets 1 octets 76
幸运的是,flowd 不会强迫你在收集器从传感器接收流数据时就做出所有的过滤决策。通过使用 -f 标志,你可以指定一个单独的文件,包含过滤语句,从更大的收集的流数据集中提取特定数据。例如,为了查看指向 Web 服务器的 HTTP 流量,你可以编写一个过滤器,仅存储目标地址为你的 Web 服务器地址并且目标端口为 TCP 端口 80 的流量,或者源地址为你的 Web 服务器并且源端口为 TCP 端口 80 的流量:
webserver = 192.0.2.227
discard all
accept dst $webserver port 80 proto tcp
accept src $webserver port 80 proto tcp
store RECV_TIME
store SRC_ADDR
store DST_ADDR
store PACKETS
store OCTETS
假设你将过滤器存储在 towebserver.flowdfilter 中,那么你可以从 /var/log/flowd 中提取符合过滤条件的流量,像这样:
$ **sudo flowd-reader -v -f towebserver.flowdfilter /var/log/flowd | tail**
FLOW recv_time 2011-04-01T21:13:15.505524 src [89.250.115.174] dst
[192.0.2.227] packets 6 octets 414
FLOW recv_time 2011-04-01T21:13:15.505524 src [192.0.2.227] dst
[89.250.115.174] packets 4 octets 725
FLOW recv_time 2011-04-01T21:13:49.605833 src [216.99.96.53] dst [192.0.2.227]
packets 141 octets 7481
FLOW recv_time 2011-04-01T21:13:49.605833 src [192.0.2.227] dst [216.99.96.53]
packets 212 octets 308264
FLOW recv_time 2011-04-01T21:14:04.606002 src [91.121.94.14] dst [192.0.2.227]
packets 125 octets 6634
FLOW recv_time 2011-04-01T21:14:04.606002 src [192.0.2.227] dst [91.121.94.14]
packets 213 octets 308316
FLOW recv_time 2011-04-01T21:14:38.606384 src [207.46.199.44] dst
[192.0.2.227] packets 10 octets 642
FLOW recv_time 2011-04-01T21:14:38.606384 src [192.0.2.227] dst
[207.46.199.44] packets 13 octets 16438
FLOW recv_time 2011-04-01T21:15:14.606768 src [213.187.176.94] dst
[192.0.2.227] packets 141 octets 7469
FLOW recv_time 2011-04-01T21:15:14.606768 src [192.0.2.227] dst
[213.187.176.94] packets 213 octets 308278
除了这里展示的过滤选项外,flowd 过滤功能还支持许多其他选项。其中一些选项在其他过滤上下文中可能很熟悉,例如 PF,包括一系列面向网络的参数;其他选项则更多地面向提取特定日期或时间段内来源的流量数据以及其他面向存储的参数。完整的说明,像往常一样,可以在 man flowd.conf 中找到。
一旦你提取了所需的数据,你可以使用几种工具来处理和展示数据。
使用 pfflowd 收集 NetFlow 数据
对于不支持通过 pflow 导出 NetFlow 数据的系统,可以通过 pfflowd 包来实现 NetFlow 支持。正如我们在上一节中看到的那样,PF 状态表数据与 NetFlow 数据模型非常契合,而 pfflowd 的目的是记录来自本地系统的 pfsync 设备的状态变化。一旦启用,pfflowd 充当 NetFlow 传感器,将 pfsync 数据转换为 NetFlow 格式,并传输到网络上的 NetFlow 收集器。
pfflowd 工具由 Damien Miller 编写并维护,提供于 www.mindrot.org/projects/pfflowd/,也可以通过 OpenBSD 和 FreeBSD 的包管理系统以 net/pfflowd 的形式安装。在 NetBSD 上由于缺乏对 pfsync 的支持,pfflowd 在该平台上暂时不可用。
SNMP 工具和与 PF 相关的 SNMP MIB
简单网络管理协议(SNMP) 是为了让网络管理员收集和监控其系统运行的关键数据,并从一个集中式系统对多个网络节点进行配置更改而设计的。^([48]) SNMP 协议提供了一个明确定义的接口,并有一个方法来扩展 管理信息库(MIB),它定义了被管理的设备和对象。
无论是专有的还是开源的网络管理和监控系统,一般都支持某种形式的 SNMP,在一些产品中,这甚至是核心功能。在 BSD 系统上,SNMP 支持通常通过net-snmp包提供,该包为你提供了检索 SNMP 数据和收集数据供管理系统检索所需的工具。该包在 OpenBSD 上称为net/net-snmp,在 FreeBSD 上称为net-mgmt/net-snmp,在 NetBSD 上也称为net/net-snmp。OpenBSD 的snmpd(主要由 Reyk Floeter 编写)作为 OpenBSD 4.3 版本的一部分首次亮相,并实现了所有所需的 SNMP 功能。(有关详细信息,请参阅man snmpd和man snmpd.conf。)
有一些 MIB(管理信息库)可以让 SNMP 监控获取 PF 数据。Joel Knight 维护这些 MIB,用于获取 PF、CARP 和 OpenBSD 内核传感器的数据,并提供下载,地址是 www.packetmischief.ca/openbsd/snmp/。该网站还提供了net-snmp包的补丁,以便集成 OpenBSD 的 MIB。
安装包和扩展后,你的支持 SNMP 的监控系统将能够详细监控 PF 数据。(FreeBSD 的bsnmpd包括 PF 模块,详细信息请参见bsnmpd手册页面。)
日志数据作为有效调试的基础
在本章中,我们介绍了如何收集、显示和解释 PF 启用系统的运行数据。了解如何查找并使用有关系统行为的信息,对多种目的都非常有用。
跟踪运行系统的状态本身就很有用,但在测试配置时,能够读取和解释日志数据更加重要。日志数据的另一个重要用途是跟踪你在配置中所做更改的影响,比如调整系统以获得最佳性能时。在下一章中,我们将重点介绍如何根据日志数据和其他观察结果检查和优化配置,以获得最佳性能。
^([44]) 在 OpenBSD 上,所有pftop功能都包含在各种systat视图中,正如前一节所描述的那样。
^([45]) 配置示例中列出的颜色值将给你一个包含红色、蓝色和绿色线条的图表。为了本书的印刷版,我们将颜色更改为灰度值:0 192 0变为105 105 105,0 0 255变为192 192 192,而255 0 0变为0 0 0。
^([46]) 要更深入地了解基于 NetFlow 工具的网络分析,请参考 Michael W. Lucas 的《网络流量分析》(No Starch Press, 2010)。
^([47]) flow-tools 和 nfdump 的活跃维护项目主页分别是 code.google.com/p/flow-tools/ 和 nfdump.sourceforge.net/。(旧版本仍应可以从 www.splintered.net/sw/flow-tools/ 获取。)nfsen 的 Web 前端有一个项目页面,地址为 nfsen.sourceforge.net/. 获取有关flowd的最新信息,请访问 www.mindrot.org/flowd.html.
^([48]) 该协议于 1988 年 8 月首次通过 RFC 1067 发布,现在已经进入第三个主要版本,定义在 RFC 3411 至 3418 中。
第十章:精确调整你的配置

到现在为止,你已经花费了大量时间设计你的网络并在 PF 配置中实现这个设计。将配置调整到完美——也就是说,消除任何剩余的配置错误和低效——有时可能非常具有挑战性。
本章描述了将帮助你获得所需配置的选项和方法。首先,我们将查看一些全球性选项和设置,它们会对配置的行为产生深远的影响。
你可以调整的内容和你可能不应该轻易更改的内容
网络配置本质上是非常可调的。在浏览 pf.conf 手册页或其他参考文档时,你可能会被可以调整的选项和设置的数量所淹没,以便获得一个完美优化的配置。
请记住,对于 PF 来说,默认设置对于大多数配置来说是合理的。有些设置和变量适合调优;其他设置则应附带一个大警告,表示只有在非常特殊的情况下,才应该进行调整(如果有必要的话)。
在这里,我们将看看一些你应该了解的全局设置,尽管在大多数情况下你不需要更改它们。
这些选项以 set 选项设置 形式写出,并且要放在任何宏定义之后,但在翻译或过滤规则之前。
注意
如果你阅读 pf.conf 手册页,你会发现还有一些其他选项。然而,大多数选项在网络测试和性能调优的上下文中并不相关。
阻止策略
block-policy 选项决定了 PF 是否会向尝试创建连接但被随后阻止的主机提供反馈。如果提供反馈,该选项有两个可能的值:
-
drop在没有反馈的情况下丢弃被阻止的数据包。 -
return用状态码返回,例如Connection refused或类似信息。
阻止策略的正确策略多年来一直是一个备受讨论的话题。block-policy 的默认设置是 drop,这意味着数据包会被静默丢弃,没有任何反馈。然而,静默丢弃数据包会让发送方可能重新发送那些未确认的数据包,而不是断开连接。因此,发送方可能会继续发送,直到相关的超时计数器到期。如果你不希望这种行为成为默认设置,可以将阻止策略设置为 return:
set block-policy return
这个设置意味着发送方的网络堆栈将收到一个明确的信号,表示连接被拒绝。
无论你使用哪种block-policy选项,它都会指定全局默认的阻塞策略。然而,如果需要,你仍然可以为特定规则修改阻塞类型。例如,你可以将暴力破解保护规则集从第六章更改为将block-policy设置为return,但也可以使用block drop quick from <bruteforce>来让暴力破解者浪费时间,如果他们在被添加到<bruteforce>表后还继续停留。你还可以为来自不可路由地址的流量指定drop,这些流量通过你的面向互联网的接口进入,或者其他明显不受欢迎的流量,例如尝试将你的设备用于放大分布式拒绝服务(DDoS)攻击。^([49])
跳过接口
skip选项让你将特定接口排除在所有 PF 处理之外。其效果类似于该接口上的pass-all规则,但实际上它禁用了该接口上的所有 PF 处理。例如,你可以使用此选项来禁用回环接口组的过滤,在大多数配置中,在该接口上进行过滤对于安全性或便利性几乎没有帮助:
set skip on lo
实际上,在回环接口上进行过滤几乎没有任何用处,而且可能会导致一些常见程序和服务出现奇怪的结果。默认情况下,skip未设置,这意味着所有配置的接口都可以参与 PF 处理。除了让规则集稍微简化之外,在不需要过滤的接口上设置skip会带来轻微的性能提升。
状态策略
state-policy选项指定 PF 如何将数据包与状态表匹配。它有两个可能的值:
-
使用默认的
floating状态策略,流量可以在所有接口上匹配状态,而不仅仅是状态创建的那个接口。 -
使用
if-bound策略时,流量将只在创建状态的接口上匹配;其他接口上的流量将无法匹配现有的状态。
与block-policy选项类似,此选项指定全局的状态匹配策略,但如果需要,你可以在每条规则的基础上覆盖状态匹配策略。例如,在使用默认floating策略的规则集中,你可以有如下规则:
pass out on egress inet proto tcp to any port $allowed modulate state (if-bound)
使用此规则,任何尝试重新进入的返回流量必须通过创建状态的相同接口才能匹配状态表条目。
if-bound策略有用的情况非常少,因此你应该将此设置保留为默认值。
状态默认
在 OpenBSD 4.5 中引入的state-defaults选项使你可以将特定的状态选项设置为规则集中所有规则的默认选项——除非这些状态选项在单个规则中被其他选项显式覆盖。
这是一个常见的例子:
set state-defaults pflow
该选项设置配置中的所有pass规则,以生成通过pflow设备导出的 NetFlow 数据。
在某些情况下,将状态跟踪选项(例如连接限制)作为全局状态默认值应用于整个规则集是有意义的。以下是一个示例:
set state-defaults max 1500, max-src-conn 100, source-track rule
此选项将每条规则的默认最大状态条目数设置为 1,500,单个主机的最大并发连接数为 100,并且每个加载的规则集中的每条规则都有单独的限制。
对于 keep state 中的任何有效选项,它们也可以包含在 set state-defaults 语句中。如果有一些状态选项不是系统默认的,但您希望将其应用于配置中的所有规则,使用这种方式设置状态默认值是很有用的。
超时
timeout 选项设置了与状态表条目相关的超时和相关选项。大多数可用的参数是特定于协议的值,以秒为单位存储,并以 tcp.、udp.、icmp. 和 other. 为前缀。然而,adaptive.start 和 adaptive.end 表示状态表条目的数量。
以下 timeout 选项会影响状态表的内存使用和在一定程度上的查找速度:
-
adaptive.start和adaptive.end值设置了当状态条目数量达到adaptive.start值后,超时值缩减的限制。当状态数量达到adaptive.end时,所有超时都被设置为 0,实际上立即使所有状态过期。默认值为 6,000 和 12,000(分别计算为状态限制的 60% 和 120%)。这些设置与通过limit选项设置的内存池限制参数密切相关。 -
interval值表示清除过期状态和片段之间的秒数。默认值为 10 秒。 -
frag值表示在丢弃之前,片段将保持在未组装状态的秒数。默认值为 30 秒。 -
设置后,
src.track表示源跟踪数据将在最后一个状态过期后保持的秒数。默认值为 0 秒。
您可以通过 pfctl -s timeouts 检查所有 timeout 参数的当前设置。例如,以下显示了一个使用默认值运行的系统:
$ **sudo pfctl -s timeouts**
tcp.first 120s
tcp.opening 30s
tcp.established 86400s
tcp.closing 900s
tcp.finwait 45s
tcp.closed 90s
tcp.tsdiff 30s
udp.first 60s
udp.single 30s
udp.multiple 60s
icmp.first 20s
icmp.error 10s
other.first 60s
other.single 30s
other.multiple 60s
frag 30s
interval 10s
adaptive.start 6000 states
adaptive.end 12000 states
src.track 0s
这些选项可以用来调整您的设置以优化性能。然而,从默认值更改协议特定的设置会带来一定的风险,因为有效但空闲的连接可能会被过早丢弃或直接阻止。
限制
limit 选项设置 PF 用于状态表和地址表的内存池大小。这些是硬性限制,因此您可能需要根据各种原因增加或调整这些值。如果您的网络繁忙,且需要的数量超过了默认值的限制,或者如果您的设置需要较大的地址表或大量的表,那么本节将非常相关。
请记住,通过内存池可用的总内存量来自内核内存空间,而总可用量是总可用内核内存的函数。内核内存在一定程度上是动态的,但分配给内核的内存量永远不能等于或超过系统中的所有物理内存。 (如果发生这种情况,将没有空间运行用户模式程序。)
可用池内存量取决于您使用的硬件平台以及特定于本地系统的一些难以预测的变量。在 i386 架构上,最大内核内存在 768MB 至 1GB 范围内,这取决于多种因素,包括系统中的硬件设备数量和类型。实际可用于内存池分配的数量来自这个总数,同样取决于一些特定于系统的变量。
要检查当前的limit设置,请使用pfctl -sm。典型的输出如下:
$ **sudo pfctl -sm**
states hard limit 10000
src-nodes hard limit 10000
frags hard limit 5000
tables hard limit 1000
table-entries hard limit 200000
要更改这些值,请编辑pf.conf以包括一个或多个包含新limit值的行。例如,您可以使用以下行提高状态数的硬限制到 25,000,并将表条目数的硬限制提高到 300,000:
set limit states 25000
set limit table-entries 300000
你也可以在单行中通过括号同时设置多个limit参数:
set limit { states 25000, src-nodes 25000, table-entries 300000 }
总之,除非为较大的安装可能增加这三个参数,否则你几乎肯定不应该改变任何限制。然而,如果确实这样做,监视系统日志以获取任何指示您更改的限制具有不良副作用或不适合可用内存的迹象是非常重要的。将debug级别设置为更高值对于观察调整limit参数的效果可能非常有用。
调试
debug选项决定了 PF 在kern.debug日志级别下生成的错误信息(如果有)。默认值为err,意味着只记录严重错误。自 OpenBSD 4.7 以来,这里的日志级别对应于常规syslog级别,从emerg(记录紧急情况)、alert(可更正但非常严重的错误)、crit(记录关键条件)、err(记录错误)到warning(记录警告)、notice(记录异常情况)、info(记录信息消息)和debug(记录完整调试信息,可能只对开发人员有用)。
注意
在 OpenBSD 4.7 之前的版本中,PF 使用其自己的日志级别系统,默认为urgent(相当于新系统中的err)。其他可能的设置包括none(无消息)、misc(比urgent稍多报告)、loud(对大多数操作生成状态消息)。为了兼容性,pfctl解析器仍然接受旧式调试级别。
在我的一个网关以debug级别运行了一段时间后,/var/log/messages文件的一个典型块看起来是这样的:
**$ tail -f /var/log/messages**
Oct 4 11:41:11 skapet /bsd: pf_map_addr: selected address 194.54.107.19
Oct 4 11:41:15 skapet /bsd: pf: loose state match: TCP 194.54.107.19:25
194.54.107.19:25 158.36.191.135:62458 [lo=3178647045 high=3178664421 win=33304
modulator=0 wscale=1] [lo=3111401744 high=3111468309 win=17376 modulator=0
wscale=0] 9:9 R seq=3178647045 (3178647044) ack=3111401744 len=0 ackskew=0
pkts=9:12
Oct 4 11:41:15 skapet /bsd: pf: loose state match: TCP 194.54.107.19:25
194.54.107.19:25 158.36.191.135:62458 [lo=3178647045 high=3178664421 win=33304
modulator=0 wscale=1] [lo=3111401744 high=3111468309 win=17376 modulator=0
wscale=0] 10:10 R seq=3178647045 (3178647044) ack=3111401744 len=0 ackskew=0
pkts=10:12
Oct 4 11:42:24 skapet /bsd: pf_map_addr: selected address 194.54.107.19
在debug级别,PF 会重复报告当前正在处理的接口的 IP 地址。在选择的地址信息之间,PF 会对同一数据包发出两次警告,提示序列号处于期望范围的边缘。初看起来,这种详细级别似乎有点压倒性,但在某些情况下,研究这种输出是诊断问题的最佳方法,之后还可以检查是否你的解决方案起到了作用。
注意
此选项可以通过命令行使用pfctl -x设置,后跟你想要的调试级别。命令pfctl -x debug将提供最大调试信息;pfctl -x none会完全关闭调试消息。
请记住,一些debug设置可能会产生大量日志数据,在极端情况下,可能会影响性能,甚至达到自我拒绝服务的程度。
规则集优化
ruleset-optimization选项启用或设置规则集优化器的模式。OpenBSD 4.1 及等效版本中,ruleset-optimization的默认设置为none,这意味着在加载时不执行规则集优化。从 OpenBSD 4.2 开始,默认设置为basic,这意味着在规则集加载时,优化器会执行以下操作:
-
删除重复的规则
-
删除作为其他规则子集的规则
-
如果适合,合并规则到表中(典型的规则到表优化是根据相同标准(源和/或目标地址除外)来进行传递、重定向或阻止的规则)
-
更改规则顺序以提高性能
例如,假设你有宏tcp_services = { ssh, www, https },并结合规则pass proto tcp from any to self port $tcp_services。在规则集中其他地方,你还有一个不同的规则,表示pass proto tcp from any to self port ssh。第二个规则显然是第一个规则的子集,它们可以合并为一个。另一个常见的组合是有一个pass规则,如pass proto tcp from any to int_if:network port $tcp_services,其余的pass规则相同,目标地址都在int_if:network范围内。
在ruleset-optimization设置为profile时,优化器会根据网络流量分析已加载的规则集,以确定quick规则的最佳顺序。
你还可以通过命令行使用pfctl设置优化选项的值:
$ **sudo pfctl -o basic**
此示例启用basic模式下的规则集优化。
由于优化可能会移除或重新排序规则,因此某些统计数据的含义—主要是每条规则的评估次数—可能会以难以预测的方式发生变化。然而,在大多数情况下,效果是微不足道的。
优化
optimization选项指定用于状态超时处理的配置文件。可选值包括normal、high-latency、satellite、aggressive和conservative。建议保持默认的normal设置,除非你有非常特定的需求。
high-latency 和 satellite 是同义词;使用这些值时,状态会更慢地过期,以补偿潜在的高延迟。
aggressive 设置会提前过期状态以节省内存。原则上,这可能会增加丢弃空闲但有效连接的风险,尤其是在系统已经接近负载和流量极限时,但根据经验,aggressive 优化设置很少,甚至从未干扰有效流量。
conservative 设置尽可能地保留状态和空闲连接,代价是会增加一些内存使用。
碎片重组
与 scrub 相关的碎片重组选项在 OpenBSD 4.6 中进行了显著的重构,引入了新的 set reassemble 选项,用于开启或关闭碎片包的重组。如果 reassemble 设置为 off,则碎片包会被丢弃,除非它们匹配一个包含 fragment 选项的规则。默认设置是 set reassemble on,这意味着碎片包会被重组,且在重组后的包中,原本在单个碎片上设置的不分片位会被清除。
清理你的流量
接下来我们将讨论的两个特性,scrub 和 antispoof,有一个共同的主题:它们提供了自动保护,防止网络流量中潜在的危险杂乱物。总的来说,它们通常被称为“网络卫生”工具,因为它们大大净化了网络流量。
包规范化与清理:OpenBSD 4.5 及更早版本
在 PF 版本(包括 OpenBSD 4.5 及之前版本)中,scrub 关键字启用网络流量规范化。使用 scrub 时,碎片化的包会被重新组装,非法的碎片——如重叠的碎片——会被丢弃,从而使得最终的包完整且明确无误。
启用 scrub 提供了一定程度的保护,防止某些基于包碎片错误处理的攻击。^([50]) 有许多附加选项可以使用,但最简单的形式适用于大多数配置:
scrub in
为了使某些服务能与 scrub 一起工作,必须设置特定选项。例如,某些 NFS 实现根本无法与 scrub 一起使用,除非你使用 no-df 参数清除任何设置了不分片位的包中的该位。某些服务、操作系统和网络配置的组合可能需要使用一些更为特殊的 scrub 选项。
包括清理的包规范化:OpenBSD 4.6 及之后版本
在 OpenBSD 4.6 中,scrub 从独立规则内容降级为一个可以附加到 pass 或 match 规则的动作(引入 match 规则是 OpenBSD 4.6 中 PF 的主要新特性之一)。在同一版本中对 scrub 代码进行重写时,另一个重要的变化是,众多的包重组选项被删除,取而代之的是新的 reassemble 选项,该选项简单地打开或关闭重组。
使用新的scrub语法时,你需要在括号中提供至少一个选项。以下配置对于我管理的几个网络来说效果很好:
match in all scrub (no-df max-mss 1440)
这个选项会清除“不要分片”位,并将最大分段大小设置为 1,440 字节。
还有其他变种,尽管scrub选项在 OpenBSD 4.6 版本中有所缩减,但通过查阅手册页和进行一些实验,你应该能够满足特定需求。对于大多数设置,像前面引用的全局匹配规则是合适的,但请记住,如果需要,你可以在每个规则的基础上变化scrub选项。
如果你发现需要调试与scrub相关的问题,请查阅pf.conf手册页并咨询相关邮件列表的专家。
使用 antispoof 防止地址伪造
一些非常有用和常见的数据包处理操作可以编写为 PF 规则,但如果不小心,它们会变得冗长、复杂且容易出错。因此,antispoof被实现为一个常见的特殊情况过滤和阻止机制。该机制通过阻止看似来自不可能逻辑方向的数据包来防止伪造或伪装的 IP 地址活动。
使用antispoof,你可以指定希望过滤掉来自全世界的伪造流量,以及可能来自你自己网络的伪造数据包。图 10-1 展示了这个概念。
图 10-1. antispoof 丢弃来自错误网络的数据包。
要建立图中所示的那种保护,需为图示网络中的两个接口指定antispoof,并使用以下两行配置:
antispoof for $ext_if
antispoof for $int_if
这些行扩展为复杂的规则。第一条规则阻止来自看似直接连接到 antispoof 接口的网络的流量,但该流量却通过了不同的接口。第二条规则对内部接口执行相同的功能,阻止任何带有看似本地网络地址的流量,通过非$int_if接口到达。然而,请记住,antispoof并不旨在检测与运行 PF 的机器不直接连接的远程网络的地址伪造。
测试你的设置
现在是时候拿出精确的规格,来描述你的设置应该如何工作了。
我们示例网络的物理布局以一个网关为中心,该网关通过 $ext_if 连接到互联网。通过 $int_if 连接到网关的是一个本地网络,该网络包含工作站,可能还包括一个或多个供本地使用的服务器。最后,我们有一个DMZ,它通过 $dmz_if 连接,里面的服务器提供服务给本地网络和互联网。图 10-2 显示了该网络的逻辑布局。
图 10-2. 带有 DMZ 服务器的网络
相应的规则集规范大致如下:
-
我们网络外的机器应该能够访问我们在 DMZ 中的服务器提供的服务,但不应访问本地网络。
-
我们本地网络中的机器,连接到
$int_if,应能够访问 DMZ 中的服务器提供的服务,并访问我们网络外定义的服务列表。 -
DMZ 中的机器应能访问外部世界的一些网络服务。
当前的任务是确保我们现有的规则集确实实现了规范。我们需要测试该设置。一个有用的测试是尝试表 10-1 中的测试序列。
你的配置可能需要进行其他测试,或者在某些细节上有所不同,但你实际的测试场景应该明确说明如何记录数据包和连接。关键是,在开始测试之前,你应该决定每个测试用例的预期和期望结果是什么。
通常,你应使用典型用户可能使用的应用程序进行测试,比如不同操作系统上的网页浏览器或邮件客户端。连接应该按照规范简单地成功或失败。如果一个或多个基本测试结果不符合预期,可以开始调试你的规则集。
表 10-1. 示例规则集测试用例序列
| 测试操作 | 预期结果 |
|---|---|
| 尝试从本地网络到 DMZ 中每个服务器的允许端口进行连接。 | 连接应通过。 |
| 尝试从本地网络到网络外服务器的每个允许端口进行连接。 | 连接应通过。 |
| 尝试从 DMZ 到本地网络的任何端口进行连接。 | 连接应被阻止。 |
| 尝试从 DMZ 到你网络外服务器的每个允许端口进行连接。 | 连接应通过。 |
尝试从网络外部到 DMZ 中的 $webserver 在 $webports 中的每个端口进行连接。 |
连接应通过。 |
连接应通过。尝试从网络外部到 DMZ 中的 $webserver 在端口 25(SMTP)上的连接。 |
连接应被阻止。 |
尝试从网络外部连接到 DMZ 中端口 80(HTTP)的$emailserver。 |
连接应该被阻止。 |
尝试从网络外部连接到 DMZ 中端口 25(SMTP)的$emailserver。 |
连接应该被允许。 |
| 尝试从网络外部连接到本地网络中的一台或多台机器。 | 连接应该被阻止。 |
调试你的规则集
当你的配置没有按预期行为时,可能是规则集逻辑存在错误,因此你需要找到错误并纠正它。追踪规则集中的逻辑错误可能会非常耗时,可能需要手动评估规则集——包括它在pf.conf文件中的存储版本,以及宏扩展和任何优化后的加载版本。
用户通常最初将问题归咎于 PF,但实际上问题往往是基本的网络问题。网络接口设置错误的双工模式、错误的子网掩码和故障的网络硬件是常见的罪魁祸首。
在深入规则集本身之前,你可以轻松地判断 PF 配置是否引发了问题。为此,可以通过pfctl -d禁用 PF,看看问题是否消失。如果禁用 PF 后问题仍然存在,你应该转而调试网络配置的其他部分。如果禁用 PF 后问题消失,并且你准备开始调整 PF 配置,请确保 PF 已启用,并且通过以下命令加载了你的规则集:
**$ sudo pfctl -si | grep Status**
Status: Enabled for 20 days 06:28:24 Debug: err
Status: Enabled告诉我们 PF 已启用,因此我们尝试使用不同的pfctl命令查看已加载的规则:
**$ sudo pfctl -sr**
match in all scrub (no-df max-mss 1440)
block return log all
block return log quick from <bruteforce> to any
anchor "ftp-proxy/*" all
这里,pfctl -sr等同于pfctl -s rules。输出可能会比这里显示的稍长,但这可以作为加载规则集时你应该预期看到的输出的一个好例子。
为了调试的目的,可以考虑在pfctl命令行中添加-vv标志,以查看规则编号和一些额外的调试信息,像这样:
**$ sudo pfctl -vvsr**
@0 match in all scrub (no-df max-mss 1440)
[ Evaluations: 341770 Packets: 3417668 Bytes: 2112276585 States: 125 ]
[ Inserted: uid 0 pid 14717 State Creations: 92254 ]
@1 match out on nfe0 inet from 10.0.0.0/8 to any queue(q_def, q_pri) nat-to
(nfe0:1) round-robin static-port
[ Evaluations: 341770 Packets: 0 Bytes: 0 States: 0 ]
[ Inserted: uid 0 pid 14717 State Creations: 0 ]
@2 match out on nfe0 inet from 192.168.103.0/24 to any queue(q_def, q_pri)
nat-to (nfe0:1) round-robin static-port
[ Evaluations: 68623 Packets: 2138128 Bytes: 1431276138 States: 103 ]
[ Inserted: uid 0 pid 14717 State Creations: 39109 ]
@3 block return log all
[ Evaluations: 341770 Packets: 114929 Bytes: 62705138 States: 0 ]
[ Inserted: uid 0 pid 14717 State Creations: 0 ]
@4 block return log (all) quick from <bruteforce:0> to any
[ Evaluations: 341770 Packets: 2 Bytes: 104 States: 0 ]
[ Inserted: uid 0 pid 14717 State Creations: 0 ]
@5 anchor "ftp-proxy/*" all
[ Evaluations: 341768 Packets: 319954 Bytes: 263432399 States: 0 ]
[ Inserted: uid 0 pid 14717 State Creations: 70 ]
现在你应该对加载的规则集进行结构化的逐步检查。找到与正在调查的数据包匹配的规则。最后匹配的规则是什么?如果有多个规则匹配,其中有一个是quick规则吗?(如你在前面的章节中可能记得,当数据包匹配一个quick规则时,评估会停止,quick规则指定的内容就是数据包的处理方式。)如果是这样,你需要追踪评估过程,直到到达规则集的末尾,或者数据包匹配到一个quick规则,之后过程结束。如果你的规则集逐步检查的结果与预期匹配的数据包规则不同,你就找到了逻辑错误。务必小心match规则。如果你无法确定为何特定数据包匹配了某个block或pass规则,原因可能是某个match规则应用了一个操作,使得数据包或连接匹配了与预期不同的过滤标准。
规则集逻辑错误通常分为三种类型:
-
你的规则没有匹配,因为它从未被评估。规则集中的一个
quick规则先匹配了,评估就停止了。 -
你的规则被评估,但由于规则的标准,最终没有匹配到数据包。
-
你的规则被评估并且匹配,但数据包也匹配了规则集中的另一个规则。最后匹配的规则决定了连接的处理方式。
第九章介绍了tcpdump,它是一个读取和解读 PF 日志的有价值工具。该程序也非常适合查看特定接口上通过的流量。你关于 PF 日志的学习,以及如何使用tcpdump的过滤功能,将在你想要追踪到底是哪些数据包到达了哪个接口时派上用场。
这是一个使用tcpdump监控 TCP 流量(但不包括 SSH 或 SMTP 流量)在xl0接口上的示例,并以非常详细的模式(vvv)打印结果。
**$ sudo tcpdump -nvvvpi xl0 tcp and not port ssh and not port smtp**
tcpdump: listening on xl0, link-type EN10MB
21:41:42.395178 194.54.107.19.22418 > 137.217.190.41.80: S [tcp sum ok]
3304153886:3304153886(0) win 16384 <mss 1460,nop,nop,sackOK,nop,wscale
0,nop,nop,timestamp 1308370594 0> (DF) (ttl 63, id 30934, len 64)
21:41:42.424368 137.217.190.41.80 > 194.54.107.19.22418: S [tcp sum ok]
1753576798:1753576798(0) ack 3304153887 win 5792 <mss 1460,sackOK,timestamp
168899231 1308370594,nop,wscale 9> (DF) (ttl 53, id 0, len 60)
这里展示的连接是成功连接到一个网站。
然而,仍然有更多有趣的事情需要探索,比如连接在不应该失败的情况下失败,或者连接在你的规范明确表示不应该成功的情况下却成功。
这些情况中的测试涉及追踪数据包在你配置中的路径。再次强调,检查 PF 是否启用,或者禁用 PF 是否会产生不同的结果是非常有用的。在初步测试的结果基础上,你将执行与之前描述的相同类型的规则集分析:
-
一旦你对数据包如何穿越规则集和网络接口有了合理的理论,就使用
tcpdump逐一查看每个接口上的流量。 -
使用
tcpdump的过滤功能来提取你需要的信息——也就是说,只查看应该匹配你特定情况的数据包,比如port smtp and dst 192.0.2.19。 -
找到你的假设与网络流量现实不匹配的地方。
-
为可能涉及的规则开启日志记录,并将
tcpdump应用于相关的pflog接口,查看数据包实际上匹配了哪个规则。
测试过程的主要框架是相对固定的。如果你已经将原因缩小到你的 PF 配置,那么,接下来就是找出哪些规则匹配,哪个规则最终决定数据包是通过还是被阻塞。
了解你的网络并保持控制
本书的反复主题是,PF 和相关工具如何使你作为网络管理员相对容易地控制你的网络,并让它按你希望的方式运作——换句话说,PF 如何帮助你构建所需的网络。
运行网络是件有趣的事,希望你已经享受了我认为是网络安全最佳工具的介绍。在介绍 PF 时,我一开始就做出了一个有意识的决定:通过有趣且有用的配置向你介绍方法和思维方式,而不是提供一个完整的功能目录,或者把这本书做成完整的参考手册。完整的 PF 参考资料已经存在于手册页中,这些手册每六个月会随着新的 OpenBSD 版本发布而更新。你还可以在我在附录 A 中列出的资源中找到更多信息。
现在你已经具备了关于 PF 可以做什么的广泛基础知识,你可以开始根据自己的需求来构建网络。你已经到了可以在手册页中找到所需准确信息的阶段。这时,真正有趣的部分开始了!
^([49]) 如果你还没有遇到过这种恶劣情况,迟早会遇到。这是关于一个 DDoS 攻击案例的描述,攻击者和受害者的笨拙程度几乎相等——[http://bsdly.blogspot.com/2012/12/ddos-bots-are-people-or-manned-by-some.html](http://bsdly.blogspot.com/2012/12/ddos-bots-are-people-or-manned-by-some.html)。你的攻击者很可能比这些更聪明、更装备精良。
^([50]) 一些著名的攻击技术,包括几种历史上的拒绝服务攻击(DoS)设置,利用了片段处理中的漏洞,这可能导致内存溢出或其他资源耗尽。一种针对思科 PIX 防火墙系列的漏洞就是在[http://www.cisco.com/en/US/products/products_security_advisory09186a008011e78d.shtml](http://www.cisco.com/en/US/products/products_security_advisory09186a008011e78d.shtml)中描述的。
附录 A. 资源

虽然我希望能涵盖 PF 配置的所有细节,但在这些页面中,覆盖所有可能的配置细节证明是不可能的。我希望这里列出的资源能补充一些细节,或者提供略有不同的视角。它们中的一些甚至因为自身的趣味性而值得一读。
互联网上的一般网络与 BSD 资源
以下是本书中引用的常见网络资源。值得查看各个 BSD 项目的官方网站,以获取最新的信息。
-
对于 OpenBSD 用户来说,特别值得关注的是在线的 OpenBSD Journal (
undeadly.org/). 它提供关于 OpenBSD 和相关问题的新闻和文章。 -
OpenBSD 的官网
www.openbsd.org/是获取 OpenBSD 信息的主要参考。如果你在使用 OpenBSD,你会不时访问这个网站。 -
你可以在
www.openbsd.org/papers/找到 OpenBSD 开发者的演讲和论文合集。这个网站是了解 OpenBSD 持续发展的一个良好信息来源。 -
OpenBSD 常见问题解答 (
www.openbsd.org/faq/index.html) 更像是一本用户指南,而非传统的问答文档。在这里,你将找到大量背景信息以及如何设置和运行 OpenBSD 系统的逐步说明。 -
Henning Brauer 的演讲“更快的包—网络栈和 PF 的性能调优” (
quigon.bsws.de/papers/2009/eurobsdcon-faster_packets/) 是当前主要 PF 开发者概述了最近 OpenBSD 版本中为提高网络性能所做的工作,PF 是其中的核心组件。 -
PF: OpenBSD 数据包过滤器 (
www.openbsd.org/faq/pf/index.html), 也称为 PF 用户指南 或 PF 常见问题解答,是 OpenBSD 团队维护的官方 PF 文档。本指南会根据每个版本进行更新,是 PF 从业者极为宝贵的参考资源。 -
Bob Beck 的“pf. 它不仅仅是防火墙” (
www.ualberta.ca/~beck/nycbug06/pf/) 是一场 NYCBUG 2006 的演讲,讲解了 PF 的冗余性和可靠性特性,并通过来自阿尔伯塔大学网络的实际案例进行了说明。 -
Daniel Hartmeier 的 PF 页面 (
www.benzedrine.cx/pf.html) 是他整理的 PF 相关资料,并提供了指向网络上各种资源的链接。 -
Daniel Hartmeier 的《OpenBSD 有状态包过滤器(pf)的设计与性能》(
www.benzedrine.cx/pf-paper.html)是他在 Usenix 2002 年会议上发表的论文。它描述了 PF 的初步设计和实现。 -
Daniel Hartmeier 的三部分undeadly.org PF 系列包括“PF:防火墙规则集优化”(
undeadly.org/cgi?action=article&sid=20060927091645), “PF:测试你的防火墙”(undeadly.org/cgi?action=article&sid=20060928081238), 和“PF:防火墙管理”(undeadly.org/cgi?action=article&sid=20060929080943)。这三篇文章详细讲解了各自的主题,同时又保持了较高的可读性。 -
RFC 1631,《IP 网络地址转换器(NAT)》,1994 年 5 月(
www.ietf.org/rfc/rfc1631.txt,由 K. Egevang 和 P. Francis 编写)是 NAT 规范的第一部分,它的生命力比作者们最初的预期要长久。虽然它仍然是理解 NAT 的重要资源,但它已经在很大程度上被更新后的 RFC 3022(www.ietf.org/rfc/rfc3022.txt,由 P. Srisuresh 和 K. Egevang 编写),该文档于 2001 年 1 月发布,所取代。 -
RFC 1918,《私人互联网地址分配》,1996 年 2 月(
www.ietf.org/rfc/rfc1918.txt,由 Y. Rebhter、B. Moskowitz、D. Karrenberg、G.J. de Groot 和 E. Lear 编写)是 NAT 和私人地址空间难题的第二部分。该 RFC 描述了分配私有、不可路由地址空间的动机,并定义了地址范围。RFC 1918 被指定为当前最佳实践。 -
如果你正在寻找一本全面详细地介绍网络协议的书,且明确倾向于 TCP/IP 视角,那么 Charles M. Kozierok 的《TCP/IP 指南》(No Starch Press,2005 年 10 月),并且有在线更新的版本,地址为
www.tcpipguide.com/,几乎没有其他书籍能与之匹敌。它有超过 1600 页,虽然不算是口袋书,但在你的桌面上或浏览器窗口中非常有用,可以帮助你澄清在其他书籍中解释不清的任何网络术语。
示例配置和相关思考
许多人慷慨地分享了他们的经验,并在网上提供了示例配置。以下是我最喜欢的一些。
-
Marcus Ranum 的《计算机安全中的六个最愚蠢的想法》(
www.ranum.com/security/computer_security/editorials/dumb/index.html), 2005 年 9 月 1 日,是我长期以来的最爱。本文探讨了关于安全的一些常见误解及其对现实世界安全工作产生的不幸影响。 -
Randal L. Schwartz 的《使用 OpenBSD 的包过滤器监控网络流量》(
www.stonehenge.com/merlyn/UnixReview/col51.html)展示了流量监控和使用标签进行计费的实际示例。尽管在这几年里 PF 和标签的一些细节发生了变化,但这篇文章依然可读,并且很好地呈现了几个重要概念。 -
瑞典用户组 Unix.se 的Brandvägg med OpenBSD (
unix.se/Brandv%E4gg_med_OpenBSD)及其示例配置,例如基本的 ALTQ 配置,在我初期使用时非常有用。该网站很好地提醒我们,像本地用户组这样的志愿者努力可以成为宝贵的信息来源。 -
#pf IRC 频道维基(
www.probsd.net/pf/)是由#pf IRC 频道讨论参与者维护的文档、示例配置和其他 PF 信息的集合。这是一个非常值得关注的志愿者努力的例子。 -
来自意大利的 OpenBSD 爱好者 Daniele Mazzocchio 维护着 Kernel Panic 网站,该网站收录了关于各种 OpenBSD 主题的有用文章和教程类文档,网址为
www.kernel-panic.it/openbsd.html(英文和意大利文)。对于从一个似乎致力于保持材料与最新稳定版 OpenBSD 同步更新的人那里获取关于各种有趣话题的新视角,访问该站点非常值得。 -
Kenjiro Cho 的《使用 ALTQ 管理流量》(
www.usenix.org/publications/library/proceedings/usenix99/cho.html) 是描述 ALTQ 设计及其在 FreeBSD 上早期实现的原始论文。 -
Jason Dixon 的《使用 OpenBSD 和 CARP 的故障转移防火墙》,发表于 2005 年 5 月的SysAdmin Magazine (
planet.admon.org/howto/failover-firewalls-with-openbsd-and-carp/),概述了 CARP 和 pfsync,并提供了一些实际示例。 -
Theo de Raadt 的 OpenCON 2006 演讲“硬件的开放文档:为什么硬件文档如此重要,以及为什么它如此难以获得” (
openbsd.org/papers/opencon06-docs/index.html),是附录 B 中关于自由操作系统硬件支持的一条重要灵感来源,特别是针对 OpenBSD。
其他 BSD 系统上的 PF
PF 已经从 OpenBSD 移植到其他 BSD 系统,尽管这些努力的目标自然是尽可能与 OpenBSD 最新版本的 PF 保持同步,但追踪其他 BSD 系统中的 PF 项目仍然是有用的。
-
FreeBSD 数据包过滤器(pf)主页 (
pf4freebsd.love2party.net/) 描述了在 FreeBSD 上与 PF 相关的早期工作及其项目目标。目前该页面并未完全更新最新进展,但一旦 Max Laier 注意到他在印刷版书籍中的引用,页面可能会再次活跃起来。 -
NetBSD 项目在
www.netbsd.org/docs/network/pf.html上维护着关于 PF 的页面,你可以在这里找到关于 NetBSD 上 PF 的最新信息。
BSD 和网络书籍
除了似乎不断扩展的在线资源外,几本书籍也可以作为本书的伴随或补充读物。
-
Michael W. Lucas, Absolute OpenBSD, 第 2 版(No Starch Press,2013)。这本书提供了 OpenBSD 的全面指南,内容丰富且实践性强。
-
Michael W. Lucas, Network Flow Analysis(No Starch Press,2010)。这是少数几本使用免费的 NetFlow 工具进行网络分析和管理的书籍之一,本书展示了如何使用这些工具和方法,发现你网络中真实发生的事情。
-
Brandon Palmer 和 Jose Nazario, Secure Architectures with OpenBSD(Addison-Wesley,2004)。本书概述了 OpenBSD 的特性,并着重介绍了构建安全和可靠系统的方法。书中参考了当时最新版本的 OpenBSD 3.4。
-
Douglas R. Mauro 和 Kevin J. Schmidt, Essential SNMP, 第 2 版(O'Reilly Media,2005)。正如书名所示,这是一本关于 SNMP 的基础参考书。
-
Jeremy C. Reed(编辑),The OpenBSD PF Packet Filter Book(Reed Media Services,2006)。本书基于 PF 用户指南,扩展到涵盖 FreeBSD、NetBSD 和 DragonFly BSD 上的 PF,并包括一些关于与 PF 互操作的第三方工具的附加内容。
-
Christopher M. Buechler 和 Jim Pingle, pfSense: The Definitive Guide(Reed Media Services,2009)。这本约 515 页的书是一本全面的指南,介绍了基于 FreeBSD 和 PF 的防火墙设备分发版本。根据写作时的计划,修订版预计于 2014 年出版。
无线网络资源
Kjell Jørgen Hole 的 Wi-Fi 课程材料 (www.nowires.org/) 是理解无线网络的一个极好的资源。该课程材料主要面向参加 Hole 教授课程的卑尔根大学学生,但它是免费的,值得一读。
spamd 和灰名单相关资源
如果处理电子邮件是你生活的一部分(或者未来可能成为你生活的一部分),你可能已经喜欢了本书中对spamd、陷阱邮件和灰名单的描述。如果你想要比相关 RFC 中找到的更多背景信息,以下文档和网络资源将提供相关内容。
-
Greylisting.org (
www.greylisting.org/)收集了有用的灰名单相关文章和关于灰名单及 SMTP 的一般信息。 -
Evan Harris 的《垃圾邮件控制战争中的下一步:灰名单》(
greylisting.org/articles/whitepaper.shtml) 是最初的灰名单论文。 -
Bob Beck 的《OpenBSD spamd——灰名单及其他》(
www.ualberta.ca/~beck/nycbug06/spamd/) 是一场 NYCBUG 演讲,解释了spamd的工作原理,并描述了spamd在阿尔伯塔大学基础设施中的作用。(请注意,演讲中提到的许多“未来工作”已经实施。) -
《有效的垃圾邮件和恶意软件对策》(
bsdly.blogspot.com/2014/02/effective-spam-and-malware.html), 最初是我在 BSDCan 2007 的论文并进行了更新,包含了如何使用灰名单、spamd以及各种其他免费工具和 OpenBSD 来成功打击你网络中的垃圾邮件和恶意软件的最佳实践描述。 -
一个有前景的新进展是 Peter Hessler 的BGP-spamd项目,该项目略微滥用 BGP 路由协议,将
spamd数据分发到参与的主机之间。更多信息请访问该项目的官方网站bgp-spamd.net/。
与书籍相关的网络资源
有关本书的新闻和更新,请访问 No Starch Press 网站上的书籍主页 (www.nostarch.com/pf3/)。该页面包含指向我的个人网页空间的链接,在那里将会发布各种更新和书籍相关资源。关于本书的新闻和更新也会发布在 www.bsdly.net/bookofpf/。与本书相关的公告可能也会通过我的博客 bsdly.blogspot.com/ 出现。
我维护着教程手稿《使用 OpenBSD 的 PF 数据包过滤器进行防火墙设置》,它是本书的前身。我的政策是在合适的时候进行更新,通常是在我了解 PF 和相关软件的变化或新特性时,以及准备参加会议时。该教程手稿以 BSD 许可协议发布,可以从我的网页空间下载,格式有多种,地址是 home.nuug.no/~peter/pf/。更新版本会不定期出现在该网址,通常是在活动之间的自然调整过程中。
购买 OpenBSD 光盘并捐赠!
如果你喜欢这本书或觉得它有用,请访问 OpenBSD.org 订购页面 www.openbsd.org/orders.html 购买光盘套装,或者你也可以访问捐赠页面 www.openbsd.org/donations.html,通过金钱捐赠来支持 OpenBSD 项目的进一步开发。
如果你更倾向于向公司捐赠,你可以联系 OpenBSD 基金会,这是一家于 2007 年成立的加拿大非营利公司,专门用于此目的。有关更多信息,请访问 OpenBSD 基金会网站 www.openbsdfoundation.org/。
如果你在会议上找到了这本书,附近可能会有 OpenBSD 展位,你可以购买光盘、T 恤和其他物品。
请记住,即使是免费的软件,也需要真实的工作和资金来开发和维护。
附录 B. 关于硬件支持的说明

“硬件支持怎么样?”我经常听到这个问题,通常我的回答是,“根据我的经验,OpenBSD 和其他自由系统通常可以顺利运行。”
但出于某种原因,普遍有一种看法,认为使用自由软件意味着让硬件组件正常工作会是一项艰巨的任务。过去,这种看法确实有一些事实依据。我记得曾经在我可用的硬件上努力安装 FreeBSD 2.0.5。我能够从安装光盘启动,但安装从未完成,因为我的 CD 驱动器没有得到完全支持。
但那是在 1995 年 6 月,当时 PC 的 CD 驱动器通常配有几乎但不完全是 IDE 接口,并附带一个声卡,而且便宜的 PC 通常没有内建任何类型的网络硬件。配置一台机器进行网络使用通常意味着需要在网络接口卡或主板上调整跳线,或者运行一些奇怪的专有设置软件——如果你有好运,系统上有或可以安装以太网接口的话。
时过境迁。今天,你可以合理地期望系统中的所有重要组件都能与 OpenBSD 兼容。当然,构建最佳配置可能需要一些谨慎和规划,但这并不一定是坏事。
获取合适的硬件
为你的系统选择合适的硬件,本质上是要检查你的系统是否满足项目和网络的需求:
-
查阅在线硬件兼容性列表。
-
查阅手册页,或者使用
apropos关键字命令(其中关键字是你正在寻找的设备类型)。 -
如果你想了解更多背景信息,可以查阅相关邮件列表的档案。
-
使用你喜欢的搜索引擎,找到有关特定设备与操作系统兼容性的信息。
在大多数情况下,硬件会按预期工作。然而,有时候其他功能正常的硬件可能会带来一些奇怪的限制。
很多设备设计时依赖于必须在操作系统使用该设备之前加载的固件。这种设计选择的动机几乎总是为了降低设备成本。当一些厂商拒绝授权固件的再分发时,这个决定就变成了一个问题,因为这意味着像 OpenBSD 这样的操作系统无法将固件与其发布版一起打包。
这种类型的问题在与几种类型的硬件相关的情况下已经出现。在许多情况下,制造商已经被说服改变主意并允许重新分发。然而,并非所有情况都会发生。一个例子是集成在许多热门笔记本型号中的基于英特尔的无线网络硬件。该硬件在许多操作系统中得到支持,包括通过wpi和iwn驱动程序在 OpenBSD 中。但即使有了这些驱动程序,硬件也不会工作,除非用户手动获取并安装所需的固件文件。安装完成并且有某种形式的互联网连接可用后,OpenBSD 用户可以运行fw_update命令来获取并安装或升级系统识别为需要固件文件的组件的固件。
注
在受支持的硬件受限的情况下,OpenBSD 手册通常会注明这一事实,甚至可能包括可能能够改变制造商政策的人的电子邮件地址。
只需制造商的许可政策进行轻微更改,就可以使自由软件用户的生活更加轻松,并提高销售额。可能在您阅读本文时,大多数类似情况都已得到解决。请务必在线查看最新信息,并准备好用您的钱包投票,如果某个特定公司拒绝采取明智行动。
如果您在网上购物,请在另一个标签页或窗口中保留手册。如果您去实体店,请确保告诉店员您将使用 BSD。如果您对他们试图向您出售的零件不确定,请请求借用一台机器浏览在线手册和其他文档。您甚至可以请求允许使用 CD 或 USB 存储设备从您感兴趣的硬件启动一台机器并研究dmesg输出。提前告知店员您的项目可能会使您更容易在零件不起作用时获得退款。如果零件起作用,请告知卖方是良好的倡导。您的请求很可能是卖方第一次听说您喜欢的操作系统。
面临的硬件支持开发人员问题
诸如 OpenBSD 和其他 BSD 系统并非完全成熟地从某位神的脑海中涌现出来(尽管有人会争辩这个过程并没有那么不同)。相反,它们是一群聪明和敬业的开发人员多年努力的结果。
BSD 开发人员都是高素质且极其敬业的人,他们不知疲倦地工作——大多数人在业余时间——以产生惊人的结果。然而,他们并不生活在一个拥有一切所需的气泡中。硬件本身或支持它的充分文档通常对他们不可用。另一个常见问题是文档通常仅在保密协议(NDA)下提供,这限制了开发人员如何使用信息。^([51])
通过逆向工程,开发人员即使没有适当的文档,也可以编写驱动程序来支持硬件,但这个过程是复杂的,包括有根据的猜测、编码和测试,直到结果开始显现。逆向工程需要大量时间,而且—由于只有立法者和游说者才知道的原因—它在世界多个法域中会带来法律后果。
好消息是,你可以帮助开发者获得他们需要的硬件和其他资料。
如何帮助硬件支持工作
如果你能贡献高质量的代码,BSD 项目非常欢迎你的参与。如果你自己不是开发者,贡献代码可能不是一个选项。这里有几种其他的贡献方式:
-
从支持开源的供应商那里购买硬件。 在做出关于你所在组织的设备采购决策时,告诉供应商开源友好性是你采购决策中的一个因素。
-
让硬件供应商知道你对他们支持(或缺乏支持)你喜欢的操作系统的看法。 一些硬件供应商非常有帮助,提供了样品和程序员文档。另一些则不太乐意,甚至表现得相当敌对。无论是这两种供应商,还是介于其间的供应商,都需要鼓励。写信给他们,告诉他们你认为他们做得对的地方,以及他们可以改进的地方。例如,如果一个供应商拒绝提供编程文档,或者只有在签署保密协议的情况下才提供,来自潜在客户的一封理性且条理清晰的信件可能会起到决定性的作用。
-
帮助测试系统并查看你感兴趣的硬件驱动程序。 如果一个驱动程序已经存在或正在开发,开发者总是对其他人设备上该驱动程序的表现报告感兴趣。报告系统运行良好总是受欢迎的,但包含详细问题描述的错误报告对于创建和维护高质量的系统来说更加重要。
-
捐赠硬件或资金。 开发者总是需要硬件进行开发,资金当然也能帮助他们满足日常需求。如果你能捐赠资金或硬件,可以查看该项目的捐赠页面(*
www.openbsd.org/donations.html对于 OpenBSD)或物品需求页面(www.openbsd.org/want.html对于 OpenBSD)。企业或其他希望通过加拿大非营利机构向 OpenBSD 捐赠的,可以通过 OpenBSD 基金会进行捐赠,基金会网站地址是www.openbsdfoundation.org/*。对 OpenBSD 的捐赠最有可能帮助 PF 开发,但如果你更愿意捐赠给 FreeBSD、NetBSD 或 DragonFly BSD,你可以在它们的网站上找到如何捐赠的信息。
无论你与 BSD 及硬件的关系如何,我希望本附录能帮助你做出明智的决策,了解该购买什么以及如何支持 BSD 的发展。你的支持将有助于提供更多、更高质量的免费软件供大家使用。
^([51]) 这也是一个常见的讨论话题。例如,参见 Theo de Raadt 在 OpenCON 2006 上的演讲《为什么硬件文档如此重要,以及为什么这么难获得》,该演讲可在 www.openbsd.org/papers/opencon06-docs/index.html 查阅。


浙公网安备 33010602011771号