SOCKS5和Dante介绍
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.
SOCKS5和Dante介绍
发表于
2022-03-02 分类于 Network
SOCKS
是一个比较简单的通用代理协议,用于在客户端与服务器之间代理网络数据包。最新的版本是5
, 所以一般叫做SOCKS5
,协议规范是RFC1928。但SOCKS5
并不兼容之前的SOCKS4
和SOCKS4A
。SOCKS5
在SOCKS4
的基础上添加了UDP
转发功能和校验
机制。
SOCKS5
的工作过程简单可以归纳为协商、请求、和转发三个阶段。以TCP
代理场景来看, 一般流程是:
- 客户端建立TCP连接
- 客户端发送客户端侧支持的校验方法
- 服务端回应选择的校验方法
- 客户端和服务端之间按选择的校验方法完成校验
- 客户端发送所需的请求给服务端。
SOCKS5
支持不同的请求类型,包括CONNECT
,BIND
,UDPASSOCIATE
等。 - 服务端接收到请求,从中解析出目的地址,建立到目的地址的连接。
- 发送成功信息给客户端。
- 客户端开始发送应用层信息。
- 服务端在客户端和目的地址之间转发应用层信息。
其中,2-4完成协商阶段,5-7完成请求阶段,之后进入转发阶段。
当前常用的校验方法是USERNAME/PASSWORD
(RFC1929)和GSS-API
(RFC1961),具体的校验过程是由不同校验方法来自定义的。
使用USERNAME/PASSWORD
校验方法的TCP
代理时序图如下:
Dante是个较为成熟的SOCKS
服务器开源实现,一些高级模块的代码没有开源,需要购买商业支持。但Dante
的整体代码逻辑比较清晰,还是比较容易扩展的。
Dante
的进程结构比较有意思,以一种类似流水线的方式在多个进程间传递并处理请求, 而且会根据请求量对相应进程数量进行调节,相应进程的处理量会通过修改进程名称来体现。
请求传递具体实现上依赖通过UNIX Socket
在进程间传递文件描述符
,机制可以参考这篇文章:
Dante
包括5
种不同角色的进程:mother
: 初始启动的主进程,其他的进程都由mother
进程创建。但在fork
其他进程前,mother
会建立两对UNIX Socket
通道,一个用于在父子进程之间发送数据和文件描述符,另一个用于监控进程的存活状态。创建完其他进程后,mother
开始循环处理客户端的TCP连接的accept
。monitor
: 基于超时时间处理其他进程的共享内存数据。negotiate
: 处理SOCKS
协议的协商。request
: 建立到远端服务器的请求。io
: 在客户端和远端服器之间转发数据。
整体的进程架构图如下:
上述的基于TCP
的代理流程在sockd
中的处理流程是:
- 客户端向
sockd
发起TCP
连接。 sockd
的mother
进程accept
连接,然后选择一个negotiate
进程,将相关数据结构及客户端连接fd
发送给它。negotiate
进程接收到fd
和数据结构,从客户端fd
读取协商请求和方法。从中选定一种方法,发送给客户端。- 客户端根据
sockd
返回的校验方法发送校验请求。 negotiate
进程读取校验请求并校验通过后,发送成功状态给客户端。- 客户端发送代理请求。
negotiate
进程读取代理请求并构建相应的数据结构,将这个数据结构和客户端fd
再发送回mother
进程。mother
进程收到这些信息后再选择一个request
进程,并将这些信息转发给该进程。request
进程根据收到的请求信息建立到远端服务器的TCP连接,并根据客户端和远端服务器两端信息构建相关数据结构,和两个fd
一起发送回mother
进程。mother
进程将收到的数据和两个fd
转发给选定的一个io
进程。io
进程接收到这些信息后,在两个fd
之间进行数据转发。
整体流程如图:
每个角色进程的逻辑主体都是基于select
实现的事件驱动模型。众所周知,select
有最大支持1024
个文件描述符的限制。而在一些操作系统具体实现上,实际并不会真的去检查该限制。Dante
的实现就利用这一点来突破1024
的限制。这篇文章介绍了这种方法:
https://blog.csdn.net/dog250/article/details/105896693
但实际上这里还是存在一个BUG。Dante
默认的编译情况下,会定义_FORTIFY_SOURCE
, 这会导致FD_SET
中会去检查1024
的限制。当代理请求较多时,子进程越来越多,mother
的fd
数量就会超过1024
而导致进程崩溃。
可以通过不再定义这个宏来修复问题,也可以通过另外的方法来绕过这个问题。
Dante
支持创建多个mother
进程,每个mother
都会再创建自己的一系列negotiate
,request
, io
等进程,但monitor
进程只是最初的mother
(叫做main mother
)会创建。因为main mother
是在开始listen
之后再fork
其他进程,因而多个mother
进程都监听在同一端口上,它们会调用accept
来抢夺客户端连接。抢夺到连接的mother
会在自己的一系列子进程中进行处理。这样我们可以限制每个mother
的文件描述符小于1024
,而启动若干个mother
。mother
的数量可以通过命令行参数-N
来指定。这种情况的进程架构如图:
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
, 使用其他结构替换数组等等。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK