shadowsocks源码阅读

逻辑结构

image.png

软件本身是违法的,因此不考虑其违法用途,使用场景可以假定为某个对外网进行了限制的局域网。

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
G:\ycl_dl\CHROME DOWNLOAD\SHADOWSOCKS-MASTER\SHADOWSOCKS
│ asyncdns.py
│ common.py
│ daemon.py
│ encrypt.py
│ eventloop.py
│ local.py
│ lru_cache.py
│ manager.py
│ server.py
│ shell.py
│ tcprelay.py
│ udprelay.py
│ __init__.py

└─crypto
openssl.py
rc4_md5.py
sodium.py
table.py
util.py
__init__.py

先大体的浏览了一遍,大致理解作用(有的代码实在不太懂)

  • 首先寻找main函数,在local.py和server.py中各有一个main函数,应该是本地的服务端和服务器的服务端的运行函数。
  • tcp/udprelay.py 主要负责tcp、udp相关的服务
  • crypto目录 负责加密部分,发现其可以使用的对称加密方法非常的多
  • asyncdns.py 按照rfc1035标准,实现了dns查询,async已经暗示了是异步处理的
  • encrypt.py 对crypto中的文件进行了调用,进一步形成接口
  • eventloop.py 这个地方实在没有看懂,上网看别人的博客分析,根据内容推测大概是为了实现并发,逻辑是在tcp/udprelay上层的
  • daemon.py 实现守护进程
  • shell.py 用于读取命令行内容,还可以确定python版本等配置内容
  • lru_cache.py lru缓存相关
  • common.py 一些函数,看到了例如字节流转成编程使用的数据类型的函数等等

运行逻辑

文中将shadowsocks简称为ss
由上面的简要阅读后,发现主要内容都在tcp/udprelay.py、eventloop.py、local.py、server.py当中,需要重点分析一番。

local.py和server.py

两个部分的代码都有一定的相似性,作用也都大同小异,于是合在一起说说阅读的理解。

不同点
  • server.py有用户名密码验证部分,对于local到来的连接,首先要进行用户名、密码的验证。
  • server端只能运行在linux或者unix操作系统上,因为使用有linux内核编程的部分,比如这一部分,使用到了Linux内核提供的fork函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    for i in range(0, int(config['workers'])):
    r = os.fork()
    if r == 0:
    logging.info('worker started')
    is_child = True
    run_server()
    break
    else:
    children.append(r)
    if not is_child:
    def handler(signum, _):
    for pid in children:
    try:
    os.kill(pid, signum)
    os.waitpid(pid, 0)
    except OSError: # child may already exited
    pass
    sys.exit()
    相同点
    相同点还是分常多的,剥去以上的两个不同点,代码实现的功能其实类似可以概括为以下两个部分:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        # 读取配置
    conf = shell.parse_config()
    # 根据配置决定要不要以守护进程的方式运行
    daemon.daemonize(conf)
    """
    此处省去好多if判断
    """
    loop = eventloop.init()
    tcp_server = tcprelay.init(conf)
    udp_server = udprelay.init(conf)
    dns_resolver = asyncdns.init(conf)

    # 将 TCPRelay、UDPRelay 和 DNSResolver 注册到事件循环中
    tcp_server.add_to_loop(loop)
    udp_server.add_to_loop(loop)
    dns_resolver.add_to_loop(loop)

    loop.run()
    以上,大致可以明白两个包含main函数的主要文件的操作逻辑,位于整个项目食物链的顶端,调用其他文件,也从这里分析出剩下的重要文件位于tcp/udprelay.py、eventloop.py当中。

    eventloop.py

    文件实现了三种“多线程”的方式,实际上是实现的io多路复用技术
  • epoll
  • select
  • kqueue

有这三种方法,最常用到的方法是epoll方法
epoll对象,是python自带的库select中实现的,可以实现多路复用技术

在linux,一切皆文件.所以当调用epoll_create时,内核给这个epoll分配一个file,但是这个不是普通的文件,而是只服务于epoll.
所以当内核初始化epoll时,会开辟一块内核高速cache区,用于安置我们监听的socket,这些socket会以红黑树的形式保存在内核的cache里,以支持快速的查找,插入,删除.同时,建立了一盒list链表,用于存储准备就绪的事件.所以调用epoll_wait时,在timeout时间内,只是简单的观察这个list链表是否有数据,如果没有,则睡眠至超时时间到返回;如果有数据,则在超时时间到,拷贝至用户态events数组中

相关用法记录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import select 导入select模块
epoll = select.epoll() 创建一个epoll对象
epoll.register(文件句柄,事件类型) 注册要监控的文件句柄和事件事件类型:
  select.EPOLLIN 可读事件
  select.EPOLLOUT 可写事件
  select.EPOLLERR 错误事件
  select.EPOLLHUP 客户端断开事件
