Cloudflare Workers是著名的Serverless运行平台,其底层为Chrome V8引擎,可以在Cloudflare的全球分布式节点上运行JavaScript/WASM。近期,Cloudflare Workers新发布了一个用于创建TCP套接字的API - connect()。利用此API可以在Workers上实现基于TCP的会话层协议,例如SOCKS5。本文将会讲解SOCKS5协议细节,并在Workers上实现SOCKS5客户端。
SOCKS5应作为中间件使用,为此可以设计一个函数,对connect()
进行封装:
/**
*
* @param {Array} socks5
* @param {number} addressType
* @param {string} addressRemote
* @param {number} portRemote
*/
export async function socks5Connect(socks5, addressType, addressRemote, portRemote) {
const [port, hostname, password, username] = socks5.reverse();
// Connect to the SOCKS server
const socket = connect({
hostname,
port,
});
// Communicate with the SOCKS server
// ...
return socket;
}
SOCKS5代理过程分为三个阶段: 握手(包括协商和认证)、请求、Relay
握手
握手阶段包含协商和认证,其中协商阶段对协议版本与认证方法进行确认。而认证阶段进行身份验证,no authorization
模式可以省去。
协商
首先客户端向SOCKS5服务器发送greeting请求,其请求的内容为:
+-----+---------+----------+
| VER | NAUTH | AUTH |
+-----+---------+----------+
| 1 | 1 | 1 to 255 |
+-----+---------+----------+
其中VER
为版本号,NAUTH
表示AUTH
字段的字节数,即客户端允许的认证方法数。
AUTH
可选值有:
- 0x00 不需要认证
- 0x01 GSSAPI
- 0x02 用户名、密码认证
- 0x03 - 0x7F由IANA分配(保留)
- 0x03: 握手挑战认证协议
- 0x04: 未分派
- 0x05: 响应挑战认证方法
- 0x06: 传输层安全
- 0x07: NDS认证
- 0x08: 多认证框架
- 0x09: JSON参数块
- 0x0A–0x7F: 未分派
- 0x80 - 0xFE 为私人方法保留
- 0xFF 无可接受的方法
本文只实现其中最常用的两种:0x00
与0x02
。以下代码构造greeting内容并发送到SOCKS5服务器:
const socksGreeting = new Uint8Array([5, 2, 0, 2]);
const writer = socket.writable.getWriter();
await writer.write(socksGreeting);
服务端收到greeting
请求后会向客户端发送如下格式的信息:
+-----+---------+
| VER | CAUTH |
+-----+---------+
| 1 | 1 |
+-----+---------+
其中CAUTH
表示被选中的认证方法,返回值为0xFF
时表示没有可接受的认证方法。可以通过以下代码判断其返回值:
const reader = socket.readable.getReader();
let res = (await reader.read()).value;
if (res[0] !== 0x05) {
console.log(`socks server version error: ${res[0]} expected: 5`)
return;
}
if (res[1] === 0xff) {
console.log("no acceptable methods")
return;
}
当返回值为0x0500
时表示不需要认证,对于返回值为0x0502
的情况我们进入认证阶段。
认证
用户名/密码认证时,客户端先向服务端发送发送如下格式的数据:
+----+------+----------+------+----------+
|VER | ULEN | UNAME | PLEN | PASSWD |
+----+------+----------+------+----------+
| 1 | 1 | 1 to 255 | 1 | 1 to 255 |
+----+------+----------+------+----------+
其中VER
为该认证方式的版本号,与上述协议版本不同,目前恒为0x01
。服务端校验客户端认证信息后,返回如下格式响应结果:
+-----+--------+
| VER | STATUS |
+-----+--------+
| 1 | 1 |
+-----+--------+
VER
值与客户端请求对应,恒为0x01
。仅当响应结果为0x0100
时表示认证成功。
if (res[1] === 0x02) {
console.log("socks server needs auth")
if (!username || !password) {
console.log("please provide username/password")
return;
}
const authRequest = new Uint8Array([
1,
username.length,
...(new TextEncoder()).encode(username),
password.length,
...(new TextEncoder()).encode(password)
]);
await writer.write(authRequest);
res = (await reader.read()).value;
// expected 0x0100
if (res[0] !== 0x01 || res[1] !== 0x00) {
console.log("fail to auth:", res.join(","))
return;
}
}
Relay
当认证通过或无需认证时进入请求与转发阶段。在该阶段,客户端先向服务端请求需要访问的地址。
其请求数据中的地址部分的格式为:
+------+----------+
| TYPE | ADDR |
+------+----------+
| 1 | Variable |
+------+----------+
TYPE
表示该地址的类型,可选值有:
- 0x01: IPv4 地址
- 0x03: 域名
- 0x04: IPv6 地址
紧随其后的ADDR
根据其类型也有三种情况:
- IPv4:4字节数据
- 域名:开头一字节表示域名长度,其后1-255字节为域名
- IPv6:16字节数据
完整的请求数据的结构为:
+----+-----+-----+----------+---------+
|VER | CMD | RSV | DSTADDR | DSTPORT |
+----+-----+-----+----------+---------+
| 1 | 1 | 1 | Variable | 2 |
+----+-----+-----+----------+---------+
VER
为协议版本,CMD
为命令码:
- 0x01: 表示CONNECT请求,即建立 TCP/IP 流连接
- 0x02: 表示BIND请求,即建立 TCP/IP 端口绑定
- 0x03: 表示UDP转发,即分配 UDP 端口
RSV
为保留字段,必须为0x00
,DSTPORT
为端口号,字节序为网络字节序(大端)
// addressType
// 1--> ipv4
// 2--> domain name
// 3--> ipv6
let DSTADDR;
if (addressType === 1) {
DSTADDR = new Uint8Array([1, ...addressRemote.split('.').map(Number)]);
} else if (addressType === 2) {
DSTADDR = new Uint8Array(
[3, addressRemote.length, ...(new TextEncoder()).encode(addressRemote)]
);
} else if (addressType === 3) {
DSTADDR = new Uint8Array(
[4, ...addressRemote.split(':').flatMap(x => [parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)])]
)
}
const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, portRemote >> 8, portRemote & 0xff]);
await writer.write(socksRequest);
console.log('Sent socks request');
服务端返回数据的格式为:
+-----+--------+-----+----------+---------+
| VER | STATUS | RSV | BNDADDR | BNDPORT |
+-----+--------+-----+----------+---------+
| 1 | 1 | 1 | Variable | 2 |
+-----+--------+-----+----------+---------+
STATUS
为状态码:
- 0x00: 请求被批准
- 0x01: 一般失败
- 0x02: 规则集不允许连接
- 0x03: 网络不可达
- 0x04: 主机不可达
- 0x05: 连接被目标主机拒绝
- 0x06: TTL 超时
- 0x07: 命令不支持或协议错误
- 0x08: 地址类型不支持
这里BNDADDR
和BNDPORT
表示绑定地址与绑定端口,其中BNDADDR
的格式与上述客户端请求数据中的地址格式相同。这两个数据表示relay服务器的地址与端口,当socks5服务器与relay服务器不同时,socks5服务器可以通过这两个字段指定relay服务器。客户端应根据该字段将后续请求发送到relay服务器,并从其获取响应数据。不过一般情况下relay服务器与socks5服务器是同一台,此时BNDADDR
与BNDPORT
全部为0。为降低复杂性,本文仅考虑socks5服务器与relay服务器相同的情况。同时考虑到鲁棒性,下述代码仅对STATUS
进行校验。
res = (await reader.read()).value;
if (res[1] === 0x00) {
console.log("socks connection opened:", res.join(","))
} else {
console.log("fail to open socks:", res.join(","))
return;
}
writer.releaseLock();
reader.releaseLock();
最后释放读写锁。此后即进入转发阶段,正常发送/读取数据即可。