开篇
这是一个从去年11月份开始的一个新项目,名字瞎起的,叫 taosocks,是一个支持 SOCKS/HTTP 协议的智能跨平台隧道代理软件,至于哪里智能了,请继续阅读。
该软件的早期版本使用C++语言写成,基于完成端口,但是只能运行在 Windows 上。算是对协议的熟悉,以及对完成端口的练习。实现基本功能之后,此版本就停止开发了。开始学习 Go语言。学习一个月Go语言之后,就开始用Go语言全部重新实现之前的所有功能。
写这个软件主要是为了学习。学习协议、Go语言等。同时作为一个后备软件,你懂的。
自己试用了三个月了已经,很稳定。暂时先发文公开在这里,后面可能会删除文章并闭源项目。
架构与原理
整体架构与工作原理应该跟 shadowsocks 相近,但是我没有参阅它的任何逻辑(我不会 Python)。
该软件分为两端:服务端和客户端。服务端运行在服务器上,客户端运行在本地。
客户端接受来自本地的连接,然后通过服务器转发其流量到目标机器。
大概类似下面图示,图画得简单,但应该很好理解:
在没有启用代理时
互联网(数据透明传输)
浏览器 <------------------------------------------> 网站服务器
启用代理后
互联网(数据加密传输)
浏览器 <-------> 代理客户端 <------------------------------------> 代理服务端 <-------> 网站服务器
+------------------------+
运行在本地
以上两个图示中:浏览器代指所有有代理需求的软件,网站服务器代指所有类型的目标机器。下同。
数据包与加密协议
基本前提
其实从根本上来说,本代理软件本身并没有加密任何流量数据。数据的加密依靠的是 代理客户端 到 代理服务端 的连接协议。
代理服务端它其实是被设计成是一个 HTTPS 服务器。如果用浏览器直接连接代理服务端,将会打开一个配置好的网站(准备移除此功能)。
那么,代理客户端是如何连接到代理服务端的呢?答案很简单:使用 HTTP/1.1 的 协议升级请求。
HTTPS证书
证书用于保证 HTTPS 连接的正确建立。到目前为止,暂时都是使用的自签证书(非公开安全)。
关于证书的更多详细,暂时不多做介绍。
连接协议
-
代理客户端首先会向一个浏览器一样发一个普通的 HTTP GET 请求到代理服务端。并附带如下的请求头部:
Connection: Upgrade Upgrade: taosocks/<version> Authorization: taosocks <key>
熟悉 HTTP协议 的朋友可能一看就明白了,这其实就是个 HTTP/1.1 协议中标准的升级协议请求,
Authorization
则是用来登录鉴权的。 就像由 HTTP/1.1 升级到 WebSocket / HTTP2.0 那样。不过这里,升级到的是我自己的协议。 -
代理服务端在检测到这个协议升级请求后,会验证协议的名字、版本、密钥,如果均验证成功,就会升级到目标协议。 简单说就是把 TCP连接 转交给后续协议维持。更简单地说,就是劫持(Hijack)。
数据传输
连接被劫持后,代理服务端就不再是一个网站服务器的角色了,而成了真真正正的「代理服务端」。
代理客户端接收来自浏览器的数据,并通过互联网直接传输到代理服务端。代理服务端则负责把代理客户端发送过来的数据转发给目标网站。 而如果是目标网站发给浏览器的数据,则由代理服务端通过互联网直接传输到代理客户端。其实说白了,就是个 TCP 中继。
由此看来,把加密交给 HTTPS 去做以后,本代理软件自己的协议及其简单了。
数据包
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
超级简单,有木有?有!有!有!(真的超级简单,不骗你。逃……
- OpenPacket 一个地址:负责告诉代理服务端:浏览器要连接哪个网站服务器。
- OpenAckPacket 一个布尔值:负责告诉代理客户端:这个网站服务器的连接状况。
- RelayPacket 一个字节数组(切片):自然就是中继数据。
用户认证
用户的认证在建立到代理服务端的连接的时候就开始了,即:Authorization
的请求头部值。
如果验证成功,则升级协议。如果验证失败,忽略此升级请求,并当成普通 HTTP 请求对待。
也就是说:你无法探测一个 HTTPS 网站服务器它到底是不是一个支持代理的代理服务端。
除非,除非你同时提供了正确的:协议名字、协议版本、密钥。
过滤规则
支持的主机过滤规则:
- 域名
- IP地址
- CIDR
支持的过滤规则有:
- 直接连接
- 总是代理
- 拒绝连接
智能代理
另外,本代理软件最大的亮点是:智能代理。
智能代理会在目标网站无法通过本地访问时,自动切换到远程代理。并自动添加代理规则。 由于是智能识别,在第一次访问不能通过本地访问的网站时,可能要花费较长的时间来确定规则。但此后就非常快了,因为规则被记录了。
目前的智能算法有以下几点:
- 如果域名解析失败。即:无解析
- 如果域名解析被污染。即:解析出错误的IP地址
- 如果目标 HTTP 服务器无响应。即:发送了数据,但没有收到响应
- 如果目标 HTTPS 数据包被丢弃。即:TLS协议未完成,比如:只发送了
Client Hello
,但没收到回复
示例输出
大概会有类似下面这样的输出:
2018/05/02 22:53:56 ? Dial host:www.facebook.com:443 - dial tcp 66.220.149.18:443: getsockopt: connection refused
2018/05/02 22:53:58 > [Proxy ] www.facebook.com:443
2018/05/02 22:53:58 + 添加代理规则:www.facebook.com
2018/05/02 22:53:59 < [Proxy ] www.facebook.com:443 [TX:822, RX:342]
2018/05/02 22:54:09 ? Dial host:static.xx.fbcdn.net:443 - dial tcp: i/o timeout
2018/05/02 22:54:11 > [Proxy ] static.xx.fbcdn.net:443
2018/05/02 22:54:11 + 添加代理规则:static.xx.fbcdn.net
2018/05/02 22:54:13 < [Proxy ] static.xx.fbcdn.net:443 [TX:1143, RX:6240]
2018/05/02 22:54:23 ? Dial host:scontent-lax3-1.xx.fbcdn.net:443 - dial tcp: i/o timeout
2018/05/02 22:54:25 > [Proxy ] scontent-lax3-1.xx.fbcdn.net:443
2018/05/02 22:54:25 + 添加代理规则:scontent-lax3-1.xx.fbcdn.net
2018/05/02 22:54:26 ? Dial host:staticxx.facebook.com:443 - dial tcp 69.171.240.27:443: i/o timeout
2018/05/02 22:54:27 < [Proxy ] facebook.com:443 [TX:822, RX:342]
2018/05/02 22:54:28 + 添加代理规则:staticxx.facebook.com
2018/05/02 22:54:28 > [Proxy ] staticxx.facebook.com:443
2018/05/02 22:54:29 ? Dial host:cx.atdmt.com:443 - dial tcp 199.16.156.40:443: getsockopt: connection refused
2018/05/02 22:54:31 > [Proxy ] cx.atdmt.com:443
2018/05/02 22:54:31 + 添加代理规则:cx.atdmt.com
2018/05/02 22:54:31 + 添加代理规则:cx.atdmt.com
2018/05/02 22:54:31 > [Proxy ] cx.atdmt.com:443
2018/05/02 22:54:34 < [Proxy ] cx.atdmt.com:443 [TX:1284, RX:684]
详细配置见文后「配置文件」。
代码与编译
项目地址
一如既往,托管在 GitHub 上: taosocks - A smart tunnel proxy that helps you bypass firewalls.。
目录结构
目录 | 含义 |
---|---|
client | 客户端代码 |
server | 服务端代码 |
internal | 客户端与服务端公共文件 |
config | 配置文件目录 |
项目编译
本项目未依赖任何第三方的包,编译简单。
分别进入到 client
和 server
目录后,执行 go build
命令即可。
配置文件
rules.txt
这个是代理规则文件。规定哪些地址直接连接、走代理或被拒绝。
格式:
域名,规则
IP,规则
IP段,规则
比如:
# qq.com 或以 qq.com 结尾的:直连
qq.com,direct
# 192.168.*.* 直连
192.168.0.0/16,direct
不在此规则中的主机将默认为直接访问,或根据智能代理确定访问。
rules-auto.txt
这里保存了自动添加的规则。程序退出(^C
)和启动时,会自动读取里面的规则,并设置为走代理。
运行
注意:请在命令行下执行!请在项目根目录执行!
根目录结构
$ ls -l
total 15
drwxr-xr-x 1 tao 197121 0 Jan 12 09:04 client
drwxr-xr-x 1 tao 197121 0 Jan 12 09:04 config
drwxr-xr-x 1 tao 197121 0 Dec 5 09:55 internal
drwxr-xr-x 1 tao 197121 0 Jan 12 09:04 server
运行代理服务端
代理服务端仅需两个参数,其它的固定在代码里面了。
$ ./server/server -h
Usage of ./server/server:
-key string
the key
-listen string
listen address(host:port) (default "0.0.0.0:1081")
$ ./bin/server --key="your password"
如果没报错的话,代理服务端就已经正常运行了。同时它了是一个 HTTPS 服务器。可以用 cURL 试试(输出内容随版本变更可能不一致)。
$ curl -k https://127.0.0.1:1081
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<h1>Welcome home!</h1>
</body>
</html>
运行代理客户端
客户端参数有好几个。
- --listen 本地监听地址,SOCKS代理和HTTP代理共用
- --server 代理服务端地址
- --insecure 是否验证证书,目前请总是设置成否(有此参数即可)
- --key 密钥
运行起来:
$ ./client/client -h
Usage of ./client/client:
-insecure
don't verify server certificate (default true)
-key string
login key
-listen string
listen address(host:port) (default "0.0.0.0:1080")
-server string
server address(host:port) (default "127.0.0.1:1081")
$ ./bin/client --key="your password"
设置浏览器代理/cURL试试
HTTP代理:
$ curl --proxy http://127.0.0.1:1080 https://example.com
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is established to be used for illustrative examples in documents. You may use this
domain in examples without prior coordination or asking for permission.</p>
<p><a href="http://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
SOCKS代理:
$ curl --proxy socks5h://127.0.0.1:1082 https://example.com
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is established to be used for illustrative examples in documents. You may use this
domain in examples without prior coordination or asking for permission.</p>
<p><a href="http://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
输出的日志
服务端:
2018/03/09 16:12:41 > example.com:443
2018/03/09 16:12:42 < example.com:443
2018/03/09 16:14:19 > example.com:443
2018/03/09 16:14:20 < example.com:443
客户端:
2018/03/09 16:12:41 > [Proxy ] example.com:443
2018/03/09 16:12:42 < [Proxy ] example.com:443 [TX:929, RX:4934]
2018/03/09 16:14:19 > [Proxy ] example.com:443
2018/03/09 16:14:20 < [Proxy ] example.com:443 [TX:929, RX:4934]