shadowsocks客户端源码分析
前言
学软件那么久最喜欢的一个开源项目就是shadowsocks了,虽然这个项目相对于其他的大型开源项目小得多,但是这个项目有中国特色社会主义情节在内,然后又有很多大神一起打造了shadowsocks项目圈,包括go语言的、路由器的、Linux客户端、Windows客户端、Android客户端、Iphone客户端等,甚至出现了shadowsocksr变种以及shadowsocksrr,这两种都是从shadowsocks
衍生而来的产物,当时众多默默无闻的大神一起为项目作出贡献。
shadowsocks
使用的是Apache License
,开源免费,国内众多开发者在国外服务搭建自己的shadowsocks
,更有人商用shadowsocks
,给国内小白用户搭建代理。这更加增加了网络的透明度,在开发者看来,可以通过shadowsocks
更加方便的访问谷歌,获取世界各地碰到的bug
信息。多年以后,可能这样自由的软件还会被继续ban掉,但是人们心中崇尚自由的精神永远不会消失。
原理
Shadowsocks
项目主要分成sslocal
和ssserver
,这是一个客户端和一个服务端,客户端放在本地,服务端架设在服务器中。
sslocal介绍
sslocal
分成两个部分,第一个部分是socks5服务端
,它负责监听本地的请求,另外一个部分是信息发送端
,它负责向远程的ssserver
发送数据包。
比如我想访问谷歌,首先需要配置浏览器的socks5
代理指向本地或者局域网内的socks5服务端
,然后socks5服务端
将包进行加密,从信息发送端
发送给远程的ssserver
。
ssserver介绍
ssserver
也分成两个部分,一个是信息接收端
,它是一个服务器,负责接收sslocal
发来的加密数据,另外一个是请求内容端
,它负责向目标服务器发送请求并将数据返回回去。
继续上述的访问谷歌的操作,ssserver
的信息接收端
接收到sslocal
发送的数据之后将数据进行解密,然后用请求内容端
将需要发送的数据发送给谷歌服务器,因为ssserver
是部署在国外,不受限制,然后将收到的数据原路返回即可到socks5客户端
。
使用wireshark分析shadowsocks
为了更加清楚的看清shadowsocks
的底层传输格式,由于我已经比较熟悉http的报文格式,所以通过浏览器的socks5
代理来与shadowsocks
进行连接,然后使用wireshark
分析底层的数据包。
参考文档 SOCKS5代理原理探索 socks5 rfc
以下的序号为上图左侧的抓包序号。
7-9 tcp的三次握手
10 socks5客户端
和socks5服务端
协商认证阶段
name | VER | NMETHODS | METHODS |
---|---|---|---|
size | 1 | 1 | 1 |
data | 05 | 01 | 00 |
VER
表示socks的版本,默认为5NMETHODS
字段是METHODS
字段占用的字节数METHODS
字段的每一个字节表示一种认证方式,表示客户端支持的全部认证方式。
11 服务端接收到客户端的请求之后,之后将认证方式发送给客户端
name | VER | METHOD |
---|---|---|
size | 1 | 1 |
data | 05 | 00 |
VER
表示socks的版本,默认为5METHOD
表示认证的方式,00
表示无需用户密码认证
13 socks5客户端指导shadowsocks
连接需要访问的服务器
name | VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
---|---|---|---|---|---|---|
size | 1 | 1 | 1 | 1 | Variable | 2 |
data | 05 | 01 | 00 | 03 | 115.114.113.112(已转成ascll) | 0050 |
VER
表示socks的版本,默认为5CMD
command的缩写,表示连接的方式- 0x01:建立 TCP 连接
- 0x03:关联 UDP 请求
RSV
保留字段,值为 0x00ATYP
address type 的缩写,取值为- 0x01:IPv4
- 0x03:域名
- 0x04:IPv6
15 当socks5服务端
与远程的需要访问的服务器建立连接之后,发送以下数据
name | VER | REP | RSV | ATYP | DST.ADDR | DST.PORT |
---|---|---|---|---|---|---|
size | 1 | 1 | 1 | 1 | Variable | 2 |
data | 05 | 00 | 00 | 01 | 0.0.0.0(已转成ascll) | 0050 |
REP
表示返回的连接状态- 0x00: 连接成功
- 其他状态参考rfc1928
17 纯正的http的Request Headers
简单粗暴的tcp发送报文,需要向远程服务器发送什么就发送什么
21 纯正的http的接收报文
远程服务器向本地发送什么,就返回给socks5客户端
什么,也是简单粗暴
27 由于远程需要访问的服务器断开连接,所以socks5服务端
对socks5客户端
也断开连接
源码分析
以下就是惊险刺激的源码分析了,目前分析的源码是shadowsocks2.9.1
因为sslocal
与ssserver
在写法上比较类似,都是接收数据+转发,所以在这里我们就只分析sslocal
。
以下贴出
sslocal.py
的主要的代码
def main():
# 检测Python版本是否支持
shell.check_python()
# 如果是已经将python转为exe文件则进入如下配置
if hasattr(sys, "frozen") and sys.frozen in \
("windows_exe", "console_exe"):
p = os.path.dirname(os.path.abspath(sys.executable))
os.chdir(p)
# 获取配置
config = shell.get_config(True)
if not config.get('dns_ipv6', False):
asyncdns.IPV6_CONNECTION_SUPPORT = False
# 守护进程设置
daemon.daemon_exec(config)
try:
# 录入日志
logging.info("starting local at %s:%d" %
(config['local_address'], config['local_port']))
# 生成dns寻址器
dns_resolver = asyncdns.DNSResolver()
# 生成tcp服务器
tcp_server = tcprelay.TCPRelay(config, dns_resolver, True)
# 生成udp服务器
udp_server = udprelay.UDPRelay(config, dns_resolver, True)
# 生成loop循环结构
loop = eventloop.EventLoop()
# 将以上的东西加入进loop循环结构
dns_resolver.add_to_loop(loop)
tcp_server.add_to_loop(loop)
udp_server.add_to_loop(loop)
def handler(signum, _):
logging.warn('received SIGQUIT, doing graceful shutting down..')
tcp_server.close(next_tick=True)
udp_server.close(next_tick=True)
signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler)
def int_handler(signum, _):
sys.exit(1)
signal.signal(signal.SIGINT, int_handler)
daemon.set_user(config.get('user', None))
# 循环运行
loop.run()
except Exception as e:
shell.print_exception(e)
sys.exit(1)
if __name__ == '__main__':
main()
代码的前半部分都是一些获取配置的,最主要的代码都在try...catch
结构中。其中主要的东西有loop循环结构
、dns寻址器
、tcp服务器
、udp服务器
。
loop循环结构
的代码在eventloop.py
中,其中封装了三种IO复用函数epoll
、kqueue
、select
,并且提供了统一的接口。
初始化dns寻址器
的代码在asyncdns.py
中,主要作用是生成dns
的相关配置
初始化tcp服务器
的代码在tcprelay.py
中,主要作用是通过配置文件对ip和端口号进行tcp的监听
初始化udp服务器
的代码在udprelay.py
中,主要作用是通过配置文件对ip和端口号进行udp的监听
整个程序通过
loop.run()
函数对客户端进行监听,具体代码如下
def run(self):
events = []
while not self._stopping:
asap = False
try:
events = self.poll(TIMEOUT_PRECISION)
except (OSError, IOError) as e:
if errno_from_exception(e) in (errno.EPIPE, errno.EINTR):
# EPIPE: Happens when the client closes the connection
# EINTR: Happens when received a signal
# handles them as soon as possible
asap = True
logging.debug('poll:%s', e)
else:
logging.error('poll:%s', e)
traceback.print_exc()
continue
for sock, fd, event in events:
handler = self._fdmap.get(fd, None)
if handler is not None:
handler = handler[1]
try:
handler.handle_event(sock, fd, event)
except (OSError, IOError) as e:
shell.print_exception(e)
now = time.time()
if asap or now - self._last_time >= TIMEOUT_PRECISION:
for callback in self._periodic_callbacks:
callback()
self._last_time = now
整个服务器在events = self.poll(TIMEOUT_PRECISION)
中进行阻塞对客户端进行监听,如果有客户端连接或者发送信息,则进入for sock, fd, event in events:
循环结构。
当有新的socks5客户端
进行访问,先进入tcprelay
类的handler.handle_event(sock, fd, event)
函数,在这个函数中,判断客户端是否是新连接的,如果为新连接上,则初始化TCPRelayHandler
,然后将其加入到eventloop
中进行循环监听。
当有已经连接过的
socks5客户端
发送信息则直接进入TCPRelayHandler
的handle_event
函数,函数大致如下
def handle_event(self, sock, event):
# handle all events in this handler and dispatch them to methods
if self._stage == STAGE_DESTROYED:
logging.debug('ignore handle_event: destroyed')
return
# order is important
if sock == self._remote_sock:
if event & eventloop.POLL_ERR:
self._on_remote_error()
if self._stage == STAGE_DESTROYED:
return
if event & (eventloop.POLL_IN | eventloop.POLL_HUP):
# read remote
self._on_remote_read()
if self._stage == STAGE_DESTROYED:
return
if event & eventloop.POLL_OUT:
# write remote
self._on_remote_write()
elif sock == self._local_sock:
if event & eventloop.POLL_ERR:
self._on_local_error()
if self._stage == STAGE_DESTROYED:
return
if event & (eventloop.POLL_IN | eventloop.POLL_HUP):
# read local
self._on_local_read()
if self._stage == STAGE_DESTROYED:
return
if event & eventloop.POLL_OUT:
# write local
self._on_local_write()
else:
logging.warn('unknown socket')
判断event
的状态,进入相关的函数
- 本地的
socks5客户端
发送信息,则进入_on_local_read()
函数 - 需要向本地的
socks5客户端
发送信息,则进入_on_local_write()
函数 - 远程的服务器发来信息,则进入
_on_remote_read()
函数 - 需要向远程的服务器发送信息,则进入
_on_remote_write()
函数
首先我们分析
socks5客户端
与sslocal
的通信
我们进入tcprelay.py
然后继续分析_on_local_read()
函数
首先判断本身与socks5客户端
的状态,由于初始化是STAGE_INIT
状态,即上文的10和11数据包,sslocal
随即发送05 00
数据包,然后更改状态为STAGE_ADDR
,即进入等待连接状态
当socks5客户端
像上文一样发送tcp连接请求时,连接的TCPRelayHandler
的状态为STAGE_ADDR
,随即进入_handle_stage_addr
函数,在此函数中马上发送05 00 00 01 00 00 00 00 10 10
给socks5客户端
,然后将刚刚收到的连接数据进行加密,保存到data_to_send
变量中,接着通过_dns_resolver.resolve(self._chosen_server[0],self._handle_dns_resolved)
的回调函数连接远程的ssserver
,然后将加密数据发送过去。上述函数是解析ssserver
地址并且回调到_handle_dns_resolved
函数与ssserver
连接,此时的连接状态是STAGE_CONNECTING
。
此时的ssserver
已经跟目标服务器连接上了
,socks5客户端
继续将数据发送给ssserver
,此时sslocal
进入_handle_stage_connecting
函数,将需要发送的数据加密发送给ssserver
。
接下来分析
sslocal
与ssserver
的通信
第一次sslocal
与ssserver
建立通信是在_handle_dns_resolved
的回调函数中,从此TCPRelayHandler
的remote_sock
与local_sock
相互绑定,二者互相监听互相通信。
之后通过_on_remote_read
函数接收ssserver
发送的数据,然后解密通过_write_to_sock
发送给sslocal
。
当ssserver
与sslocal
断开连接之后,sslocal
也会进入_on_remote_read
函数,发现没有接收到任何数据,即为关闭连接的状态,随后进入destroy
函数断开客户端连接并且销毁自己,这样连接就结束了。
总结
由于还没有仔细弄懂python的语法,所以一开始看python的代码还是感觉挺吃力的,况且shadowsocks
的注释很少,都是靠debug和输出内容大概理解作者的思路。阅读源码之前一定要看一下socks5
的底层数据原理,还要看一看python的tcp/ip通信的例子,这样才会少走好多弯路。为了巩固代码的理解,顺便为了学习python,我后续会再写一个socks5客户端
挂在服务器上面。在之后还会继续分析shadowsocks
的udp
功能。