针对进程设置路由规则

最近有一个需求是这样的:Linux 机器上有多个网络的 interface,想要让其中的一个程序使用 eth1 而不是默认的 interface,不影响其他程序。

在 Linux 上发送 TCP 数据,是通过 kernel 提供的 syscall 将数据送给 kernel,然后 kernel 负责最后的发送。而到了 kernel 这一层,TCP 就是根据五元组来确认的了:来源端口,目的端口,来源IP,目的IP,以及协议类型。所以就让“按照进程转发”这个需求变得有些复杂。

Linux 提供了 SO_MARK 可以给流量“打标签”,然后针对打了标签的流量设置 route. 我们的思路是这样的:

  1. 设置一个路由规则,将带了 0x10 mark 的流量通过 table 100 查找路由: ip rule add fwmark 0x10 table 100
  2. table 100 中只有一条 default 路由规则,就是通过 eth1 出去: ip route add default dev eth1 table 100 scope global
  3. 然后给特定程序使用 SO_MARK 打标签。这样,就可以命中我们想要的路由了。

如何给这个流量打标签,变得有趣了起来。

方法1:iptables

如果应用使用的流量有明显的特征,就可以用这种方法。比如我们的流量都是去往 3306 的,就可以针对这个端口设置一条规则:

但是话说回来,既然都知道端口了,可以直接把步骤 3 去掉,在步骤 1 让去往这个端口的流量直接命中 table 100(ip-route(8) 是支持端口的):

所以这个方法感觉比较鸡肋。而且这种只支持程序的流量必须有一个固定的模式,假如程序请求很多其他网站并且 TCP 无法识别,就麻烦了。

方法2:LD_PRELOAD 魔法

使用这种方式,我们可以不修改程序的代码而又让进程的流量全部带上标记。

第一次见到 LD_PRELOAD 是在 jvns 的博客上,简直是一个颠覆世界观的东西,它这么简单,又这么有效!

它的原理是这样:dynamic link 的程序是在启动的时候寻找应该从哪里 load 一个 symbol 的,通过设置 LD_PRELOAD 给进程,就可以覆盖动态链接的符号的查找顺序。

ldd 展示动态链接的库:

所以,当我们编译一个 .so 共享库,并且通过 LD_PRELOAD 设置成共享库的路径,那么在寻找符号的时候就会先寻找 .so 里面的,这样,我们就可以通过编写同名的函数来覆盖系统的函数。

如果你用过 proxychains ,那么你应该已经用过这个 feature 了。

proxychains 是一个代理软件,一般的软件使用代理都需要设置 http_proxy 等环境变量来告诉软件代理的地址。前提是这个软件支持读这些环境变量。但是,有了 Proxychains,你不需要软件自身支持,就可以让它走代理!原理就是,软件通过 proxychains 启动,而启动的时候,proxychains 会设置软件的 LD_PRELOAD 变量,然后去替换系统的 socket 函数,这样,任何调用都会走 proxychains 的代理!source code

所以,使用这种方法,我们只要替换程序的 socket 方法,在每次创建 socket 之后,调用 setsockopt(2) 去设置上 SO_MARK 就好了。

网上已经有一个程序 App-Route-Jail 做了这件事,它的使用方法如下:

1.clone 并且编译程序:

2.启动程序的时候带上 LD_PRELOAD 变量,要设置什么 mark,通过 MARK 环境变量传入

它的程序很简单,就是用一个新的 socket 函数,替换原来的 socket 函数:在每次创建 socket 之后,调用 setsockopt(2) 设置 mark.

等下,那既然 mark.so 里面调用的也是 socket(),怎么避免 linker 再去找一遍这个函数在哪里,然后找到了自己,造成无限循环呢?

mark.c 的代码中,是用 dysym(3) 去 load 的 socket() 这个符号:

RTLD_NEXT 就是让 dlsym 从下一个 .so 开始找:

RTLD_NEXT
Find the next occurrence of the desired symbol in the search order after the current object. This allows one to provide a wrapper around a function in another
shared object, so that, for example, the definition of a function in a preloaded shared object (see LD_PRELOAD in ld.so(8)) can find and invoke the “real” func‐
tion provided in another shared object (or for that matter, the “next” definition of the function in cases where there are multiple layers of preloading).

所以这里用的就是 glibc 提供的 socket 函数了。

