< 返回博客

proxychains和graftcp的比较


今天看到github发布了github-cli,想去试一下,结果在用cli登录的时候被GFW挡住了。尝试使用proxychains发现这个cli是用go写的,用不了。所以决定去网上找找其他能做透明代理的工具,于是就找到了一个国人写的graftcp。因为平时用类似的工具较多,所以简单再次对比一下。

原理及差异

二者的基本思路大同小异,都是通过截获子进程的socket相关的系统调用,偷偷地与代理建立连接,最后在connect调用中返回代理的socket。

但是在具体实现上,二者用了不同的方案。

proxychains

proxychains使用了LD_PRELOAD环境变量。系统调用都会经过glibc的包装,所以在c中写个connect实际上调用的是glibc的库函数。而LD_PRELOAD环境变量向用户提供了覆盖库函数的能力,proxychains正是利用了这一点。

在其源码中可以看到,它一共截获了6个函数:

connect_t true_connect;
gethostbyname_t true_gethostbyname;
getaddrinfo_t true_getaddrinfo;
freeaddrinfo_t true_freeaddrinfo;
getnameinfo_t true_getnameinfo;
gethostbyaddr_t true_gethostbyaddr;

其中,截获connect函数是为了与代理服务器进行连接,也就是执行主要的功能;而其他的几个函数是为了使用socks5的远程DNS功能,能够解决DNS污染的问题(这个思路启发了我,解决了我的一个业余项目里面关于DNS污染的问题,之后有时间了我会写一下。感谢)。

简单说下之后的过程(不考虑远程DNS)。在截获connect之后,proxychains会拿到其参数中的目标ip;然后与代理服务器建立连接,并请求其代理该ip地址;成功之后,便将受到代理的socket描述符返回给应用。至此,透明代理建立完成。

graftcp

graftcp的思路则是通过ptrace调用来截获子进程的connect连接,这是二者最大的不同,也直接影响到二者的适用情况。另外有一点不同的是,graftcp并非自己处理与代理服务器的连接,而是将此任务交给了一个叫做graftcp-local的进程去做,目前我尚不清楚为何要分开。

graftcp的README里面有个简单的架构图:

+---------------+             +---------+         +--------+         +------+
|   graftcp     |  dest host  |         |         |        |         |      |
|   (tracer)    +---PIPE----->|         |         |        |         |      |
|      ^        |  info       |         |         |        |         |      |
|      | ptrace |             |         |         |        |         |      |
|      v        |             |         |         |        |         |      |
|  +---------+  |             |         |         |        |         |      |
|  |         |  |  connect    |         | connect |        | connect |      |
|  |         +--------------->| graftcp +-------->| SOCKS5 +-------->| dest |
|  |         |  |             | -local  |         |  or    |         | host |
|  |  app    |  |  req        |         |  req    | HTTP   |  req    |      |
|  |(tracee) +--------------->|         +-------->| proxy  +-------->|      |
|  |         |  |             |         |         |        |         |      |
|  |         |  |  resp       |         |  resp   |        |  resp   |      |
|  |         |<---------------+         |<--------+        |<--------+      |
|  +---------+  |             |         |         |        |         |      |
+---------------+             +---------+         +--------+         +------+

优缺点

相比之下,proxychains最大的优点就是支持远程DNS了,但由于LD_PRELOAD的一些限制,导致了它:

  • 不能以root权限用户使用
  • 不能给静态编译的程序使用(比如所有的go程序)
  • 不能在不使用库函数进行DNS查询的程序中使用远程DNS(比如java)

相反的,graft并不限制用户和程序,适用性更广泛一点,虽然无法解决DNS污染的问题,但是这个问题可以通过更换DNS解决。


2021.3.30 更新

最近搞明白了graftcp为什么要多出来一个graftcp-local进程了。

与proxychains很大的一点不同在于,graftcp与被代理的程序并非在同一个进程中运行,而是父子进程的关系,父子进程并不共享文件描述符表,这就导致graft不能向proxychains一样“自己”去连接代理服务器并将文件描述符直接由connect返回了,因为graft创建的只能是自己进程内的描述符,不能直接交给子进程使用。(这可以理解为:ptrace原语相比于LD_LIBRARY原语失去了“有效修改connect返回的文件描述符”的能力,因此无法直接使用之前的方法。)

针对这个问题,graft的做法是建立一个单独的服务器graftcp-local,相当于“代理服务器的代理”,并将子进程发起的connect的目的地址改为该服务器的地址,让子进程发起向graftcp-local的连接。

但是这又引出了另一个问题:从graftcp-local的角度来看,它接到一个新的连接之后,并不知道这个连接实际上是要连接哪里的,因为connect请求并没有携带原目的地址信息。

这时候又需要graftcp进程了,在graftcp使用ptrace追踪到原connect请求之后,他会将原目的地址以及子进程pid发送给graftcp-local,这样graftcp-local就有了一个从pid到目的地址的映射表,在它得到新连接之后,可以通过查询/proc/net/tcp文件取得新连接所在的进程pid,从而得知它的真实目的地址。至此,graftcp-local就可以建立完整的代理连接了。

另外,我也想到了一种使用ptrace、但不需要graftcp-local这样的双重代理的方法。其原理在于socket的控制消息接口:sendmsgrecvmsg,这个接口可以将进程的文件描述符通过socket发送给另一个进程。ptrace配合上这个接口,就拥有了“有效修改connect返回的文件描述符”的能力,因此应该可以像proxychains那样实现透明代理了。

基本的思路大概为:ptrace截获socket()调用,在enter-stop的时候,由父进程调用socket()创建套接字描述符,然后通过sendmsg()发送给子进程,再将当前的socket()调用修改为recvmsg()调用(这一步是因为消息队列长度有限,避免子进程不进行recvmsg()造成消息阻塞),继续子程序;等到exit-stop的时候,新的文件描述符已经在子进程中可用了,通过查看子进程recvmsg()收到的内容,得到描述符的值,并将其设置为该系统调用的返回值。至此,就创建了一个父子进程共用的套接字描述符,等到子进程调用了connect时,父进程就可以通过这个套接字来初始化代理连接。(理论上,所有的创建文件描述符的系统调用都可以通过这种方法来实现父子进程共享,而不需要在exec()之前用dup()准备好。)

思路有了,具体的实现正在写。最近好多事情,毕设、驾照、重修、考试,五月份还要去实习,可是一件都不想做。