7

SOCKS5和Dante介绍

 2 years ago
source link: http://just4coding.com/2022/03/02/socks-dante/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

SOCKS5和Dante介绍

发表于

2022-03-02 分类于 Network

SOCKS是一个比较简单的通用代理协议,用于在客户端与服务器之间代理网络数据包。最新的版本是5, 所以一般叫做SOCKS5,协议规范是RFC1928。但SOCKS5并不兼容之前的SOCKS4SOCKS4ASOCKS5SOCKS4的基础上添加了UDP转发功能和校验机制。

SOCKS5的工作过程简单可以归纳为协商、请求、和转发三个阶段。以TCP代理场景来看, 一般流程是:

  1. 客户端建立TCP连接
  2. 客户端发送客户端侧支持的校验方法
  3. 服务端回应选择的校验方法
  4. 客户端和服务端之间按选择的校验方法完成校验
  5. 客户端发送所需的请求给服务端。SOCKS5支持不同的请求类型,包括CONNECT, BIND, UDPASSOCIATE等。
  6. 服务端接收到请求,从中解析出目的地址,建立到目的地址的连接。
  7. 发送成功信息给客户端。
  8. 客户端开始发送应用层信息。
  9. 服务端在客户端和目的地址之间转发应用层信息。

其中,2-4完成协商阶段,5-7完成请求阶段,之后进入转发阶段。

当前常用的校验方法是USERNAME/PASSWORD(RFC1929)和GSS-API(RFC1961),具体的校验过程是由不同校验方法来自定义的。

使用USERNAME/PASSWORD校验方法的TCP代理时序图如下:

1.png

Dante是个较为成熟的SOCKS服务器开源实现,一些高级模块的代码没有开源,需要购买商业支持。但Dante的整体代码逻辑比较清晰,还是比较容易扩展的。

Dante的进程结构比较有意思,以一种类似流水线的方式在多个进程间传递并处理请求, 而且会根据请求量对相应进程数量进行调节,相应进程的处理量会通过修改进程名称来体现。

请求传递具体实现上依赖通过UNIX Socket在进程间传递文件描述符,机制可以参考这篇文章:

Dante包括5种不同角色的进程:
mother: 初始启动的主进程,其他的进程都由mother进程创建。但在fork其他进程前,mother会建立两对UNIX Socket通道,一个用于在父子进程之间发送数据和文件描述符,另一个用于监控进程的存活状态。创建完其他进程后,mother开始循环处理客户端的TCP连接的accept
monitor: 基于超时时间处理其他进程的共享内存数据。
negotiate: 处理SOCKS协议的协商。
request: 建立到远端服务器的请求。
io: 在客户端和远端服器之间转发数据。

整体的进程架构图如下:

2.png

上述的基于TCP的代理流程在sockd中的处理流程是:

  • 客户端向sockd发起TCP连接。
  • sockdmother进程accept连接,然后选择一个negotiate进程,将相关数据结构及客户端连接fd发送给它。
  • negotiate进程接收到fd和数据结构,从客户端fd读取协商请求和方法。从中选定一种方法,发送给客户端。
  • 客户端根据sockd返回的校验方法发送校验请求。
  • negotiate进程读取校验请求并校验通过后,发送成功状态给客户端。
  • 客户端发送代理请求。
  • negotiate进程读取代理请求并构建相应的数据结构,将这个数据结构和客户端fd再发送回mother进程。
  • mother进程收到这些信息后再选择一个request进程,并将这些信息转发给该进程。
  • request进程根据收到的请求信息建立到远端服务器的TCP连接,并根据客户端和远端服务器两端信息构建相关数据结构,和两个fd一起发送回mother进程。
  • mother进程将收到的数据和两个fd转发给选定的一个io进程。
  • io进程接收到这些信息后,在两个fd之间进行数据转发。

整体流程如图:

3.png

每个角色进程的逻辑主体都是基于select实现的事件驱动模型。众所周知,select有最大支持1024个文件描述符的限制。而在一些操作系统具体实现上,实际并不会真的去检查该限制。Dante的实现就利用这一点来突破1024的限制。这篇文章介绍了这种方法:
https://blog.csdn.net/dog250/article/details/105896693

但实际上这里还是存在一个BUG。Dante默认的编译情况下,会定义_FORTIFY_SOURCE, 这会导致FD_SET中会去检查1024的限制。当代理请求较多时,子进程越来越多,motherfd数量就会超过1024而导致进程崩溃。

可以通过不再定义这个宏来修复问题,也可以通过另外的方法来绕过这个问题。

Dante支持创建多个mother进程,每个mother都会再创建自己的一系列negotiate,request, io等进程,但monitor进程只是最初的mother(叫做main mother)会创建。因为main mother是在开始listen之后再fork其他进程,因而多个mother进程都监听在同一端口上,它们会调用accept来抢夺客户端连接。抢夺到连接的mother会在自己的一系列子进程中进行处理。这样我们可以限制每个mother的文件描述符小于1024,而启动若干个mothermother的数量可以通过命令行参数-N来指定。这种情况的进程架构如图:

4.png

Dante里很多实现细节不太考虑性能, 大量使用数组和遍历。举例来说,negotiate进程每次循环会调用neg_gettimedout函数获取一个超时的协商请求:

while (1 /* CONSTCOND */) {
...


#if HAVE_NEGOTIATE_PHASE
gettimeofday_monotonic(&tnow);
while ((neg = neg_gettimedout(&tnow)) != NULL) {

}
}

neg_gettimedout的实现里依然是一个遍历:

static sockd_negotiate_t *
neg_gettimedout(const struct timeval *tnow)
{
size_t i;

for (i = 0; i < negc; ++i) {
if (!negv[i].allocated
|| CRULE_OR_HRULE(&negv[i])->timeout.negotiate == 0)
continue;

if (socks_difftime(tnow->tv_sec, negv[i].state.time.negotiatestart.tv_sec)
>= CRULE_OR_HRULE(&negv[i])->timeout.negotiate)
return &negv[i];
}

return NULL;
}

当协商请求很多时,negv这个数组基本被分配完,如果有一个请求超时,neg_gettimedout返回超时请求,下一次依然从数组第一个元素开始遍历。

如果对于SOCKS服务器性能有比较强的需求,Dante还是有很多优化空间的。比如使用epoll替换select, 使用其他结构替换数组等等。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK