PF ( 全称:Packet Filter ) --- 包过滤
是UNIX LIKE系统上进行TCP/IP流量过滤和网络地址转换的软件系统。PF同样也能提供TCP/IP流量的整形和控制,并且提供带宽控制和数据包优先集控制。
PF最早是由Daniel Hartmeier开发的,现在的开发和维护由Daniel 和openbsd小组的其他成员负责。
[显示全部]
要激活pf并且使它在启动时调用配置文件,编辑/etc/rc.conf文件,修改配置pf的一行:
pf=YES
重启操作系统让配置生效。
也可以通过pfctl程序启动和停止pf
# pfctl -e
# pfctl -d
注意这仅仅是启动和关闭PF,实际它不会载入规则集,规则集要么在系统启动时载入,要在PF启动后通过命令单独载入。
配置
系统引导到在rc脚本文件运行PF时PF从/etc/pf.conf文件载入配置规则。注意当/etc/pf.conf文件是默认配置文件,在系统调用rc脚本文件时,它仅仅是作为文本文件由pfctl装入并解释和插入pf的。对于一些应用来说,其他的规则集可以在系统引导后由其他文件载入。对于一些设计的非常好的unix程序,PF提供了足够的灵活性。
pf.conf 文件有7个部分:
* 宏: 用户定义的变量,包括IP地址,接口名称等等
* 表: 一种用来保存IP地址列表的结构
* 选项: 控制PF如何工作的变量
* 整形: 重新处理数据包,进行正常化和碎片整理
* 排队: 提供带宽控制和数据包优先级控制.
* 转换: 控制网络地址转换和数据包重定向.
* 过滤规则: 在数据包通过接口时允许进行选择性的过滤和阻止
除去宏和表,其他的段在配置文件中也应该按照这个顺序出现,尽管对于一些特定的应用并不是所有的段都是必须的。
空行会被忽略,以#开头的行被认为是注释.
![]() |
# pfctl -sa |
引导之后,PF可以通过pfctl程序进行操作,以下是一些例子:
# pfctl -f /etc/pf.conf 载入 pf.conf 文件
# pfctl -nf /etc/pf.conf 解析文件,但不载入
# pfctl -Nf /etc/pf.conf 只载入文件中的NAT规则
# pfctl -Rf /etc/pf.conf 只载入文件中的过滤规则
# pfctl -sn 显示当前的NAT规则
# pfctl -sr 显示当前的过滤规则
# pfctl -ss 显示当前的状态表
# pfctl -si 显示过滤状态和计数
# pfctl -sa 显示任何可显示的
完整的命令列表,请参阅pfctl的man手册页。
列表
一个列表允许一个规则集中指定多个相似的标准。例如,多个协议,端口号,网络地址等等。因此,不需要为每一个需要阻止的IP地址编写一个过滤规则,一条规则可以在列表中指定多个IP地址。列表的定义是将要指定的条目放在{ }大括号中。
当pfctl在载入规则集碰到列表时,它产生多个规则,每条规则对于列表中的一个条目。例如:
block out on fxp0 from { 192.168.0.1, 10.5.32.6 } to any
展开后:
block out on fxp0 from 192.168.0.1 to any
block out on fxp0 from 10.5.32.6 to any
多种列表可以在规则中使用,并不仅仅限于过滤规则:
rdr on fxp0 proto tcp from any to any port { 22 80 } -> \
192.168.0.6
block out on fxp0 proto { tcp udp } from { 192.168.0.1, \
10.5.32.6 } to any port { ssh telnet }
注意逗号在列表条目之间是可有可无的。
宏
宏是用户定义变量用来指定IP地址,端口号,接口名称等等。宏可以降低PF规则集的复杂度并且使得维护规则集变得容易。
宏名称必须以字母开头,可以包括字母,数字和下划线。宏名称不能包括保留关键字如:
pass, out, 以及 queue.
ext_if = "fxp0"
block in on $ext_if from any to any
这生成了一个宏名称为ext_if. 当一个宏在它产生以后被引用时,它的名称前面以$字符开头。
宏也可以展开成列表,如:
friends = "{ 192.168.1.1, 10.0.2.5, 192.168.43.53 }"
宏能够被重复定义,由于宏不能在引号内被扩展,因此必须使用下面得语法:
host1 = "192.168.1.1"
host2 = "192.168.1.2"
all_hosts = "{" $host1 $host2 "}"
宏 $all_hosts 现在会展开成 192.168.1.1, 192.168.1.2.
表是用来保存一组IPv4或者IPv6地址。在表中进行查询是非常快的,并且比列表消耗更少的内存和cpu时间。由于这个原因,表是保存大量地址的最好方法,在50,000个地址中查询仅比在50个地址中查询稍微多一点时间。表可以用于下列用途:
* 过滤,整形,NAT和重定向中的源或者目的地址.
* NAT规则中的转换地址.
* 重定向规则中的重定向地址.
* 过滤规则选项中 route-to, reply-to, 和 dup-to的目的地址.
表可以通过在pf.conf里配置和使用pfctl生成。
配置
在pf.conf文件中, 表是使用table关键字创建出来的。下面的关键字必须在创建表时指定。
* constant - 这类表的内容一旦创建出来就不能被改变。如果这个属性没有指定,可以使用pfctl添加和删除表里的地址,即使系统是运行在2或者更高得安全级别上。
* persist - 即使没有规则引用这类表,内核也会把它保留在内存中。如果这个属性没有指定,当最后引用它的规则被取消后内核自动把它移出内存。
实例:
table { 192.0.2.0/24 }
table const { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }
table persist
block in on fxp0 from { , } to any
pass in on fxp0 from to any
地址也可以用“非”来进行修改,如:
table { 192.0.2.0/24, !192.0.2.5 }
goodguys表将匹配除192.0.2.5外192.0.2.0/24网段得所有地址。
注意表名总是在符号得里面。
表也可以由包含IP地址和网络地址的文本文件中输入:
table persist file "/etc/spammers"
block in on fxp0 from to any
文件 /etc/spammers 应该包含被阻塞的IP地址或者CIDR网络地址,每个条目一行。以#开头的行被认为是注释会被忽略。
用 pfctl 进行操作
表可以使用pfctl进行灵活的操作。例如,在上面产生的表中增加条目可以这样写:
# pfctl -t spammers -T add 218.70.0.0/16
如果这个表不存在,这样会创建出这个表来。列出表中的内容可以这样:
# pfctl -t spammers -T show
-v 参数也可以使用-Tshow 来显示每个表的条目内容统计。要从表中删除条目,可以这样:
# pfctl -t spammers -T delete 218.70.0.0/16
指定地址
除了使用IP地址来指定主机外,也可以使用主机名。当主机名被解析成IP地址时,IPv4和IPv6地址都被插进规则中。IP地址也可以通过合法的接口名称或者self关键字输入表中,这样的表会分别包含接口或者机器上(包括loopback地址)上配置的所有IP地址。
一个限制时指定地址0.0.0.0/0 以及 0/0在表中不能工作。替代方法是明确输入该地址或者使用宏。
地址匹配
表中的地址查询会匹配最接近的规则,比如:
table { 172.16.0.0/16, !172.16.1.0/24, 172.16.1.100 }
block in on dc0 all
pass in on dc0 from to any
任何自dc0上数据包都会把它的源地址和goodguys表中的地址进行匹配:
* 172.16.50.5 - 精确匹配172.16.0.0/16; 数据包符合可以通过
* 172.16.1.25 - 精确匹配!172.16.1.0/24; 数据包匹配表中的一条规则,但规则是“非”(使用“!”进行了修改);数据包不匹配表会被阻塞。
* 172.16.1.100 - 准确匹配172.16.1.100; 数据包匹配表,运行通过
* 10.1.4.55 - 不匹配表,阻塞。
包过滤是在数据包通过网络接口时进行选择性的运行通过或者阻塞。pf检查包时使用的标准是基于的3层(IPV4或者IPV6)和4层(TCP, UDP, ICMP, ICMPv6)包头。最常用的标准是源和目的地址,源和目的端口,以及协议。
过滤规则集指定了数据包必须匹配的标准和规则集作用后的结果,在规则集匹配时通过或者阻塞。规则集由开始到结束顺序执行。除非数据包匹配的规则包含quick关键字,否则数据包在最终执行动作前会通过所有的规则检验。最后匹配的规则具有决定性,决定了数据包最终的执行结果。存在一条潜在的规则是如果数据包和规则集中的所有规则都不匹配,则它会被通过。
规则语法
一般而言,最简单的过滤规则语法是这样的:
action direction [log] [quick] on interface [af] [proto protocol] \
from src_addr [port src_port] to dst_addr [port dst_port] \
[tcp_flags] [state]
action
数据包匹配规则时执行的动作,放行或者阻塞。放行动作把数据包传递给核心进行进一步出来,阻塞动作根据block-policy 选项指定的方法进行处理。默认的动作可以修改为阻塞丢弃或者阻塞返回。
direction
数据包传递的方向,进或者出
log
指定数据包被pflogd( 进行日志记录。如果规则指定了keep state, modulate state, or synproxy state 选项,则只有建立了连接的状态被日志。要记录所有的日志,使用log-all
quick
如果数据包匹配的规则指定了quick关键字,则这条规则被认为时最终的匹配规则,指定的动作会立即执行。
interface
数据包通过的网络接口的名称或组。组是接口的名称但没有最后的整数。比如ppp或fxp,会使得规则分别匹配任何ppp或者fxp接口上的任意数据包。
af
数据包的地址类型,inet代表Ipv4,inet6代表Ipv6。通常PF能够根据源或者目标地址自动确定这个参数。
protocol
数据包的4层协议:
tcp
udp
icmp
icmp6
/etc/protocols中的协议名称
0~255之间的协议号
使用列表的一系列协议.
src_addr, dst_addr
IP头中的源/目标地址。地址可以指定为:
单个的Ipv4或者Ipv6地址.
CIDR 网络地址.
能够在规则集载入时通过DNS解析到的合法的域名,IP地址会替代规则中的域名。
网络接口名称。网络接口上配置的所有ip地址会替代进规则中。
带有/掩码(例如/24)的网络接口的名称。每个根据掩码确定的CIDR网络地址都会被替代进规则中。.
带有()的网络接口名称。这告诉PF如果网络接口的IP地址改变了,就更新规则集。这个对于使用DHCP或者拨号来获得IP地址的接口特别有用,IP地址改变时不需要重新载入规则集。
带有如下的修饰词的网络接口名称:
o :network - 替代CIDR网络地址段 (例如:192.168.0.0/24)
o :broadcast - 替代网络广播地址(例如:192.168.0.255)
o :peer - 替代点到点链路上的ip地址。
另外,:0修饰词可以附加到接口名称或者上面的修饰词后面指示PF在替代时不包括网络接口的其余附加(alias)地址。这些修饰词也可以在接口名称在括号()内时使用。例如:fxp0:network:0
表.
上面的所有项但使用!(非)修饰词
使用列表的一系列地址.
关键字 any 代表所有地址
关键字 all 是 from any to any的缩写。
src_port, dst_port
4层数据包头中的源/目标端口。端口可以指定为:
1 到 65535之间的整数
/etc/services中的合法服务名称
使用列表的一系列端口
一个范围:
o != (不等于)
o (大于)
o = (大于等于)
o > (反转范围)
最后2个是二元操作符(他们需要2个参数),在范围内不包括参数。
o : (inclusive range)
inclusive range 也是二元操作符但范围内包括参数。
tcp_flags
指定使用TCP协议时TCP头中必须设定的标记。 标记指定的格式是: flags check/mask. 例如: flags S/SA -这指引PF只检查S和A(SYN and ACK)标记,如果SYN标记是“on”则匹配。
state
指定状态信息在规则匹配时是否保持。
keep state - 对 TCP, UDP, ICMP起作用
modulate state - 只对 TCP起作用. PF会为匹配规则的数据包产生强壮的初始化序列号。
synproxy state - 代理外来的TCP连接以保护服务器不受TCP SYN FLOODs欺骗。这个选项包含了keep state 和 modulate state 的功能。
默认拒绝
按照惯例建立防火墙时推荐执行的是默认拒绝的方法。也就是说先拒绝所有的东西,然后有选择的允许某些特定的流量通过防火墙。这个方法之所以是推荐的是因为它宁可失之过于谨慎(也不放过任何风险),而且使得编写规则集变得简单。
产生一个默认拒绝的过滤规则,开始2行过滤规则必须是:
block in all
block out all
这会阻塞任何通信方在任何方向上进入任意接口的所有流量。
通过流量
流量必须被明确的允许通过防火墙或者被默认拒绝的策略丢弃。这是数据包标准如源/目的端口,源/目的地址和协议开始活动的地方。无论何时数据包在被允许通过防火墙时规则都要设计的尽可能严厉。这是为了保证设计中的流量,也只有设计中的流量可以被允许通过。
实例:
# 允许本地网络192.168.0.0/24流量通过dc0接口进入访问openbsd机器的IP地址
#192.168.0.1,同时也允许返回的数据包从dc0接口出去。
pass in on dc0 from 192.168.0.0/24 to 192.168.0.1
pass out on dc0 from 192.168.0.1 to 192.168.0.0/24
# Pass TCP traffic in on fxp0 to the web server running on the
# OpenBSD machine. The interface name, fxp0, is used as the
# destination address so that packets will only match this rule if
# they‘re destined for the OpenBSD machine.
pass in on fxp0 proto tcp from any to fxp0 port www
quick 关键字
每个数据包都要按自上至下的顺序按规则进行过滤。默认情况下,数据包被标记为通过,这个可以被任一规则改变,在到达最后一条规则前可以被来回改变多次,最后的匹配规则是“获胜者”。存在一个例外是:过滤规则中的quick关键字具有取消进一步往下处理的作用,使得规则指定的动作马上执行。看一下下面的例子:
错误:
block in on fxp0 proto tcp from any to any port ssh
pass in all
在这样的条件下,block行会被检测,但永远也不会有效果,因为它后面的一行允许所有的流量通过。
正确:
block in quick on fxp0 proto tcp from any to any port ssh
pass in all
这些规则执行的结果稍有不同,如果block行被匹配,由于quick选项的原因,数据包会被阻塞,而且剩下的规则也会被忽略。
状态保持
PF一个非常重要的功能是“状态保持”或者“状态检测”。状态检测指PF跟踪或者处理网络连接状态的能力。通过存贮每个连接的信息到一个状态表中,PF能够快速确定一个通过防火墙的数据包是否属于已经建立的连接。如果是,它会直接通过防火墙而不用再进行规则检验。
状态保持有许多的优点,包括简单的规则集和优良的数据包处理性能。
PF is able to match packets moving in either direction to state table entries meaning that filter rules which pass returning trafficdon‘t need to be written.
并且,由于数据包匹配状态连接时不再进行规则集的匹配检测,PF用于处理这些数据包的时间大为减少。
当一条规则使用了keep state选项,第一个匹配这条规则的数据包在收发双方之间建立了一个状态。现在,不仅发送者到接收者之间的数据包匹配这个状态绕过规则检验,而且接收者回复发送者的数据包也是同样的。例如:
pass out on fxp0 proto tcp from any to any keep state
这允许fxp0接口上的任何TCP流量通过,并且允许返回的流量通过防火墙。状态保持是一个非常有用的特性,由于状态查询比使用规则进行数据包检验快的多,因此它可以大幅度提高防火墙的性能。
状态调整选项和状态保持的功能在除了仅适用于TCP数据包以为完全相同。在使用状态调整时,输入连接的初始化序列号(ISN)是随机的,这对于保护某些选择ISN存在问题的操作系统的连接初始化非常有用。从openbsd 3.5开始,状态调整选项可以应用于包含非TCP的协议规则。
对输出的TCP, UDP, ICMP数据包保持状态,并且调整TCP ISN。
pass out on fxp0 proto { tcp, udp, icmp } from any to any modulate state
状态保持的另一个优点是ICMP通信流量可以直接通过防火墙。例如,如果一个TCP连接使用了状态保持,当和这个TCP连接相关的ICMP数据包到来时,它会自动找到合适的状态记录,直接通过防火墙。
状态记录的范围被state-policy runtime选项总体控制,也能基于单条规则由if-bound, group-bound, 和 floating state选项关键字设定。这些针对单条规则的关键字在使用时具有和state-policy选项同样的意义。例如:
pass out on fxp0 proto { tcp, udp, icmp } from any to any modulate state (if-bound)
状态规则指示为了使数据包匹配状态条目,它们必须通过fxp0网络接口传递。
需要注意的是,nat,binat,rdr规则隐含在连接通过过滤规则集审核的过程中产生匹配连接的状态。
UDP状态保持
“不能为UDP产生状态,因为UDP是无状态的协议”。确实,UDP通信会话没有状态的概念(明确的开始和结束通信),这丝毫不影响PF为 UDP会话产生状态的能力。对于没有开始和结束数据包的协议,PF仅简单追踪匹配的数据部通过的时间。如果到达超时限制,状态被清除,超时的时间值可以在pf.conf配置文件中设定。
TCP 标记
基于标记的TCP包匹配经常被用于过滤试图打开新连接的TCP数据包。TCP标记和他们的意义如下所列:
* F : FIN - 结束; 结束会话
* S : SYN - 同步; 表示开始会话请求
* R :RST - 复位;中断一个连接
* P : PUSH - 推送; 数据包立即发送
* A : ACK - 应答
* U : URG - 紧急
* E : ECE - 显式拥塞提醒回应
* W : CWR - 拥塞窗口减少
要使PF在规则检查过程中检查TCP标记,flag关键需按如下语法设置。
flags check/mask
mask部分告诉PF仅检查指定的标记,check部分说明在数据包头中哪个标记设置为“on”才算匹配。
pass in on fxp0 proto tcp from any to any port ssh flags S/SA
上面的规则通过的带SYN标记的TCP流量仅查看SYN和ACK标记。带有SYN和ECE标记的数据包会匹配上面的规则,而带有SYN和ACK的数据包或者仅带有ACK的数据包不会匹配。
注意:在前面的openbsd版本中,下面的语法是支持的:
. . . flags S
现在,这个不再支持,mask必须被说明。
标记常常和状态保持规则联合使用来控制创建状态条目:
pass out on fxp0 proto tcp all flags S/SA keep state
这条规则允许为所有输出中带SYN和ACK标记的数据包中的仅带有SYN标记的TCP数据包创建状态。
使用标记时必须小心,理解在做什么和为什么这样做。小心听取建议,因为相当多的建议时不好的。一些人建议创建状态“只有当SYN标记设定而没有其他标记”时,这样的规则如下:
. . . flags S/FSRPAUEW 糟糕的主意!!
这个理论是,仅为TCP开始会话时创建状态,会话会以SYN标记开始,而没有其他标记。问题在于一些站点使用ECN标记开始会话,而任何使用ECN连接你的会话都会被那样的规则拒绝。比较好的规则是:
. . . flags S/SAFR
这个经过实践是安全的,如果流量进行了整形也没有必要检查FIN和RST标记。整形过程会让PF丢弃带有非法TCP标记的进入数据包(例如SYN和FIN以及SYN和RST)。强烈推荐总是进行流量整形:
scrub in on fxp0
.
.
.
pass in on fxp0 proto tcp from any to any port ssh flags S/SA \
keep state
TCP SYN 代理
通过,当客户端向服务器初始化一个TCP连接时,PF会在二者直接传递握手数据包。然而,PF具有这样的能力,就是代理握手。使用握手代理,PF自己会和客户端完成握手,初始化和服务器的握手,然后在二者之间传递数据。这样做的优点是在客户端完成握手之前,没有数据包到达服务器。这样就消沉了TCP SYN FLOOD欺骗影响服务器的问题,因为进行欺骗的客户端不会完成握手。
TCP SYN 代理在规则中使用synproxy state关键字打开。例如:
pass in on $ext_if proto tcp from any to $web_server port www \
flags S/SA synproxy state
这样, web服务器的连接由PF进行TCP代理。
由于synproxy state工作的方式,它具有keep state 和 modulate state一样的功能。
如果PF工作在桥模式下,SYN代理不会起作用。
阻塞欺骗数据包
地址欺骗是恶意用户为了隐藏他们的真实地址或者假冒网络上的其他节点而在他们传递的数据包中使用虚假的地址。一旦实施了地址欺骗,他们可以隐蔽真实地址实施网络攻击或者获得仅限于某些地址的网络访问服务。
PF通过antispoof关键字提供一些防止地址欺骗的保护。
antispoof [log] [quick] for interface [af]
log
指定匹配的数据包应该被pflogd进行日志记录
quick
如果数据包匹配这条规则,则这是最终的规则,不再进行其他规则集的检查。
interface
激活要进行欺骗保护的网络接口。也可以是接口的列表。
af
激活进行欺骗保护的地址族,inet代表Ipv4,inet6代表Ipv6。
实例:
antispoof for fxp0 inet
当规则集载入时,任何出现了antispoof关键字的规则都会扩展成2条规则。假定接口fxp0具有ip地址10.0.0.1和子网掩码255.255.255.0(或者/24),上面的规则会扩展成:
block in on ! fxp0 inet from 10.0.0.0/24 to any
block in inet from 10.0.0.1 to any
这些规则实现下面的2个目的:
* 阻塞任何不是由fxp0接口进入的10.0.0.0/24网络的流量。由于10.0.0.0/24的网络是在fxp0接口,具有这样的源网络地址的数据包绝不应该从其他接口上出现。
* 阻塞任何由10.0.0.1即fxp0接口的IP地址的进入流量。主机绝对不会通过外面的接口给自己发送数据包,因此任何进入的流量源中带有主机自己的IP地址都可以认为是恶意的!
注意:antispoof规则扩展出来的过滤规则会阻塞loopback接口上发送到本地地址的数据包。这些数据包应该明确的配置为允许通过。例如:
pass quick on lo0 all
antispoof for fxp0 inet
使用antispoof应该仅限于已经分配了IP地址的网络接口,如果在没有分配IP地址的网络接口上使用,过滤规则会扩展成:
block drop in on ! fxp0 inet all
block drop in inet all
这样的规则会存在阻塞所有接口上进入的所有流量的危险。
被动操作系统识别
被动操作系统识别是通过基于远端主机TCP SYN数据包中某些特征进行操作系统被动检测的技术。这些信息可以作为标准在过滤规则中使用。
PF检测远端操作系统是通过比较TCP SYN数据包中的特征和已知的特征文件对照来确定的,特征文件默认是/etc/pf.os。如果PF起作用,可是使用下面的命令查看当前的特征列表。
# pfctl -s osfp
在规则集中,特征可以指定为OS类型,版本,或者子类型/补丁级别。这些条目在上面的pfctl命令中有列表。要在过滤规则中指定特征,需使用os关键字:
pass in on $ext_if any os OpenBSD keep state
block in on $ext_if any os "Windows 2000"
block in on $ext_if any os "Linux 2.4 ts"
block in on $ext_if any os unknown
指定的操作系统类型unknow允许匹配操作系统未知的数据包。
注意以下的内容::
*操作系统识别偶尔会出错,因为存在欺骗或者修改过的使得看起来象某个操作系统得数据包。
* 某些修改或者打过补丁得操作系统会改变栈得行为,导致它或者和特征文件不符,或者符合了另外得操作系统特征。
* OSFP 仅对TCP SYN数据包起作用,它不会对其他协议或者已经建立得连接起作用。
IP 选项
默认情况下,PF阻塞带有IP选项得数据包。这可以使得类似nmap得操作系统识别软件工作困难。如果应用程序需要通过这样的数据包,例如多播或者IGMP,可以使用allow-opts关键字
pass in quick on fxp0 all allow-opts
过滤规则实例
下面是过滤规则得实例。运行PF的机器充当防火墙,在一个小得内部网络和因特网之间。只列出了过滤规则,queueing, nat, rdr,等等没有在实例中列出。
ext_if = "fxp0"
int_if = "dc0"
lan_net = "192.168.0.0/24"
# scrub incoming packets
scrub in all
# setup a default deny policy
block in all
block out all
# pass traffic on the loopback interface in either direction
pass quick on lo0 all
# activate spoofing protection for the internal interface.
antispoof quick for $int_if inet
# only allow ssh connections from the local network if it‘s from the
# trusted computer, 192.168.0.15. use "block return" so that a TCP RST is
# sent to close blocked connections right away. use "quick" so that this
# rule is not overridden by the "pass" rules below.
block return in quick on $int_if proto tcp from ! 192.168.0.15 \
to $int_if port ssh flags S/SA
# pass all traffic to and from the local network
pass in on $int_if from $lan_net to any
pass out on $int_if from any to $lan_net
# pass tcp, udp, and icmp out on the external (Internet) interface.
# keep state on udp and icmp and modulate state on tcp.
pass out on $ext_if proto tcp all modulate state flags S/SA
pass out on $ext_if proto { udp, icmp } all keep state
# allow ssh connections in on the external interface as long as they‘re
# NOT destined for the firewall (i.e., they‘re destined for a machine on
# the local network). log the initial packet so that we can later tell
# who is trying to connect. use the tcp syn proxy to proxy the connection.
pass in log on $ext_if proto tcp from any to { !$ext_if, !$int_if } \
port ssh flags S/SA synproxy state
简介
网络地址转换是映射整个网络(或者多个网络)到单个IP地址的方法。当ISP分配的IP地址数目少于要连上互联网的计算机数目时,NAT是必需的。NAT在RFC1631中描述。
NAT允许使用RFC1918中定义的保留地址族。典型情况下,内部网络可以下面的地址族:
10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
担任NAT任务的操作系统必须至少要2块网卡,一块连接到因特网,另一块连接内部网络。NAT会转换内部网络的所有请求,使它们看起来象是来自进行NAT工作的主机系统。
NAT如何工作
当内部网络的一个客户端连接因特网上的主机时,它发送目的是那台主机的IP数据包。这些数据包包含了达到连接目的的所有信息。NAT的作用和这些信息相关。
* 源 IP 地址 (例如, 192.168.1.35)
* 源 TCP 或者 UDP 端口 (例如, 2132)
当数据包通过NAT网关时,它们会被修改,使得数据包看起来象是从NAT网关自己发出的。NAT网关会在它的状态表中记录这个改变,以便:
a)将返回的数据包反向转换
b)确保返回的数据包能通过防火墙而不会被阻塞。
例如,会发生下面的改变:
* 源 IP: 被网关的外部地址所替换(例如, 24.5.0.5)
* 源端口:被随机选择的网关没有在用的端口替换 (例如, 53136)
内部主机和因特网上的主机都不会意识到发生了这个转变步骤。对于内部主机,NAT系统仅仅是个因特网网关,对于因特网上的主机,数据包看起来直接来自NAT系统,它完全不会意识到内部工作站的存在。
当因特网网上的主机回应内部主机的数据包时,它会使用NAT网关机器的外部地址和转换后的端口。然后NAT网关会查询状态表来确定返回的数据包是否匹配某个已经建立的连接。基于IP地址和端口的联合找到唯一匹配的记录告诉PF这个返回的数据包属于内部主机192.168.1.35。然后PF会进行和出去的数据包相反的转换过程,将返回的数据包传递给内部主机。
ICMP数据包的转换也是类似的,只是不进行源端口的修改。
注意:转换后的数据包仍然会通过过滤引擎,根据定义的过滤规则进行阻塞或者通过。唯一的例外是如果nat规则中使用了pass关键字,会使得经过nat的数据包直接通过过滤引擎。
还要注意由于转换是在过滤之前进行,过滤引擎所看到的是上面“nat如何工作”中所说的经过转换后的ip地址和端口的数据包。
IP 转发
由于NAT经常在路由器和网关上使用,因此IP转发是需要的,使得数据包可以在UNIX机器的不同网络接口间传递。IP转发可以通过sysctl命令打开:
# sysctl -w net.inet.ip.forwarding=1
# sysctl -w net.inet6.ip6.forwarding=1 (if using IPv6)
要使这个变化永久生效,可以增加如下行到/etc/sysctl.conf文件中:
net.inet.ip.forwarding=1
net.inet6.ip6.forwarding=1
这些行是本来就存在的,但默认安装中被#前缀注释掉了。删除#,保存文件,IP转发在机器重启后就会发生作用。
配置NAT
一般的NAT规则格式在pf.conf文件中是这个样子:
nat [pass] on interface [af] from src_addr [port src_port] to \
dst_addr [port dst_port] -> ext_addr [pool_type] [static-port]
nat
开始NAT规则的关键字。
pass
使得转换后的数据包完全绕过过滤规则。
interface
进行数据包转换的网络接口。
af
地址类型,inet代表Ipv4,inet6代表Ipv6。PF通常能根据源/目标地址自动确定这个参数。
src_addr
被进行转换的IP头中的源(内部)地址。源地址可以指定为:
单个的Ipv4或者Ipv6地址.
CIDR 网络地址.
能够在规则集载入时通过DNS解析到的合法的域名,IP地址会替代规则中的域名。
网络接口名称。网络接口上配置的所有ip地址会替代进规则中。
带有/掩码(例如/24)的网络接口的名称。每个根据掩码确定的CIDR网络地址都会被替代进规则中。.
带有()的网络接口名称。这告诉PF如果网络接口的IP地址改变了,就更新规则集。这个对于使用DHCP或者拨号来获得IP地址的接口特别有用,IP地址改变时不需要重新载入规则集。
带有如下的修饰词的网络接口名称:
o :network - 替代CIDR网络地址段 (例如, 192.168.0.0/24)
o :broadcast - 替代网络广播地址(例如, 192.168.0.255)
o :peer - 替代点到点链路上的ip地址。
另外,:0修饰词可以附加到接口名称或者上面的修饰词后面指示PF在替代时不包括网络接口的其余附加(alias)地址。这些修饰词也可以在接口名称在括号()内时使用。例如:fxp0:network:0
表.
上面的所有项但使用!(非)修饰词
使用列表的一系列地址.
关键字 any 代表所有地址
关键字 all 是 from any to any的缩写。
src_port
4层数据包头中的源端口。端口可以指定为:
1 到 65535之间的整数
/etc/services中的合法服务名称
使用列表的一系列端口
一个范围:
o != (不等于)
o (大于)
o = (大于等于)
o > (反转范围)
最后2个是二元操作符(他们需要2个参数),在范围内不包括参数。
o : (inclusive range)
inclusive range 也是二元操作符但范围内包括参数。
Port选项在NAT规则中通常不使用,因为目标通常会NAT所有的流量而不过端口是否在使用。
dst_addr
被转换数据包中的目的地址。目的地址类型和源地址相似。
dst_port
4层数据包头中的目的目的端口,目的端口类型和源端口类型相似。
ext_addr
NAT网关上数据包被转换后的外部地址。外部地址可以是:
单个的Ipv4或者Ipv6地址.
CIDR 网络地址.
能够在规则集载入时通过DNS解析到的合法的域名,IP地址会替代规则中的域名。
网络接口名称。网络接口上配置的所有ip地址会替代进规则中。
带有/掩码(例如/24)的网络接口的名称。每个根据掩码确定的CIDR网络地址都会被替代进规则中。.
带有()的网络接口名称。这告诉PF如果网络接口的IP地址改变了,就更新规则集。这个对于使用DHCP或者拨号来获得IP地址的接口特别有用,IP地址改变时不需要重新载入规则集。
带有如下的修饰词的网络接口名称:
o :network - 替代CIDR网络地址段 (例如, 192.168.0.0/24)
o :broadcast - 替代网络广播地址(例如, 192.168.0.255)
o :peer - 替代点到点链路上的ip地址。
另外,:0修饰词可以附加到接口名称或者上面的修饰词后面指示PF在替代时不包括网络接口的其余附加(alias)地址。这些修饰词也可以在接口名称在括号()内时使用。例如:fxp0:network:0
使用列表的一系列地址.
pool_type
指定转换后的地址池的类型
static-port
告诉PF不要转换TCP和UDP数据包中的源端口
这条规则最简单的形式如下:
nat on tl0 from 192.168.1.0/24 to any -> 24.5.0.5
这条规则是说对从tl0网络接口上到来的所有192.168.1.0/24网络的数据包进行NAT,将源地址转换为24.5.0.5。
尽管上面的规则是正确的,但却不是推荐的形式。因为维护起来有困难,当内部或者外部网络有变化时都要修改这一行。比较一下下面这条比较容易维护的规则:(tl0时外部,dc0是内部):
nat on tl0 from dc0:network to any -> tl0
优点是相当明显的,可以任意改变2个接口的IP地址而不用改变这条规则。
象上面这样在地址转换中使用接口名称时,IP地址在pf.conf文件载入时确定,并不是凭空的。如果使用DHCP还配置外部地址,这会存在问题。如果分配的IP地址改变了,NAT仍然会使用旧的IP地址转换出去的数据包。这会导致对外的连接停止工作。为解决这个问题,应该给接口名称加上括号,告诉PF自动更新转换地址。
nat on tl0 from dc0:network to any -> (tl0)
这个方法对IPv4 和 IPv6地址都用效。
双向映射可以通过使用binat规则建立。Binat规则建立一个内部地址和外部地址一对一的映射。这会很有用,比如,使用独立的外部IP地址用内部网络里的机器提供web服务。从因特网到来连接外部地址的请求被转换到内部地址,同时由(内部)web服务器发起的连接(例如DNS查询)被转换为外部地址。和NAT规则不过,binat规则中的tcp和udp端口不会被修改。
例如:
web_serv_int = "192.168.1.100"
web_serv_ext = "24.5.0.6"
binat on tl0 from $web_serv_int to any -> $web_serv_ext
转换规则例外设置
使用no关键字可以在转换规则中设置例外。例如,如果上面的转换规则修改成这样:
no nat on tl0 from 192.168.1.10 to any
nat on tl0 from 192.168.1.0/24 to any -> 24.2.74.79
则除了192.168.1.10以外,整个192.168.1.0/24网络地址的数据包都会转换为外部地址24.2.74.79。
注意第一条匹配的规则起了决定作用,如果是匹配有no的规则,数据包不会被转换。No关键字也可以在binat和rdr规则中使用。
检查 NAT 状态
要检查活动的NAT转换可以使用pfctl带-s state 选项。这个选项列出所有当前的NAT会话。
# pfctl -s state
fxp0 TCP 192.168.1.35:2132 -> 24.5.0.5:53136 -> 65.42.33.245:22 TIME_WAIT:TIME_WAIT
fxp0 UDP 192.168.1.35:2491 -> 24.5.0.5:60527 -> 24.2.68.33:53 MULTIPLE:SINGLE
解释 (对第一行):
fxp0
显示状态绑定的接口。如果状态是浮动的,会出现self字样。
TCP
连接使用的协议。
192.168.1.35:2132
内部网络中机器的IP地址 (192.168.1.35),源端口(2132)在地址后显示,这个也是被替换的IP头中的地址。 of the machine on the internal network. The
source port (2132) is shown after the address. This is also the address
that is replaced in the IP header.
24.5.0.5:53136
IP 地址 (24.5.0.5) 和端口 (53136) 是网关上数据包被转换后的地址和端口。
65.42.33.245:22
IP 地址 (65.42.33.245) 和端口 (22) 是内部机器要连接的地址和端口。
TIME_WAIT:TIME_WAIT
这表明PF认为的目前这个TCP连接的状态。
如果在办公地点应用了NAT,内部网所有的机器都可以访问因特网。但如何让NAT网关后面的机器能够被从外部访问?这就是重定向的来源。重定向允许外来的流量发送到NAT网关后面的机器。
看个例子:
rdr on tl0 proto tcp from any to any port 80 -> 192.168.1.20
这一行重定向了TCP端口80(web服务器)流量到内部网络地址192.168.1.20。因此,即使192.168.1.20在网关后面的内部网络,外部仍然能够访问它。
上面rdr行中from any to any部分非常有用。如果明确知道哪些地址或者子网被允许访问web服务器的80端口,可以在这部分严格限制:
rdr on tl0 proto tcp from 27.146.49.0/24 to any port 80 -> 192.168.1.20
这样只会重定向指定的子网。注意这表明可以重定向外部不同的访问主机到网关后面不同的机器上。这非常有用。例如,如果知道远端的用户连接上来时使用的IP地址,可以让他们使用网关的IP地址和端口访问他们各自的桌面计算机。
rdr on tl0 proto tcp from 27.146.49.14 to any port 80 -> \
192.168.1.20
rdr on tl0 proto tcp from 16.114.4.89 to any port 80 -> \
192.168.1.22
rdr on tl0 proto tcp from 24.2.74.178 to any port 80 -> \
192.168.1.23
重定向和包过滤
注意:转换后的数据包仍然会通过过滤引擎,根据定义的过滤规则进行阻塞或者通过。唯一的例外是如果rdr规则中使用了pass关键字,会使得重定向的数据包直接通过过滤引擎。
还要注意由于转换是在过滤之前进行,过滤引擎所看到的是在匹配rdr规则经过转换后的目标ip地址和端口的数据包。
* 192.0.2.1 - 因特网上的主机
* 24.65.1.13 - openbsd路由器的外部地址
* 192.168.1.5 - web服务器的内部IP地址
rdr on tl0 proto tcp from 192.0.2.1 to 24.65.1.13 port 80 \
-> 192.168.1.5 port 8000
数据包在经过rdr规则前的模样:
* 源地址: 192.0.2.1
* 源端口: 4028 (由操作系统任意选择)
* 目的地址: 24.65.1.13
* 目的端口: 80
数据包经过rdr规则后的模样:
* 源地址: 192.0.2.1
* 源端口: 4028
* 目的地址: 192.168.1.5
* 目的: 8000
过滤引擎看见的IP数据包时转换发生之后的情况。
安全隐患
重定向确实存在安全隐患。在防火墙上开了一个允许流量进入内部网络的洞,被保护的网络安全潜在的受到了威胁!例如,如果流量被转发到了内部的web服务器,而web服务器上允许的守护程序或者CGI脚本程序存在漏洞,则这台服务器存在会被因特网网上的入侵者攻击危险。如果入侵者控制了这台机器,就有了进入内部网络的通道,仅仅是因为这种流量是允许通过防火墙的。
这种风险可以通过将允许外部网络访问的系统限制在一个单独的网段中来减小。这个网段通常也被称为非军事化区域(DMZ)或者私有服务网络(PSN)。通过这个方法,如果web服务器被控制,通过严格的过滤进出DMZ/PSN的流量,受影响的系统仅限于DMZ/PSN网段。
重定向和反射
通常,重定向规则是用来将因特网上到来的连接转发到一个内部网络或者局域网的私有地址。例如:
server = 192.168.1.40
rdr on $ext_if proto tcp from any to $ext_if port 80 -> $server \
port 80
但是,当一个重定向规则被从局域网上的客户端进行测试时,它不会正常工作。这是因为重定向规则仅适用于通过指定端口($ext_if,外部接口,在上面的例子中)的数据包。从局域网上的主机连接防火墙的外部地址,并不意味着数据包会实际的通过外部接口。防火墙上的TCP/IP栈会把到来的数据包的目的地址在通过内部接口时与它自己的IP地址或者别名进行对比检测。那样的数据包不会真的通过外部接口,栈在任何情况下也不会建立那样的通道。因而,PF永远也不会在外部接口上看到那些数据包,过滤规则由于指定了外部接口也不会起作用。
指定第二条针对内部接口的也达不到预想的效果。当本地的客户端连接防火墙的外部地址时,初始化的TCP握手数据包是通过内部接口到达防火墙的。重定向规则确实起作用了,目标地址被替换成了内部服务器,数据包通过内部接口转发到了内部的服务器。但源地址没有进行转换,仍然包含的是本地客户端的IP地址,因此服务器把它的回应直接发送给了客户端。防火墙永远收不到应答不可能返回客户端信息,客户端收到的应答不是来自它期望的源(防火墙)会被丢弃,TCP握手失败,不能建立连接。
当然,局域网里的客户端仍然会希望象外部客户一样透明的访问这台内部服务器。有如下的方法解决这个问题:
水平分割 DNS
存在这样的可能性,即配置DNS服务器使得它回答内部主机的查询和回答外部主机的查询不一样,因此内部客户端在进行名称解析时会收到内部服务器的地址。它们直接连接到内部服务器,防火墙根本不牵扯。这会降低本地流量,因为数据包不会被送到防火墙。
将服务器移到独立的本地网络
增加单独的网络接口卡到防火墙,把本地的服务器从和客户端同一个网段移动到专用的网段(DMZ)可以让本地客户端按照外部重定向连接的方法一样重定向。使用单独的网段有几个优点,包括和保留的内部主机隔离增加了安全性;服务器(我们的案例中可以从因特网访问)一旦被控制,它不能直接存取本地网络因为所有的连接都必须通过防火墙。
TCP 代理
一般而言,TCP代理可以在防火墙上设置,监听要转发的端口或者将内部接口上到来的连接重定向到它监听的端口。当本地客户端连接防火墙时,代理接受连接,建立到内部服务器的第二条连接,在通信双方间进行数据转发。
简单的代理可以使用inetd和nc建立。下面的/etc/inetd.conf中的条目建立一个监听套接字绑定到lookback地址(127.0.0.1)和端口5000。连接被转发到服务器192.168.1.10的80端口。
127.0.0.1:5000 stream tcp nowait nobody /usr/bin/nc nc -w \
20 192.168.1.10 80
下面的重定向规则转发内部接口的80端口到代理:
rdr on $int_if proto tcp from $int_net to $ext_if port 80 -> \
127.0.0.1 port 5000
RDR 和 NAT 结合
通过对内部接口增加NAT规则,上面说的转换后源地址不变的问题可以解决。
rdr on $int_if proto tcp from $int_net to $ext_if port 80 -> \
$server
no nat on $int_if proto tcp from $int_if to $int_net
nat on $int_if proto tcp from $int_net to $server port 80 -> \
$int_if
这会导致由客户端发起的初始化连接在收到内部接口的返回数据包时转换回来,客户端的源ip地址被防火墙的内部接口地址代替。内部服务器会回应防火墙的内部接口地址,在转发给本地客户端时可以反转NAT和RDR。这个结构是非常复杂的,因为它为每个反射连接产生了2个单独的状态。必须小心配置防止NAT规则应用到了其他流量,例如连接由外部发起(通过其他的重定向)或者防火墙自己。注意上面的rdr规则会导致TCP/IP栈看到来自内部接口带有目的地址是内部网络的数据包。
简介
PF提供了许多方法来进行规则集的简化。一些好的例子是使用宏和列表。另外,规则集的语言或者语法也提供了一些使规则集简化的捷径。首要的规则是,规则集越简单,就越容易理解和维护。
使用宏
宏是非常有用的,因为它提供了硬编码地址,端口号,接口名称等的可选替代。在一个规则集中,服务器的IP地址改变了?没问题,仅仅更新一下宏,不需要弄乱花费了大量时间和精力建立的规则集。
通常的惯例是在PF规则集中定义每个网络接口的宏。如果网卡需要被使用不同驱动的卡取代,例如,用intel代替3com,可以更新宏,过滤规则集会和以前功能一样。另一个优点是,如果在多台机器上安装同样的规则集,某些机器会有不同的网卡,使用宏定义网卡可以使的安装的规则集进行最少的修改。使用宏来定义规则集中经常改变的信息,例如端口号,IP地址,接口名称等等,建议多多实践!
# define macros for each network interface
IntIF = "dc0"
ExtIF = "fxp0"
DmzIF = "fxp1"
另一个惯例是使用宏来定义IP地址和网络,这可以大大减轻在IP地址改变时对规则集的维护。
# define our networks
IntNet = "192.168.0.0/24"
ExtAdd = "24.65.13.4"
DmzNet = "10.0.0.0/24"
如果内部地址扩展了或者改到了一个不同的IP段,可以更新宏为:
IntNet = "{ 192.168.0.0/24, 192.168.1.0/24 }"
当这个规则集重新载入时,任何东西都跟以前一样。
使用列表
来看一下一个规则集中比较好的例子使得RFC1918定义的内部地址不会传送到因特网上,如果发生传送的事情,可能导致问题。
block in quick on tl0 inet from 127.0.0.0/8 to any
block in quick on tl0 inet from 192.168.0.0/16 to any
block in quick on tl0 inet from 172.16.0.0/12 to any
block in quick on tl0 inet from 10.0.0.0/8 to any
block out quick on tl0 inet from any to 127.0.0.0/8
block out quick on tl0 inet from any to 192.168.0.0/16
block out quick on tl0 inet from any to 172.16.0.0/12
block out quick on tl0 inet from any to 10.0.0.0/8
看看下面更简单的例子:
block in quick on tl0 inet from { 127.0.0.0/8, 192.168.0.0/16, \
172.16.0.0/12, 10.0.0.0/8 } to any
block out quick on tl0 inet from any to { 127.0.0.0/8, \
192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }
这个规则集从8行减少到2行。如果联合使用宏,还会变得更好:
NoRouteIPs = "{ 127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, \
10.0.0.0/8 }"
ExtIF = "tl0"
block in quick on $ExtIF from $NoRouteIPs to any
block out quick on $ExtIF from any to $NoRouteIPs
注意虽然宏和列表简化了pf.conf文件,但是实际是这些行会被pfctl(8)扩展成多行,因此,上面的例子实际扩展成下面的规则:
block in quick on tl0 inet from 127.0.0.0/8 to any
block in quick on tl0 inet from 192.168.0.0/16 to any
block in quick on tl0 inet from 172.16.0.0/12 to any
block in quick on tl0 inet from 10.0.0.0/8 to any
block out quick on tl0 inet from any to 10.0.0.0/8
block out quick on tl0 inet from any to 172.16.0.0/12
block out quick on tl0 inet from any to 192.168.0.0/16
block out quick on tl0 inet from any to 127.0.0.0/8
可以看到,PF扩展仅仅是简化了编写和维护pf.conf文件,实际并不简化pf(4)对于规则的处理过程。
宏不仅仅用来定义地址和端口,它们在PF的规则文件中到处都可以用:
pre = "pass in quick on ep0 inet proto tcp from "
post = "to any port { 80, 6667 } keep state"
# David‘s classroom
$pre 21.14.24.80 $post
# Nick‘s home
$pre 24.2.74.79 $post
$pre 24.2.74.178 $post
扩展后:
pass in quick on ep0 inet proto tcp from 21.14.24.80 to any \
port = 80 keep state
pass in quick on ep0 inet proto tcp from 21.14.24.80 to any \
port = 6667 keep state
pass in quick on ep0 inet proto tcp from 24.2.74.79 to any \
port = 80 keep state
pass in quick on ep0 inet proto tcp from 24.2.74.79 to any \
port = 6667 keep state
pass in quick on ep0 inet proto tcp from 24.2.74.178 to any \
port = 80 keep state
pass in quick on ep0 inet proto tcp from 24.2.74.178 to any \
port = 6667 keep state