Ovear's Blog

I'am Ovear,Ovear is me.

Theme Refrain made by Eiko

Proudly powered by WordPress

iptables-extensions中socket模块是个啥?

今天看到这个叫做socket的iptables 模块挺有意思的,但是没有找到太多的资料;想了想翻译一下文档,应该可以作为理解一下这个模块的作用的开始。

(然而发现并不能,所以又研究了一大堆相关的其他东西,就有了这一篇)

文档

   socket
       如果能通过分组执行socket搜索后,能够找到一个打开的TCP/UDP
       socket,则会进行匹配。具体来说,这会匹配一条已建立(establshed)
       的链接,或者非0绑定监听器(不是监听0.0.0.0或者::/非INADDR_ANY)
       socket(比如说有可能是非本地地址)。搜索是通过TCP/UDP分组的元
       组进行的,对于ICMP/ICMPv6错误分组来说,则是使用内嵌在内的原始
       TCP/UDP头部中的元组。

       --transparent
              忽略非透明(non-transparent) sockets.

       --nowildcard
              不要忽略绑定在'任意'地址的sockets。默认情况下,不会
              匹配0绑定监听器;因为在那种情况下,本地的服务可以
              拦截本来会被转发的流量。因此,在匹配使用策略路由重
              定向到本地的转发流量时,本选项有安全影响。当使用
              socket 匹配器来实现绑定在非本地地址的完全透明代理
              时,更推荐使用 --transparent 选项。

       例如(假如被标记为1的分组会被发送到本地):

              -t mangle -A PREROUTING -m socket --transparent -j MARK
              --set-mark 1

       --restore-skmark
              将分组标记设置为匹配到的socket的标记。可以和 --transparent
              以及 --nowildcard 选项组合使用来限制恢复分组标记时匹配的
              sockets。

       例如:一个打开了2个透明(IP_TRANSPARENT)的sockets,并
       使用SO_MARK选项设置了一个标记。我们可以这样过滤匹配的分组:

              -t mangle -I PREROUTING -m socket --transparent --restore-
              skmark -j action

              -t mangle -A action -m mark --mark 10 -j action2

              -t mangle -A action -m mark --mark 11 -j action3

源代码中的注释

/* 基于 "socket" 匹配的重定向 (无特定规则)
 * ===================================================
 *
 * 有一些链接使用了动态对端/端口(如FTP数据链接),
 * 这种链接用户无法显式为其创建规则。
 * 这些情况都可以通过通用的“socket”规则来处理。
 * 假设信任的代理应用不需要显示的规则就可以建立链接
 * (当然除了通用的"socket"规则除外)。在这种情况下
 * 以下socket按照下列优先顺序匹配:
 *
 *   - 匹配: 如果使用__分组__元组匹配到了一条完全
 *     建立的链接(established)
 *
 *   - 匹配:如果存在非0绑定(不是监听0.0.0.0或者::/非
 *     INADDR_ANY)的监听器(比如说有可能是非本
 *     地地址)。我们不允许0绑定监听器是因为本地服
 *     务可能会拦截通过机器的流量。
 */

PowerDNS的说明

你在到处看到的-m socket

网上很多TPROXY的iptables示例中都包含一条使用了-m socket -p tcp也没有解释的改进。这个iptables的socket模块补丁能匹配到对应的本地socket,这可能比执行到一组特定的规则更加精确或快速。

下面这个到处都是的配置设定了一个接受并标记分组的redirect chain:

iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT

接下来这些规则保证了已建立的本地socket对应的所有内容都被发送到那;然后就是新分组需要进行的操作:

iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
iptables -t mangle -A PREROUTING -p tcp --dport 25 -j TPROXY \
  --tproxy-mark 0x1/0x1 --on-port 10025 --on-ip 127.0.0.1
iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY \
  --tproxy-mark 0x1/0x1 --on-port 10080 --on-ip 127.0.0.1

TProxy-exmaple中的说明

