FreeBSD ipfw简明教程

@vrqq  May 2, 2022

序 route 和 firewall 的关系

例如从网卡到来的TCP包,内容为{src_MAC, dst_MAC, src_IP, dst_IP, TCP_flags, ..., user-data}

  • "TCP/IP包": 参见 OSI七层模型

    • 有的"包"不能精确的定义“属于OSI模型的哪一层”,这不重要,模型里提及的基本概念适用就可以了
  • ether: "虚拟网卡" 可以有IP、 mac地址等。

    • 和物理网卡不同:一个“物理网卡”在系统中可拆成很多"虚拟网卡",例如ssh到路由器看到的 "wl0:0", "eth1:4"等等
    • VLAN: 每个VLAN可以被当作一个ether
  • kernel.route: 作用是 从任何一个地方收到一个"IP包",按路由表把这个包"原封不动"丢到目标ether

    • kernel.route 只处理IP层 他不关心是TCP还是UDP 而且不改内容,所以不在这里做NAT
    • FreeBSD看路由表 netstat -rn
  • firewall: 插在ether和route中间,不仅是只读的防火墙,还可以修改包头(例如做NAT)

    • NAT的原理: 在这里firewall修改了TCP包头,把"实际地址"换成了"当前ether",从而骗过本机route和上游route
    • 不仅可以处理"TCP包" 还可以处理例如"ICMP", "EAP", "UDP"等等(对应到OSI模型的不同层)
    • 常见的有 linux下的iptable, FreeBSD的IPFW
    • 实际还支持例如按user过滤 本文不关心
  • https://www.freebsd.org/cgi/man.cgi?ipfw#PACKET_FLOW

    • to device 当作去往ether (进而调用驱动把binary-data送出去)
    • to upper layers 当作去往kernel.route (和监听者app如nginx 下文仅用route代指)
    • ipfw在图中的[ip_input/output], [ether_demux], [bdg_forward], [ether_output_frame]这五个框的位置进行匹配拦截+包头变换,匹配规则如下一章。

启动 和 ipfw举例

回到这个简图: https://www.freebsd.org/cgi/man.cgi?ipfw#PACKET_FLOW

  • ether和route间共有5个检查点,ipfw是个表 最多有65535行,当有数据包经过检查点时 从头到尾跑一遍这个表决定放行、拒绝或修改
  • 一外来数据包首先到达[ether_demux]检查点,触发MAC地址检查,这个点不看IP地址

    • 如果通过 接下来到达[ip_input]检查点,触发IP+TCP/UDP/ICMP/...检查,这个点不看MAC地址
  • router->ether方向同理,在[ip_output]和[ether_output_frame]两点检查
  • [ether_demux] [ether_output_frame] [bdg_forward] 这三个点位属于"layer2"系列
  • /etc/sysctl.conf中如果写了net.link.ether.ipfw=1,则表示启用[ether_demux]和[ether_output_frame]检查点(不写则使用默认值)

rc.conf
FreeBSD的服务启动均在rc.conf中配置 当作kernel plugin,开机过程中 rc.conf完全加载后才会显示 "Login: ..." prompt。(linux下大部分采用了systemd)
添加如下内容至/etc/rc.conf,并在/etc/ipfw.rules内写启动脚本

## rc.conf
gateway_enable="YES"
firewall_enable="YES"
firewall_nat_enable="YES"
firewall_script="/etc/ipfw.rules"

新增过滤规则

         |<-------------------- RULE FORMAT --------------------------------->|
                                 |<--------------------- RULE BODY ----------->|
ipfw add [rule_number] [action] [proto] from [src]      to [dst] [options]
ipfw add 100           allow    tcp     from any        to me 22 in via vtnet0   #(1)
ipfw add 100           allow    tcp     from me 22      to any   out via vtnet0  #(2)
ipfw add 101           count    tcp     from 10.0.0.0/8 to me    via vtnet0      #(3)
ipfw add 102           allow    tcp     from me         to any   via vtnet0      #(4)
  • (1,2) 匹配22端口的出入流量
  • (3) 统计10.0.0.0/8发来的数据数量包 (但没说放行)
  • (4) 允许tcp类型的数据包从 本机任意端口 发往 任何目的地任何端口
  • in表示dst-IP为本机,out指dst-IP为其他机器

    • in out并非数据流动方向,out数据包也可以经[ip_input]流入route 见NAT一章

keep-state匹配器
现在运行wget https://1.1.1.1/,系统给wget分配个tcp port作出口(例如tcp 33331),发起TCP连接至1.1.1.1:445。但上述"规则4"只允许数据包发出,不允许数据包接收,有去无回,TCP没法握手。
于是使用check-state配合keep-state让ipfw自适应:rule_102标记了当本机发起新建TCP连接时 记录

         |<-------------------- RULE FORMAT ------------------------------------ ->|
                                    |<----------------- RULE BODY ----------------->|
ipfw add [rule_number] [action]    [proto] from [src] to [dst] [options............]
ipfw add 102           allow       all     from me    to any   via vtnet0 keep-state  #(4)
ipfw add 103           check-state                                                    #(5)
  • (4) 修改102号规则后,当本机(me)发起tcp连接时,会将"双方ip + port"记录下来,在(5)匹配刚刚创建的新规

    • 如果不写(5) 也会导致数据包有去无回
    • 4和5没有顺序要求,4是检测出包,5是检测回包
  • (5) keep-state: Upon a match, the firewall will create a dynamic rule, whose default behaviour is to match bidirectional traffic between source and destination IP/port using the same protocol.