epoll.unregister(文件句柄) 销毁文件句柄
epoll.poll(timeout) 当文件句柄发生变化,则会以列表的形式主动报告给用户进程,timeout超时时间,默认为-1,即一直等待直到文件句柄发生变化,如果指定为1 那么epoll每1秒汇报一次当前文件句柄的变化情况,如果无变化则返回空
epoll.fileno() 返回epoll的控制文件描述符(Return the epoll control file descriptor)
epoll.modfiy(fineno,event) fineno为文件描述符 event为事件类型 作用是修改文描述符所对应的事件
epoll.fromfd(fileno) 从1个指定的文件描述符创建1个epoll对象
epoll.close() 关闭epoll对象的控制文件描述符

ss实现多路复用的方式是建立一个eventloop类,再类中定义以下变量

  • _fdmap
    作为一个字典,存储一些handler,也就是存储着事件
  • _last_time
    存储超时时间
  • _periodic_callbacks
    数组形式,记录回调函数
  • _stopping
    默认值为false,当调用stop方法是置为true

整个文件的精华其实是run函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)
import traceback
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

简要的说它实际上干了这样一个事情:
当发生事件时,通过事件对应的文件描述符 fd 找到 handler,调用 handler.handle_event(sock, fd, event) 来将事件交由 handler 处理,同时每隔 TIMEOUT_PRECISION 秒调用 TCPRelay、UDPRelay 或 DNSResolver 的 handle_periodic 函数处理超时或清除缓存
eventloop文件阅读完毕,迫切需要ralay文件参与了现在。

tcprelay.py

因为我们的主要目的就是为了实现tcp,对于udp实际上是不怎么关心的,因此我们只看一个,这时候发现不论是local端还是server端,他们都调用的同一个TCP relay,但是两者的功能又不很一样,因此有几分好奇。
tcprelay文件是项目中最大的文件,一共有700多行,主要定义了两个类

  • TCPRelayHandler
  • TCPRelay

这个时候想要分析两个类,我们就不得不回到main函数里面寻找,发现逻辑是这样的

  • main起来后会把TCPRelay加到eventloop。
  • 有新连接进来后在TCPRelay的handle_event处理,accept后生成一个TCPRelayHandler用于处理后续的事件。
  • TCPRelayHandler初始化的时候会把自己加到loop里,并把自己添加到fd_to_handlers,后续再有事件TCPRelay 会通过 fd_to_handlers 交对应的 TCPRelayHandler中的handle_event处理。

个人感觉这个文件中最神奇的部分是对于本地端口和远程端口的定义,对于业务逻辑和local与server对同一段代码的复用有至关重要的作用,网上找到了一张描述的很好的图
image.png

其次开头的注释也非常重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# as sslocal:    
# stage 0 auth METHOD received from local, reply with selection message ——STAGE_INIT
# stage 1 addr received from local, query DNS for remote ——STAGE_ADDR
# stage 2 UDP assoc ——STAGE_UDP_ASSOC
# stage 3 DNS resolved, connect to remote ——STAGE_DNS
# stage 4 still connecting, more data from local received ——STAGE_CONNECTING
# stage 5 remote connected, piping local and remote ——STAGE_STREAM

# as ssserver:
# stage 0 just jump to stage 1
# stage 1 addr received from local, query DNS for remote
# stage 3 DNS resolved, connect to remote
# stage 4 still connecting, more data from local received
# stage 5 remote connected, piping local and remote

定义了许多的stage,分别于我们通信过程中的流程相对应,具体含义在后面也解释的很清楚。同时每一种状态在tcp_handler类里面都有对应的方法,负责了socks5中通讯的过程,整理如下:

1
2
3
4
5
6
7
STAGE_INIT —— _handle_stage_init    
STAGE_ADDR —— _handle_stage_addr
STAGE_UDP_ASSOC ——对应UdpRelay中的方法,这里不做说明
STAGE_DNS —— 不做处理, 继续等待dns解析
STAGE_CONNECTING —— _handle_stage_connecting
STAGE_STREAM —— _handle_stage_stream
STAGE_DESTROYED —— destroy

处理函数起到的作用与注释中的目的相同,不多做解释,可读性也比较强。
同时所有的函数命名都有一定的特点,如_handler_stage_xxx类似函数,都是对socks5协议通信相关,_local\remote_read\write类似函数都是与套接字数据的读取有关,规律性非常的强。
另一个类中值得一提的就是handle_event函数,其实在上一个文件中这个方法就出现了

1
2
3
4
5
6
7
8
9
10
11
12
def handle_event(self, sock, fd, event):
# 如果是 TCPRelay 的 socket
if sock == self._server_socket:
conn = self._server_socket.accept()
TCPRelayHandler(self, self._fd_to_handlers,
self._eventloop, conn[0], self._config,
self._dns_resolver, self._is_local)
else:
# 找到 fd 对应的 TCPRelayHandler
handler = self._fd_to_handlers.get(fd, None)
if handler:
handler.handle_event(sock, event)

