支持 Mu API 的无侵入式多用户 Shadowsocks Server 中间件

从挑轮子到自己造轮子。

一年半前,32service 开始使用 orvice/ss-panel 管理,Server 使用的是作者基于 Shadowsocks-Go 修改的 shadowsocks-go-mu

但是 Shadowsocks-Go 版本已经多年没有更新了,而 orvice fork 的 Mu 版本连 UDP Relay 都不支持。后来逛 GitHub 发现了 shadowsocks-py-mu,作者介绍是基于 Shadowsocks-Python 版添加了兼容 Mu API 的功能。不过当时的版本在启用 OTA 和 UDP Relay 之后会有 Bug,和 Shadowsocks-libev 版本的 Client 连接时会出现 TCP RST 的问题。这个问题一度困扰了我好几个月,和朋友多次测试之后才定位到了 Python 版和 libev 版本的兼容问题。后来想着,要是 libev 版本能支持多用户功能就好了。

就这样有了 shadowsocks-munager

Shadowsocks Python 版在设计当初就有为多用户作打算,以至于后来同样是 clowwindy 编写的 libev 版本也支持这样的接口。当然,也有 Python 的 fork 版本支持直接读取 MySQL 的,不过不在本文讨论范围。ss-manager 提供的 API 很简单,用 socket 通信,发 UDP 包,具体 Python 例子可以直接看 Wiki。

shadowsocks-munager 一开始的想法很简单,通过 Mu API 读用户端口密码,发 UDP 包通知 ss-manager 开启对应端口,实际上是 ss-manager 会新建一个 ss-server 进程。之后是获取从启动到现在每个端口的流量,减去上次的流量就知道这一时段跑了多少流量,通过 Mu API POST 回服务器即可。分析之后发现没什么技术难度,就是一个有限状态机,额外会有一些特殊情况,例如是欠费了,超流量了,新端口没有初始化之类的。技术栈也很简单,requests + sleep,全程同步阻塞操作。

实际上,上面描述的第一个版本已经稳定在好几台服务器上跑好几个月,只是 Pythonista 的毛病又犯了,实在是看不惯同步请求和 sleep 这么粗暴的做法,而且把用户数据丢在 Python Dict 上也让我踹踹不安。而让我下定决心重构的一个契机是,我们需要计算一个时间差里面网卡跑的流量而换算成网速,这个时间差就很微妙了,短了不准确,长则会阻塞其他操作,就算是 fork 出来一个线程干这件事也是不够优雅,IO 密集程序的归宿是异步。

关于异步 Python 异步框架也不少,我想到的第一个是 gevent 的 Monkey patching,不过打 patching 这件事太 evil 了,即使是 useful evil。后来看了看 Twisted,之前用 Scrapy 的时候稍稍了解过,当时为了实现一个简单的非阻塞 sleep,啪啪啪出来一堆代码把我吓呆了,pass pass。最后就是选了 Tornado,比较好上手,而且我主要都是 HTTP 请求,不会花很多时间在数据库的 IO,所以 Tornado 封装得恰到好处。选完异步框架,就顺便把数据储存的坑给填了,存这些读写频繁而又不特别对持久化有要求的数据,当然是选择 Redis 啦。

在设计上,我抽象出了三个类,Munager、SSManager 和 MuAPI。

  1. SSManager 负责管理每一个 ssserver,将每个端口的流量记录到 Redis,并提供接口查询每个端口的密码和加密方式。由于 ss-manager 没有提供查询的 API,实际是这部分的信息是序列化在 Redis ,由 SSManager 自行维护的。
  2. MuAPI 封装了和 sspanel 的 Mu API。
  3. Munager 是在 Top Level,负责定时从 MuAPI 读取当前用户信息,然后通知 SSManager 要增删那些端口;此外还需要从 SSManager 读取每个端口的流量,通过 MuAPI 写入计费系统;最后 Munager 还承担着一系列边缘功能,例如是服务器当前负载统计,网速统计,到各个节点的延时和丢包统计的功能。

没有什么问题是套一个中间件解决不了的!