NAT转换器
就是家中的wifi路由器,让局域网能上网,原理是在通过网线把数据包送出之前,改掉包头,欺骗上级网络只有一个用户在用。
回到这个简图: https://www.freebsd.org/cgi/man.cgi?ipfw#PACKET_FLOW
前文说的都是READONLY accept/drop,而NAT转换器在[ip_input]和[ip_output]这两个检查点"修改"数据包头

举例:vtnet0网卡同时连接子网客户端和公网,子网网段10.0.0.0/16,当前venet的IP 47.52.68.90 (下文IP未体现 使用if vtnet0 表示自动跟踪venet0的网卡IP)

ipfw add [rule_num] [action] [proto] from [src]       to [dst] [options.....]
ipfw add 203        nat 2    all     from 10.0.0.0/16 to any   out via vtnet0 (6)
ipfw add 204        nat 2    all     from any         to me    in via vtnet0  (7)

ipfw nat [natid] config [config-options..........]
ipfw nat 2       config if vtnet0 same_ports reset deny_in (8)
  • 当前配置:单个数据包,在所有检查点中,仅成功匹配一次,后续便不再检查
  • (8) 创建一个"2号NAT黑盒",盒中记录了 "子网ip:port" 和 "公网ip:port" 对应关系

    • deny_in属性表示:若未找到记录,直接reject当前包(并不再匹配任何其他项目)
    • ipfw nat show config 显示所有NAT黑盒
  • (6) 客户端IP=10.0.7.7发起一个包,dst-IP=1.1.1.1(公网)

    • 数据包 TCP 10.0.6.1:54588 1.1.1.1:443 out via vtnet0
    • 首先进入[ip_input]检查点 成功匹配203记录,然后"2号NAT盒"将src-IP换为vtnet0(47.52.68.90)
    • 修改后数据包 TCP 47.52.68.90:54588 1.1.1.1:443 out via vtnet0
    • 然后新的数据包进入kernel-route,由于仅匹配一次,故直接丢给venet0驱动送走了
    • 在[ip_output]检查点匹配当前203记录,成功后送往"2号NAT盒"
    • 2号NAT盒 将这个包的src-IP变为当前vtnet0的ip(47.52.68.90)
  • (7) 当上述请求包的response 被1.1.1.1返回

    • 数据包 TCP 1.1.1.1:443 47.52.68.90:54588 in via vtnet0
    • 在[ip_input]检查点匹配到当前204记录,成功后送入"2号NAT盒"修改包头
    • 2号NAT盒 查表后 将这个包dst-IP变为10.0.7.7后,(由于已经匹配过)直接送还至响应客户端
  • 注意上述"203, 204两条规则"要在"上一段中104规则"之后,这样外来数据包,先由104规则匹配"the response of 当前主机发出的请求",未找到才"进入2号nat黑盒尝试查找NAT表",若再未找到,则由"2号nat盒"拒绝

    • 因为公网传回的数据包,dst-IP都是vtnet0 IP (47.52.68.90),需要在"104规则"和"2号NAT盒(triggered by 204)"两处查表

注:按官方文档,使用in-kernel NAT需要在/etc/rc.conf添加如下:

gateway_enable="YES"
firewall_enable="YES"
firewall_nat_enable="YES"

文档 https://docs.freebsd.org/en/books/handbook/firewalls/#firewalls-ipfw

Debug

  • 在 rc.conf中添加 firewall_logging="YES",然后tail -f /var/log/security看一看
  • example: ipfw add 99 count log all from 1.1.1.1 to any
  • example: ipfw add 2099 count log all from 10.0.0.0/16 to me via vtnet0

其他补充

  • set: 名词"集合、组" 不是动词

后记

Linux下iptable和route的关系图
来自: https://www.cnblogs.com/EasonJim/p/8424731.html

Reference

配置样例

  • Jail

Strongswan (swanctl)
https://blog.vrqq.org/archives/58/

#!/bin/sh
WAN="vtnet0"

##clear all
ipfw -q -f flush

## Isolate 'lo0' with other ethernet
ipfw add 1000 allow all from any to any via lo0
ipfw add 1001 deny all from any to 127.0.0.0/8
ipfw add 1001 deny all from 127.0.0.0/8 to any

## allow service via WAN-interface
# TCP22 SSH
ipfw add 2000 allow tcp from any to me 22 via $WAN
ipfw add 2000 allow tcp from me 22 to any via $WAN

# TCP80, TCP443, UDP443 HTTP/HTTPS
ipfw add 2001 allow tcp from any to me 80,443 via $WAN
ipfw add 2001 allow tcp from me 80,443 to any via $WAN
ipfw add 2001 allow udp from me 443 to any via $WAN
ipfw add 2001 allow udp from any to me 443 via $WAN

# StrongSwan IKEv2 Part1: Established
# ESP, AH, IPENCAP, UDP5000, UDP4500
ipfw add 2010 allow udp from any to me 500,4500 via $WAN
ipfw add 2010 allow udp from me 500,4500 to any via $WAN
ipfw add 2012 allow esp from any to any via $WAN
ipfw add 2013 allow ah from any to any via $WAN
ipfw add 2014 allow ipencap from any to any in recv $WAN

## Allow "auto create IN-rules" for each output request
ipfw add 3000 check-state
ipfw add 3002 allow all from me to any keep-state

# StrongSwan IKEv2 Part2: NAT
# masquerade: 10.0.0.0/16 -> $WAN
ipfw nat 2 config if $WAN same_ports reset deny_in
ipfw add 3015 nat 2 all from 10.0.0.0/16 to any out via $WAN
ipfw add 3016 nat 2 all from any to me in via $WAN

添加新评论