唯一需要的步骤就是策略路由和TPROXY iptables-rule. DIVERT规则是为了防止tproxy目标做不必要的操作而做的优化(-m socks检查socket是否符合一个网络分组头部)

到底socket 模块是什么

我看了这么多注释,其实对socket模块也没有一个清晰地了解,很多使用到的也没有说的太细,看的云里雾里的。

但是普遍接受的一个观点是新建 DIVERT 规则,避免已有TCP连接的包二次通过 TPROXY,理论上有一定的性能提升;仔细想了想可能有这么个道理。

所以要说清楚socket模块到底是什么,我们有必要稍微说明下TPROXY模块是什么

TPROXY模块系咩来尬?

它做了什么?

TPROXY的作用主要是将链接在想要建立的socket(skb)上执行预操作,使内核在协议栈层面上对这些分组/socket重定向到本地做准备;其中很重要的一点就是,重定向到某个端口,以及重定向到某个IP监听的socket。而在这个过程中,整个分组(packet)本身并未被修改,只是socket本身被系统悄悄咪咪地替换了,所以我们还是能够获取到这个分组原始的目的地(通过分析分组头部)。

为啥要策略路由?

那有的同学会问了,既然这样要给分组打标记策略路由的目的是什么?

原因其实就是上面说明的内容,虽然系统把socket偷梁换柱了,但是分组本身没有发生变化,它还会在系统中走完剩下的正常流程(比如说路由决策)。这时我们就需要将这个包引导到本地,不然就会给转发出去了。因为我们做的骚操作其实是在L4,而路由决策等操作是在L3,所以我们得一并把L3的事情给做了(路由),所以就需要策略路由来把我们悄悄咪咪做的事做完全套。

附赠的收获:TPROXY不支持转发到远程

从上述分析我们可以得出一个额外的结论:TPROXY并不支持重定向到远程机器;因为我们在L4层做的所有操作都是本地Only级别的(分组本身没有改变);一旦分组被转发出去,就GG了;远程的机器根本不知道我们暗地里做了这些事。

正如iptables-extensions所说:

It redirects the packet to a local socket without changing the packet header in any way.

他会重定向分组到一个本地socket,而不会以任何方式修改分组头部本身。
内核中的阻碍

好不容易,我们终于让本地的socket有希望(其实还收不到)收到了来自TPROXY的分组

因为分组头部未改变,我们的程序很容易的可以获取分组中原来目的的地址和端口;但是此时如果想要建立链接,我们的程序要在socket中绑定不属于自己的地址(非本地地址),才能让链接能够建立。

即程序要假装自己是目标地址,返回数据包给还蒙在鼓里的客户端。但是这种行为默认是在系统中禁止的;得过了系统的这一关,这种浑水摸鱼的操作才能成功。

所以我们就有了SOL_IP, IP_TRANSPARENT这个socket监听选(后)项(门)来说服系统让我们这么做(毕竟这种高风险操作,那不能是谁都可以做的呀)。

有了这个选项以后,我们就可以假装是目标地址,使用它的信息来在客户端不知情的情况下成功建立链接。

所以socket模块有什么关系?

说了这么多,我们把TPROXY的基本操作说完了,但是似乎这些和socket模块有什么关系呢?

其实和TPROXY的模块就在刚才说的替换socket这一步。我们要怎么确定要替换的socket究竟是哪个呢?

这时候我们再回来看一下上面关于socket模块的匹配描述。

 *   - 匹配: 如果使用__分组__元组匹配到了一条完全
 *     建立的链接(established)
 *
 *   - 匹配:如果存在非0绑定(不是监听0.0.0.0或者::/非
 *     INADDR_ANY)的监听器(比如说有可能是非本
 *     地地址)。我们不允许0绑定监听器是因为本地服
 *     务可能会拦截通过机器的流量。

这时我们就会发现,这个匹配行为似乎和TPROXY非常相似;那么其实socket模块是不是就是为了简化连接建立后TPROXY复杂的行为,从而提升性能的优化呢?

