前言
最近在网上看到一篇文章《你也能写个 Shadowsocks》,于是处于好奇就了解到了SOCKS5协议。
这里我就根据SOCKS5协议,通过NodeJS实现一个最简单的,没有加密,不需要认证Socks5代理服务。
我相信大家都听过Socket,而Socks5协议是一种网络传输协议,常用过代理,之前很有名的shadow socks也是基于此协议实现的。这里大家单词可不要看错了。
完整代码:https://gitee.com/baojuhua/node_simples/tree/master/node_simple_socks5_server
编写TCP服务
我们使用NodeJS的net
模块编写tcp服务。
使用socket.once('data',.....)
来获取首次代理请求的数据
const net = require('net');
let server = net.createServer(function (socket) {
socket.once('data', function (data) {
console.log(JSON.stringify(data));
});
socket.on('error', function (err) { console.error(`error:${err.message}`); });
});
server.listen(11100);
在浏览器中设置Socks5代理
我使用的插件是SwitchyOmega
测试效果:
首次响应请求
上面我们获取到了首次请求的数据,根据SOCKS5协议我们可以知道其含义如下:
- | VERSION | METHODS_COUNT | METHODS... |
---|---|---|---|
data | 0x05 | 0x01 | 0x00 |
说明 | 协议版本,目前固定0x05 | 客户端支持的认证方法数量 | 不需要认证 |
而我们在无需验证的情况下直接返回的结果如下:
- | VERSION | METHOD |
---|---|---|
data | 0x05 | 0x00 |
说明 | SOCKS协议版本,目前固定0x05 | 本次连接所用的认证方法,上例中为无需认证 |
根据协议添加协议验证与响应:
//....
let server = net.createServer(function (socket) {
socket.once('data', function (data) {
if (!data || data[0] !== 0x05) return socket.destroy();
socket.write(Buffer.from([5, 0]), function (err) {
if (err) socket.destroy();
socket.once('data', (data) => {
console.log(JSON.stringify(data));
});
});
});
//....
});
//....
测试效果:
实现请求解析
客户端在收到认证成功的消息后,会给代理服务器发送命令,其含义简介如下:
- | VERSION | COMMAND | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|---|---|---|---|---|---|
长度 | 1字节 | 1字节 | 1字节 | 1字节 | 1-255字节 | 2字节 |
说明 | SOCKS协议版本,固定0x05 | 命令(本文只实现CONNECT) | 保留字段 | 目标服务器地址类型(本文仅实现IP V4与域名) | 目标地址 | 端口 |
需要说的是在域名类型下,域名地址第1个字节为域名长度,剩下字节为域名名称字节数组
而服务端解析成功后,需要情况进行对应的响应:
- | VERSION | RESPONSE | RSV | ADDRESS_TYPE | BND.ADDR | BND.PORT |
---|---|---|---|---|---|---|
长度 | 1字节 | 1字节 | 1字节 | 1字节 | 1-255字节 | 2字节 |
说明 | SOCKS协议版本,固定0x05 | 响应命令 | 保留字段 | 代理服务器地址类型 | 代理服务器地址 | 代理服务器端口 |
RESPONSE 响应命令:
- 0x00 代理服务器连接目标服务器成功
- 0x01 代理服务器故障
- 0x02 代理服务器规则集不允许连接
- 0x03 网络无法访问
- 0x04 目标服务器无法访问(主机名无效)
- 0x05 连接目标服务器被拒绝
- 0x06 TTL已过期
- 0x07 不支持的命令
- 0x08 不支持的目标服务器地址类型
- 0x09 - 0xFF 未分配
我们响应的服务端地址默认是1,也就是ip v4,地址与端口可以直接就写0x00。
根据SOCKS5协议中命令部分,编写以下代码:
//...
socket.once('data', (data) => {
if (data.length < 7 || data[1] !== 0x01) return socket.destroy(); // 只支持 CONNECT
try {
addrtype = data[3];// ADDRESS_TYPE 目标服务器地址类型
if (addrtype === 3) {//0x03 域名地址(没有打错,就是没有0x02),
addrLen = data[4];//域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组
} else if (addrtype !== 1 && addrtype !== 4) {
return socket.destroy();
}
remotePort = data.readUInt16BE(data.length - 2);//最后两位为端口值
if (addrtype === 1) {// 0x01 IP V4地址
remoteAddr = data.slice(3, 7).join('.');
} else if (addrtype === 4) { //0x04 IP V6地址
return socket.write(Buffer.from([0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));//不支持IP V6
} else {//0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组
remoteAddr = data.slice(5, 5 + addrLen).toString("binary");
}
console.log(`connecting : ${remoteAddr}:${remotePort}`);
} catch (e) {
console.error(e);
}
});
//...
测试效果:
实现代理:
解析成功之后我们就可以实现正式的代理了,
代理部分很简单,创建一个TCP客户端请求,将请求信息转发给解析出来的IP与端口就行了。
//....
let remote = net.connect(remotePort, remoteAddr, function () {
console.log(`connecting : ${remoteAddr}:${remotePort}`);
socket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), function (err) {
if (err) {
console.error(`error:${err.message}`);
return socket.destroy();
}
remote.pipe(socket);
socket.pipe(remote);
});
});
remote.on('error', function (err) {
console.error(`连接到远程服务器 ${remoteAddr}:${remoteAddr} 失败,失败信息:${err.message}`);
remote.destroy();
socket.destroy();
});
//....
效果测试
本人目前在杭州,在自己老家有一台香橙派作为自己平时的测试服务器,
这里我通过远程将服务部署在香橙派上作为测试。
参考资料
- 你也能写个 Shadowsocks
- Socks5代理协议