惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Announcements
Recent Announcements
V
Visual Studio Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
云风的 BLOG
云风的 BLOG
Microsoft Security Blog
Microsoft Security Blog
博客园 - 司徒正美
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
小众软件
小众软件
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
aimingoo的专栏
aimingoo的专栏
MyScale Blog
MyScale Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC
A
About on SuperTechFans
宝玉的分享
宝玉的分享
WordPress大学
WordPress大学
B
Blog RSS Feed
G
Google Developers Blog
量子位
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 三生石上(FineUI控件)

卡瓦邦噶!

服务器高性能网络调优 | 卡瓦邦噶! 为何写作 | 卡瓦邦噶! 读《金阁寺》 | 卡瓦邦噶! 雨季又来 | 卡瓦邦噶! MTU Probe 引起的初始延迟 | 卡瓦邦噶! 3.5 秒的固定延迟问题 | 卡瓦邦噶! 学习网络的一点经验 | 卡瓦邦噶! ARP 问题诊断 | 卡瓦邦噶! 网络断断续续…… | 卡瓦邦噶! Piccolo P2P 镜像分发 | 卡瓦邦噶! 一起看电影 | 卡瓦邦噶! 《征服C指针》 | 卡瓦邦噶! 我的姥姥 | 卡瓦邦噶! Python的哲学 Python 3.5的新特性 学校不教的计算机课 垃圾回收(GC)的三种基本方式 在编程中体验纯粹的快乐 从《美丽新世界》谈自由 在快钱实习 迷人的嗓音和迷人的故事——《Sleepyhead》 Python 的十个自然语言处理工具 记一个愚蠢的bug 一年炉石传说的游戏体验 《以撒的结合:重生》网页版图鉴 分清 C++的指针、引用和数组 笑话三则 自由比皇帝更伟大——《悲惨世界》笔记 Git 10 周年访谈:Linus 讲述背后故事 用 0x3f3f3f3f 设定最大int值的优点
针对进程设置路由规则
laixintao · 2023-02-25 · via 卡瓦邦噶!

最近有一个需求是这样的: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 的,就可以针对这个端口设置一条规则:

1

iptables -A PREROUTING -t mangle -i bond0 -p tcp --dport 3306 -j MARK --set-mark 0x01

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

1

ip rule add pproto tcp dport 3306 lookup 100

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

方法2:LD_PRELOAD 魔法

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

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

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

ldd 展示动态链接的库:

Shell

1

2

3

4

5

6

7

$ ldd /usr/bin/nc

        linux-vdso.so.1 (0x00007ffd118ab000)

        libbsd.so.0 => /lib/x86_64-linux-gnu/libbsd.so.0 (0x00007f5a3fb99000)

        libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f5a3fb85000)

        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5a3f95d000)

        libmd.so.0 => /lib/x86_64-linux-gnu/libmd.so.0 (0x00007f5a3f950000)

        /lib64/ld-linux-x86-64.so.2 (0x00007f5a3fc45000)

所以,当我们编译一个 .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 并且编译程序:

Shell

1

2

3

4

git clone https://github.com/Intika-Linux-Network/App-Route-Jail.git

cd App-Route-Jail

chown 755 make.sh

./make.sh

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

1

MARK=10 LD_PRELOAD=./mark.so firefox

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

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

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

C

1

2

if (!_socket)

_socket = (socket_t) dlsym(RTLD_NEXT, "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:

Go

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

// setSocketMark sets packet marking on the given socket.

func setSocketMark(fd, mark int) error {

       if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_MARK, mark); err != nil {

               return os.NewSyscallError("failed to set mark", err)

       }

       return nil

}

// use this create a new dialer

dialer := &net.Dialer{

        Control: func(_, _ string, c syscall.RawConn) error {

                return c.Control(func(fd uintptr) {

                        ex := setSocketMark(int(fd), 0x01)

                        if ex != nil {

                                fmt.Printf("net dialer set mark error: %s\n", ex)

                        }

                })

        },

}

感觉使用这种方式在 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一次,在连接创建阶段,显然要比每次传输数据都暂停一下,性能要高得多。