我们来看看TPROXY的源代码中有没有蛛丝马迹。

来自内核开发组的一封信
// https://elixir.bootlin.com/linux/v6.1.11/source/include/net/netfilter/nf_tproxy.h#L75
/*
 * 这是用于用户想要显式通过iptables拦截符合规则的链接。在这种情况
 * 下,我们假设sockets按照以下顺序匹配:
 *
 *   - 匹配: 如果使用__分组__元组匹配到了一条完全建立的链接,
 *     就直接返回它;我们假设重定向已经完成了,因为我们正在
 *     处理的是一条已经建立的链接。
 *
 *   - 匹配:如果有与重定向目的地(就是链接的on-porth和on-ip)
 *     匹配的监听socket,不管socket是监听在0.0.0.0还是一个具体
 *     的地址,都直接返回它。我们这么做的理由是因为已经存在
 *     显式的规则了,所以监听器不管是不是监听在某个网卡上,
 *     还是0地址都不重要。用户已经声明了他想要重定向(因为
 *     他新增了这条规则)。
 *
 * 请注意,TPROXY目标和socket模块的匹配之间存在重叠。通常情况
 * 下,如果这两条规则你都创建了,“socket”模块会是第一条规则,实际
 * 上所有已建立的链接都会通过这条规则。
 */

还有另一段小注释

// https://elixir.bootlin.com/linux/v6.1.11/source/net/ipv4/netfilter/nf_tproxy_ipv4.c#L103
/* 提示: 就算是监听在0.0.0.0的监听器,我们
 * 也会返回。那种情况在xt_socket中被过滤
 * 了,因为xt_TPROXY也需要0绑定监听器。
*/
尘埃落定

到这里,关于socket模块作用的这个问题,就呼之欲出了。

但是我们还是不要飞的这么快,再捋一下这个逻辑:

TPROXYsocket模块的匹配规则有重叠之处;链接建立后(established)不管是TPROXY还是socket模块,都是直接匹配的这个socket

同时链接建立后,系统中该分组对应的socket也不会再发生改变了(不然链接就错乱啦);而TPROXY内部的逻辑远比socket复杂,那么内核组的开发者们自然就想利用这个来优化下性能了(当然这里指的是TCP,UDP的TPROXY内核实现有些许不同,但是大体的思路是一致的)。

我们再回想下TPROXY之前的操作,L4偷换socket,L3标记分组修改路由;那现在socket已经换过,差的就是给分组打上对应标记从而使得策略路由生效了。

再回过头来看我们的DIVERT CHAIN做的不正好就是这一件事嘛?而socks模块的逻辑比TPROXY简单许多,这就存在了性能差异。

结论:socks模块在这里的作用,确实是在尝试优化TPROXY,以获取更高的性能

尾声

折腾了这么久,终于找到了这个问题的答案了。

期间在网上漫无目的的搜索,也没有找到太多有用的信息,最后还是通过源代码阅读之术和运气的加持之下,才磕磕碰碰的勉强找到了答案。

其实之前也尝试过很多次看Linux内核的源码,但是Linux内核的设计实在是太高端了,没能够看懂太多东西。

