Netfilter

Netfilter

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。

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

  • 上面的图中画出了两个 "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

  1. 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.

  2. 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.


Last update: 2021-04-13 03:21:46 UTC