[关闭]
@zwh8800 2017-08-03T05:58:27.000000Z 字数 4055 阅读 330187

使用 lua 编写 socks5 代理

blog 网络编程 lua libuv socks5


之前做过一个有趣的东西,把一部分 NodeJS 的 API 移植到了 lua 上。让我可以在 lua 里使用异步网络 API 了,我把做的这个小东西叫做 lua-libuv。今天去参加了一下 Gopher China 2016 中间听得无聊了,便打开电脑,基于自己的 lua-libuv 编写了一个 socks5 协议的 proxy。这篇博客,讲解一下 socks5 协议以及实现。



socks5 协议介绍

socks5 协议可能是中国网友见得最多的协议了。很多著名的工具 都就会在本机的 1080 端口开启一个 socks5 proxy 服务,供浏览器来调用。我们今天就来聊一聊 socks5 协议。

socks5 协议不只是在中国流行,它其实是最常见的代理协议之一,几乎所有浏览器都会原生支持 socks5 协议,而在 Mac 上则是系统级别的支持,所以可见其流行程度仅次于 http 代理。由于 socks5 是工作在 tcp 层的协议,所以它又比 http 代理灵活很多。比如 http 代理只能用来看网页。但是使用 socks5 协议还可以上外网打游戏,也可以使用代理登陆 QQ 。但是 socks5 也有自己的短板,比如对 udp 支持的不好(socks5 把域名 resolve 的事都做了,就为了避免 client 调用 udp )


socks5 协议详解

socks5 协议非常简单,rfc1928 整个只有 9 页,我大概用了 10 分钟粗略看了一下就明白 socks5 的设计了。

端口

socks5 一般回使用 1080 端口来提供服务。

握手

socks5 协议使用四次应答式的握手建立一个链接。分别会

四次握手结束之后,client 会把代理服务器当作是自己真正想通讯的服务器一样对待。而代理则会转发真正的通讯信息。

流程

1. 协商 method

当一个客户端需要建立一个 firewall 之外的连接时,首先向 socks5 服务器的 1080 端口发起一个 tcp 连接。

随后,客户端向服务器提供自己支持的 method 列表。数据的 payload 如下:

  1. +----+----------+----------+
  2. |VER | NMETHODS | METHODS |
  3. +----+----------+----------+
  4. | 1 | 1 | 1 to 255 |
  5. +----+----------+----------+

上面是字段名,下面是字段长度。

字段分别是:

method 的选项有如下几项,不过我感觉最常用的也就是第一个了。

2. 选择 method

服务器接到请求之后,应当选用一种 method 返回给客户端:

  1. +----+--------+
  2. |VER | METHOD |
  3. +----+--------+
  4. | 1 | 1 |
  5. +----+--------+

之后如果 method 是需要鉴权的,会进行相应的鉴权。这里不谈了。

3. 请求

之后客户端把自己想要连接的信息封成一个请求发给 proxy 。格式如下:

  1. +----+-----+-------+------+----------+----------+
  2. |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
  3. +----+-----+-------+------+----------+----------+
  4. | 1 | 1 | X'00' | 1 | Variable | 2 |
  5. +----+-----+-------+------+----------+----------+

其中,

这里面,命令又会有三种情况。如上,udp因为没有建立连接这一步,所以监听和连接是等同的。

地址类型也是,当地址类型不同时,目的地址的长度会不一样

所以需要按照 ATYP 来读取目的地址。

4. 回复

服务器收到请求后,需要向目的地址建立连接,如果失败了,需要告诉客户端原因。如果成功了,也需要告诉客户端代理服务器使用的地址和端口信息。

  1. +----+-----+-------+------+----------+----------+
  2. |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
  3. +----+-----+-------+------+----------+----------+
  4. | 1 | 1 | X'00' | 1 | Variable | 2 |
  5. +----+-----+-------+------+----------+----------+

例子

假如浏览器访问 https://lengzzz.com 需要如下对话

05 01 00

05 00

05 01 00 03(ATYP) 0b(LEN) 6c 65 6e 67 7a 7a 7a 2e 63 6f 6d(DOMAINNAME) 01 bb(PORT)

05 00 00 01(ATYP) 0a 00 00 11(IP) e9 c7(PORT)


Lua 实现

大概就是用了两个函数来 parse 出 client 的 payload ,分别 parse 协商 method 阶段的 payload 和请求阶段的 payload 。

  1. function parseMethodPayload(payload)
  2. if payload:byte(1) ~= SocksVersion then
  3. return nil, Errors.VersionError
  4. end
  5. local method = {
  6. version = SocksVersion,
  7. methods = {},
  8. }
  9. local methodCount = payload:byte(2)
  10. method.methods = {payload:byte(3, 3 + methodCount - 1)}
  11. return method
  12. end
  1. function parseRequestPayload(payload)
  2. if payload:byte(1) ~= SocksVersion then
  3. return nil, Errors.VersionError
  4. end
  5. local request = {
  6. version = SocksVersion,
  7. command = CommandType.Connect,
  8. addressType = AddressType.IPv4,
  9. distAddress = '',
  10. distPort = 0,
  11. }
  12. if payload:byte(2) > CommandType.Udp then
  13. return nil, Errors.CommandTypeNotSupported
  14. else
  15. request.command = payload:byte(2)
  16. end
  17. local requestAddressType = payload:byte(4)
  18. if requestAddressType ~= AddressType.IPv4 and
  19. requestAddressType ~= AddressType.DomainName and
  20. requestAddressType ~= AddressType.IPv6
  21. then
  22. return nil, Errors.AddressTypeNotSupported
  23. else
  24. request.addressType = requestAddressType
  25. end
  26. local portIndex
  27. if request.addressType == AddressType.IPv4 then
  28. local ipBytes = {payload:byte(5, 8)}
  29. request.distAddress = table.concat(ipBytes, '.')
  30. portIndex = 9
  31. elseif request.addressType == AddressType.DomainName then
  32. local len = payload:byte(5)
  33. request.distAddress = payload:sub(6, 6 + len - 1)
  34. portIndex = 5 + len + 1
  35. elseif request.addressType == AddressType.IPv6 then
  36. return nil, Errors.AddressTypeNotSupported
  37. end
  38. local portBytes = {payload:byte(portIndex, portIndex + 1) }
  39. request.distPort = portBytes[1] * 256 + portBytes[2]
  40. return request, nil
  41. end

比较关键的就是这里,在会场写的,还是比较糙了。详细的代码看 Github 就好啦:

https://github.com/zwh8800/lua-libuv/blob/master/test/socksProxy.lua

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注