但是这个Netfilter/iptables之旅,恰好给了我这么个机会。这一路上见识到了TProxy的奇妙想法、设计;简洁的实现(核心思想真的非常简单,没想到还可以有这种骚操作),充满魅力的Linux代码(特别精妙);非常强大的Netfilter/iptables。(小声:可惜iptables还没搞明白,nftables它又来了;你不要过来啊啊啊啊啊啊(:з」∠)

第一次阅读Linux内核代码(挖坑:以后看看有没有机会能不能把源码分析过程也整理出来),可能有一些错误,欢迎大家指正。

小尾巴

最后的最后,其实我还是好奇上面的DIVERT规则是否能优化一下;看起来socks模块现在似乎新增了一些选项,现在似乎都没有利用上。之后测试一下来更新一下结果看看哈哈哈哈哈~

// 选项1,只用 --transparent

iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT

iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j DIVERT

// 选项2,结合 --transparent 和 --restore-skmark

iptables -t mangle -A PREROUTING -p tcp -m socket --transparent --restore-skmark -j ACCEPT

选项1使用了--transparent选项,只应用本地socket透明代理的部分,理论上应该会降低其他分组被意外打上标记的问题。

选项2则在选项1的基础上更进一步,使用了--restore-skmark的新特性,将多条语句合并为一句;根据提交记录[10]介绍,应该是能达到和最初优化语句一样的目标的,同时还增加了更多可能性。

小尾巴2——来自Nftables的说明

时隔文章发布一年半以后,今天看到tproxy的Kernel文档中包含了使用Nftbales的示例,而Nftables的文档更清晰的说明了socket模块的作用,所以特意加上来以帮助大家理解。

你也可以通过以下nft命令来实现

# nft add table filter
# nft add chain filter divert "{ type filter hook prerouting priority -150; }"
# nft add rule filter divert meta l4proto tcp socket transparent 1 meta mark set 1 accept

Nftables的文档[11]

SOCKET 语法
    socket {transparent | mark | wildcard}
    socket cgroupv2 level NUM

Socket语法可以用来搜索一个已经存在的打开的TCP/UDP socket以及其可与分组关联的属性。它寻找的是已经建立的或者非0绑定的监听socket(可能使用非本地地址)。你也可以通过此语法来匹配给定祖先等级/ancestor level的cgroupv2 socket。比如如果一个socket属于cgroupv2 a/b,祖先等级1会检查并匹配cgroup a,而祖先等级2则会检查并匹配cgroup b.

Table 32. 可使用的socket属性

属性名称 描述 类型
transparent 匹配到的socket中的IP_TRANSPARENT选项的值。可以为0或者1。 boolean (1 bit)
mark socket mark的值 (SOL_SOCKET, SO_MARK)。 mark
wildcard 表明该socket是否为通配符绑定 (e.g. 0.0.0.0 or ::0)。 boolean (1 bit)
cgroupv2 此socket的cgroup version 2 (/sys/fs/cgroup的路径) cgroupv2
# 标记与透明socket相关的分组。“socket wildcard 0” 表示**不**匹配
# 0绑定监听socket(这通常正是您想要的)。
table inet x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        socket transparent 1 socket wildcard 0 mark set 0x00000001 accept
    }
}

# 追踪mark为15的socket对应的分组
table inet x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        socket mark 0x0000000f nftrace set 1
    }
}

# 将分组mark设置为socket mark
table inet x {
    chain y {
        type filter hook prerouting priority mangle; policy accept;
        tcp dport 8080 mark set socket mark
    }
}

# cgroupv2第一级为"user.slice"的计数器
table inet x {
    chain y {
        type filter hook input priority filter; policy accept;
        socket cgroupv2 level 1 "user.slice" counter
    }
}

参考文章

[1] https://man7.org/linux/man-pages/man8/iptables-extensions.8.html
[2] https://ipset.netfilter.org/iptables-extensions.man.html
[3] https://patchwork.ozlabs.org/project/netdev/patch/20081001142431.4893.56954.stgit@este/
[4] https://elixir.bootlin.com/linux/latest/source/net/netfilter/xt_socket.c
[5] https://github.com/kristrev/tproxy-example
[6] http://wiki.squid-cache.org/Features/Tproxy4
[7] https://powerdns.org/tproxydoc/tproxy.md.html
[8] https://www.kernel.org/doc/html/latest/networking/tproxy.html
[9] https://elixir.bootlin.com/linux/v6.1.11/source/include/net/netfilter/nf_tproxy.h#L75
[10] https://patchwork.ozlabs.org/project/netfilter-devel/patch/1434415279-15517-1-git-send-email-harouth@codeaurora.org/
[11] https://www.netfilter.org/projects/nftables/manpage.html

iptables-extensions中socket模块是个啥?有 2 条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

© 2024 Ovear's Blog All rights reserved.