这个方法也有缺点,就是它只对 dynamic link 的程序有效,像 golang 这种语言默认是全部静态编译的,连 glibc 都不用,LD_PRELOAD 自然就无效了。这种情况我们就只能修改程序的代码,让它自己标记自己的流量了。

方法3:直接在 golang 代码中直接设置  fwmark

我们需要手动创建 Dialer 来进行 tcp 连接。并且给这个 Dialer 设置一个 Control:

 

感觉使用这种方式在 Chaos Engineering 方面也会很有用,我们一直苦于如何有效对 HTTP(S) 流量进行注入而不影响其他机器的进程。使用 SO_MARK 或许可以解决。


2023年3月1日更新:这篇文章发出之后,网友评论了更多有用和有趣的方法。

方法4:通过 network namespace 配置路由

最先是在 twitter 上看 @Sleepy93216599 介绍的。不过没尝试过,就不详细展开介绍了。

方法5:通过 iptables –uid-owner 匹配

iptables 居然支持直接按照 uid 来匹配,之前一直不知道这个。这样的话只要给进程分配一个单独的 user 来运行就好了。

此外,还支持 gid, pid, sid, cmd owner 来匹配:

–uid-owner userid
Matches if the packet was created by a process with the given effective user id.
–gid-owner groupid
Matches if the packet was created by a process with the given effective group id.
–pid-owner processid
Matches if the packet was created by a process with the given process id.
–sid-owner sessionid
Matches if the packet was created by a process in the given session group.
–cmd-owner name
Matches if the packet was created by a process with the given command name. (this option is present only if iptables was compiled under a kernel supporting this feature)

方法6:使用 ptrace(2) hook 系统调用 connect(2)

这个方法简直惊为天人。

本文评论里介绍了 gg 这个工具,可以用来 hook golang 写的程序,感觉很神奇,但是没看懂怎么实现的,只有 FAQ 里面提过一次 ptrace,然后灵光一闪发现可以用 ptrace 搞事情。

后来看到 graftcp 这个工具,文档和代码写的都很好,看的让人拍大腿。简单来说,就是程序启动的时候,通过 ptrace(2) 进行跟踪,然后当程序开始创建 tcp 的时候,拦截 connect(2) 调用,获取目标地址,然后给这个程序修改成自己的地址,欺骗程序,自己拿到了真正的地址。然后恢复执行。后面程序成功创建了 tcp 连接,但是却是到代理的 tcp 连接,而不是真正到目标地址的链接。这样,代理就在这之间工作了。而这对程序是无感的。

文档还提到一个有趣的点,就是为什么不直接去 hook write(2)read(2)呢? 原因有2:

  • 不方便拿到真正的目标地址(原来的方法 hook connect(2)) 是通过 PIPE 把地址告诉代理进程。
  • 去改写程序的 buffer 内存,会造成缓冲区溢出等错误。(而原来的方法直接修改 connect(2) 的参数是在寄存器,安全的多)。
  • 另外 ptrace 是有性能损耗的,直接hook一次,在连接创建阶段,显然要比每次传输数据都暂停一下,性能要高得多。


针对进程设置路由规则”已经有11条评论

  1. iptables 应该还支持 uid 的匹配,使用 -m owner –uid-owner $uid 匹配流量。可以使用特定用户启动进程,然后 iptables set mark。

  2. ptrace太影响性能了 现在连debugger都不用

    我推荐三种:
    1. 扔到单独的 netns 里,用一对 veth 和外界联通,这样所有流量都一定走 veth,剩下的不管是用 veth 作为 iptable 规则还是 bpf 都很容易。
    2. cgroup/net_cls 标记,比如 echo 0x100001 > /sys/fs/cgroup/net_cls/0/net_cls.classid 然后 echo $pid > /sys/fs/cgroup/net_cls/0/tasks,这样出来的 skb 都有了标记,剩下的你爱 iptables 劫持还是 ip route 都随意。
    3. cgroup bpf,直接把从进程的 syscall 拿到 skb redirect 到你指定的设备上,完全绕过内核栈。

    1最简单,2是古典内核栈,3性能最好。

          • 此外如果真的就想该进程syscall参数,写个小型的kprobe内核模块也很容易,不超过百行,性能也很好,只要想办法匹配进程就行,可以用cgroup啊netns用户组啥的。现在内核模块的接口也没那么生硬,直接在 debugfs 里动态传参。

Leave a comment

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