Netfilter 是 Linux 内核网络协议栈提供的网络管理框架。其功能非常强大,能够对流经内核网络栈的数据包做任何修改,可以轻而易举的实现防火墙(Firewall)、NAT、策略路由(Policy Based Routing)等功能。Netfilter 可以通过用户态的 iptables, iproute2 等工具管理。
mark
mark 用于对经过本机网络栈数据包作标记,可以配合策略路由等。mark 是一个 int 型数字(对于32位系统即32位数字,64位系统则为 64位数字。默认值 0),可以独立设置或测试匹配 mark 的每一个 bit 位;按照惯例,通常在 mangle 表里设置(修改)mark。
如果需要对 OUTPUT (本机发出的) 数据包都设置 mark,在 -t mangle OUTPUT 链里设置规则。
如果需要对 FORWARD (本机接收并转发的) 数据包设置 mark,在 -t mangle PREROUTING 链里设置规则(注意在 -t mangle FORWARD 里设置无效!)。
# 对所有 dst IP 属于 "chinaip" 这个 ipset 的数据包打上 mark bit 0
# 然后对所有 mark bit 0 == 1 的数据包做策略路由,使其走系统默认路由
ipset create china hash:net
ipset add china 1.0.1.0/24
#... add more China IP range entry to IP set
# mark format: VALUE[/mask]
iptables -t mangle -A PREROUTING -m set --match-set china dst -j MARK --set-mark 0x1/0x1
iptables -t mangle -A OUTPUT -m set --match-set china dst -j MARK --set-mark 0x1/0x1
ip rule add fwmark 0x1/0x1 lookup main prio 1
参考:中国 IP
iptables 中可以使用 -m mark --mark value[/mask]
参数作为条件测试匹配当前数据包 mark。
dscp
dscp (Differentiated Services Code Point) 是 IPV4 header 里 6 bits 长度的固定字段,设计初衷是用来对网络流量做 QoS。实际中,大部分网络流量的 ip frame 均未配置 dscp (所有 bit 全为 0)
dscp-class : 预置(pre-defined) 的一系列名字,对应 dscp 值。参考资料。
- Default : 0
- CS1-CS7 : 1-7
在本地网络里,dscp 可以配合 iptables / MARK 实现复杂的策略路由,其实际效果等效于跨设备的 mark。与使用 VLAN 等方式相比,这种方式好处是网络拓扑简单,配置方便,所有流量位于同一个二层和三层网络,可以对同一个设备发送或转发的的流量区分不同的路由策略。
例如,为所有本机转发发送的数据包打 dscp 标记:
iptables -t mangle -A FORWARD -j DSCP --set-dscp 1
iptables -t mangle -A FORWARD -j DSCP --set-dscp-class CS1
# 示例:对 172.17.0.2 这个 ip 的 docker 容器发出流量打 dscp
# docker run 启动容器时可以用 --ip 指定容器使用固定 IP
iptables -t mangle -A FORWARD -s 172.17.0.2 -j DSCP --set-dscp 1
匹配本机收到的数据包的 dscp,然后打 0x1 MARK
iptables -t mangle -A PREROUTING -m dscp --dscp 1 -j MARK --set-mark 0x1/0x1
对于 OpenWrt,需要安装相应的包才能使用 iptables -m dscp:
opkg install iptables-mod-ipopt kmod-ipt-ipopt
ISP 很可能会无视用户发送数据包的 dscp (甚至用用户反馈 ISP 会丢弃收到的 dscp 不为 0 的数据包,但未证实),可以在 WAN 出口重置所有 ip 包 dscp,或设为特定值:
iptables -t mangle -A POSTROUTING -o pppoe-wan -j DSCP --set-dscp 0
iptables -t mangle -A POSTROUTING -o eth0.2 -j DSCP --set-dscp 0
部分地区宽带接收的(来源 ISP 的)数据包本身也可能带 dscp 标记,例如测试江苏电信 ISP 发送的流量有 dscp = 8 标记。为避免 ISP 流量自带的 dscp 对内网的干扰,可以在网关重置其:
iptables -t mangle -A PREROUTING -i pppoe-wan -j DSCP --set-dscp 0
iptables -t mangle -A PREROUTING -i eth0.2 -j DSCP --set-dscp 0
局域网里的 Windows 设备可以通过 Network QoS Policy配置指定应用程序(例如 curl.exe) 发出流量的 dscp 值,参考:Windows/dscp。
Traversing of packets
网络数据包通过 iptables tables / chains 的流程: Docs
Receive:
某个interface收到数据包 -> PREROUTING (manage, nat) -> routing ->
是发送给本机的数据包? -> INPUT (manage, filter) -> app
不是 -> FORWARD (manage, filter) -> POSTROUTING (manage, nat) -> 某个interface发出
Send:
app发送数据包 -> routing -> OUTPUT (manage, nat, filter) -> (re)routing -> POSTROUTING (manage, nat) -> 某个interface发出
注:
- nat 表与会话管理(nf_conntrack)相关,每个连接只有第一个包会经过 nat 表,后面的包会直接按照之前包同样方式处理。
两次 routing
对于本机 app 发出(outcoming)的流量,netfilter 有2次 routing 过程,上面的流程图里称为 routing 和 (re)routing。关于这两次 routing:
- OUTPUT 链在 第一次 routing 之后。
- 对于本机 app 发出的流量,第一次 routing 除了确定下一跳之外,对于没有指定源IP的数据包,还将会为其选择源IP地址。
- 当数据包经过了iptables OUTPUT链,某条rule为其打上了fwmark或者改变了其目标地址后,由于数据包属性已经改变,需要第二次路由,即 (re)routing。Linux内核协议栈在实现第一次路由和第二次路由时,其逻辑是一样的。但请注意由于第一次路由时会为skb选择source地址,那么第二次路由时的命中路由条目的source属性将永远不会生效,所以多个网口设备策略路由时常常需要用 MASQUERADE 重写 outcoming 数据包的 src IP 以保证正确,这是一种 workaround。
- 第一次 routing 时如果未找到匹配的路有条目,会直接失败("Network is unreachable"),不会再继续经过 iptables 各个链。配置错误时会出现这种情况,用 ip route get 有时无法检测出来,例如对于 "ip route get 8.8.8.8 mark 0x2" 这种指定 mark 的流量,由于第一次 routing 时还没有 MARK (打 mark 在 -t mangle PREROUTING / OUTPUT 阶段),如果对于无 MARK 的流量 ip rule / ip route 找不到路由条目,则实际上网络访问会失败。这种情况解决方法是添加一个 dummy 的默认路由专用于第一阶段 routing,只要语法正确即可,即使实际上路由项不合法(比如下一跳IP地址没有对应的机器)亦可,因为这个路由项的目的只是让流量继续在 netfilter flow 里走下去,真正使用的路由条目会在第二次 routing 时被选择。
Flow graph
完整流程图:(注:此图有个小问题,filter FORWAED 后面应该直接是 mangle POSTROUTING, 没有 route decision)
另一张结构很清晰的流程图:
Tips
- RAW table 只与 conntrack 有关。When one drops a packet in raw table, the packet never reaches the conntrack module. This means that no connection tracking entry is created / consulted during packet's flow in the blocked direction. 而在 RAW 被 DROP 的包仍然继续进入 mangle / filter / nat, 并且最终可以正常被本机(INPUT)的应用程序接收。在实际使用中,iptables -t raw -A PREROUTING -p tcp --dport 1081 -j DROP 常常用于配合反向代理程序(例如convey(设计文档)),阻止 kernel 自动对接收到的包创建并维护 TCP 连接(TCP passthrough)。参考资料。
- 上面的图中画出了两个 "route decision" 阶段,但对于 receive / send 而言,其各自实际上应该主要使用了1个 route 阶段: receive 的 route 在 PREROUTING 之后;send 的 route 在 OUTPUT 之后。
- Receive时判断是否是发送给本机的数据包的方法是:收到数据包的destination IP是否与本机某个interface的IP相同。
- Send的OUTPUT chain除了manage和filter以外nat表里也有。nat的OUTPUT位于filter的之前,用于对从本机(app)发出的(而不是收到并FORWARD的)请求做DNAT或REDIRECT。
- Send的route阶段确定了数据包的source ip和source port。source ip和source port由app发送数据包时决定。如果app没有bind某个interface并且没有设置source ip/port,则source ip时为route使用的interface ip。
- filter表的INPUT, OUTPUT和FORWARD用来过滤数据包 -j ACCEPT /DROP
- nat表的PREROUTING / OUTPUT用来做DNAT(或REDIRECT), POSTROUTING用来做SNAT(或MASQUERADE)
- FORWARD (以及所有 FORWARD 之后的 chain) 需要设置内核参数 "net.ipv4.ip_forward=1"。(否则内核网络栈会直接丢弃网卡收到的 dst 非本机的数据包)
- nat 表的 PREROUTING / POSTROUTING 链有些时候会被跳过。主要是指对于有连接的会话 (conn),只有初始发送的数据包会经过 PREROUTING / POSTROUTING,之后发送的数据包以及收到的来自对方的数据包都会根据内核维护的 conn 连接表而被直接处理。
- conntrack (Connection tracking) 工作在 PREROUTING / OUTPUT 链(分别对于收到的/本机发出的数据包)。
bind
socket 可以 bind 某个 src IP。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
注意 bind 某个 interface 并不会使"发送数据包"跳过 route decision 阶段,bind 的作用仅仅是指定发送数据包时的 src IP。
SO_BINDTODEVICE
另一个容易与 "bind" 混淆的是 setsockopt 的 SO_BINDTODEVICE 选项:
#include <sys/socket.h>
setsockopt(sock, SO_BINDTODEVICE, "eth0");
SO_BINDTODEVICE 仅对 AF_INET (即 Internet 数据包)有效。其作用是:
If a socket is bound to an interface, only packets received from that particular interface are processed by the socket.
参考
Source Address Selection
本机发送的数据包源 IP 选择:
http://linux-ip.net/html/routing-saddr-selection.html
The application is already using the socket, in which case, the source address has been chosen. Also, the application can specifically request a particular address (not necessarily a locally hosted IP) using the bind call.
The kernel performs a route lookup and finds an outbound route for the destination. If the route contains the src parameter, the kernel selects this IP address for the outbound packet. Otherwise, the kernel will choose the first address configured on the interface which falls in the same network as the destination address or the nexthop router.