目的就是找到对应的handler去进行具体的操作。

不忘初心

因为阅读别人项目的初心是为了写自己的加密通信程序,因此对于加密部分也打开稍微的阅读了一下。

加密算法

有单个文件,分别实现三种方法,分别进行阅读
openssl.py 的加密算法使用的是基于openssl的python的一个库,叫Crypto,它通过调用这个库实现好多的加密方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# openssl.py
ciphers = {
'aes-128-cfb': (16, 16, OpenSSLCrypto),
'aes-192-cfb': (24, 16, OpenSSLCrypto),
'aes-256-cfb': (32, 16, OpenSSLCrypto),
'aes-128-ofb': (16, 16, OpenSSLCrypto),
'aes-192-ofb': (24, 16, OpenSSLCrypto),
'aes-256-ofb': (32, 16, OpenSSLCrypto),
'aes-128-ctr': (16, 16, OpenSSLCrypto),
'aes-192-ctr': (24, 16, OpenSSLCrypto),
'aes-256-ctr': (32, 16, OpenSSLCrypto),
'aes-128-cfb8': (16, 16, OpenSSLCrypto),
'aes-192-cfb8': (24, 16, OpenSSLCrypto),
'aes-256-cfb8': (32, 16, OpenSSLCrypto),
'aes-128-cfb1': (16, 16, OpenSSLCrypto),
'aes-192-cfb1': (24, 16, OpenSSLCrypto),
'aes-256-cfb1': (32, 16, OpenSSLCrypto),
'bf-cfb': (16, 8, OpenSSLCrypto),
'camellia-128-cfb': (16, 16, OpenSSLCrypto),
'camellia-192-cfb': (24, 16, OpenSSLCrypto),
'camellia-256-cfb': (32, 16, OpenSSLCrypto),
'cast5-cfb': (16, 8, OpenSSLCrypto),
'des-cfb': (8, 8, OpenSSLCrypto),
'idea-cfb': (16, 8, OpenSSLCrypto),
'rc2-cfb': (16, 8, OpenSSLCrypto),
'rc4': (16, 0, OpenSSLCrypto),
'seed-cfb': (16, 16, OpenSSLCrypto),
}

总的来说这一部分读起来还是挺让人疑惑的:一是相关文件之间的调用非常奇怪;二是密码好像是写死的16个”k”,他被写死在了程序当中

1
2
3
4
def run_method(method):
cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1) # 这里相当奇怪
decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)

同时还有一个rc4-md5.py文件,单独对rc4加密、md5做验证的加密传输方式进行了单独的包装,但是加密过程简单,相比来看破解也更简单。
此外还有一个sodium.py文件,文件主要定义了另一种新颖的加密方式:CHACHA20,之前没有接触过,具体看了一下,也上网搜集了一部分的资料

ChaCha20-Poly1305是Google所采用的一种新式加密算法,性能强大,在CPU为精简指令集的ARM平台上尤为显著(ARM v8前效果较明显),在同等配置的手机中表现是AES的4倍(ARM v8之后加入了AES指令,所以在这些平台上的设备,AES方式反而比chacha20-Poly1305方式更快,性能更好),可减少加密解密所产生的数据量进而可以改善用户体验,减少等待时间,节省电池寿命等。
20指的就是20轮的加密

密码部分总结

总的来讲,ss可以选择三种算法

  • aes 性能较好,保密性好,应用普遍,最为常用的方法
  • chacha20 在arm指令集上表现优异,算法新颖,破解难度大保密性好
  • rc4-md5 加密速度非常快,但用在外网上又被恶意破解的可能性

感受

  • 通过阅读别人的优秀代码,对个人的知识的提升感觉非常的大。学到了很多关于网络流量处理架构的知识,对相关协议,如dns协议、socks5协议,python的网络编程、python对于系统内核的调用等方面都感觉受益匪浅。因为工程本身代码量不超过万行,同时注释和相关readme也相当的详细,因此感觉非常适合读一读。
  • 同时对与项目对密码的实际应用也有了了解,了解到一种新型的加密算法
  • 读代码过程中的问题:1.没有研究lru、和线程守护相关的内容;2.如果多线程对同一变量同时进行修改,存在的写和读的问题,ss似乎没有解决,也可能存在死锁(不过死锁之后就会超时然后杀死线程,这么想想好像没毛病,但这也太暴力了)
  • 对于自己项目的实现也有一定的启示:1.有了对完成代码的进一步认识,诸如需要完成哪些部分,重点部分在哪里2.我觉得可以减少一些个人项目中不必要的部分,如可以不编程shell、lru、deamon部分、同时将两个服务器之间可以不加验证的连接。