6. 初版代码实现
话不多说,直接上完整代码+注释:
const net = require('net');
const port = 4000;
const STATE = {
INIT: 0,
SENT_FIRST_HANDSHAKE_RESPONSE: 1,
ERR: -1,
};
const server = net.createServer((socket) => {
let state = STATE.INIT;
const closeSocket = () => {
socket.destroy();
state = STATE.ERR;
};
socket.on('data', (data) => {
switch (state) {
case STATE.INIT: // 初始状态,此时收到的 data 为 client 发过来的握手数据
const version = data.readUInt8(0); // 第 0 个字节为 version
if (version !== 5) {
closeSocket();
return;
}
socket.write(Buffer.from([0x05, 0x00])); // 返回 0x0500,表示不需要认证
state = STATE.SENT_FIRST_HANDSHAKE_RESPONSE;
break;
case STATE.SENT_FIRST_HANDSHAKE_RESPONSE: // 此时收到的 data 为 client 发过来的包含 CMD,目标地址等信息的连接请求
const cmd = data.readUInt8(1);
if (cmd !== 1) {
// 不为 CONNECT
Buffer.from([
0x05, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]); // 发送给 client,不支持当前的 CMD,连接失败
closeSocket();
return;
}
// 读取 target server 的地址和端口
let addrType = data.readUInt8(3);
let addrLength, remoteAddr, remotePort;
if (addrType === 4) {
// 不支持 ipv6
socket.write(
Buffer.from([
0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
])
);
closeSocket();
return;
}
if (addrType === 3) {
// domain
addrLength = data.readUInt8(4);
remoteAddr = data.slice(5, 5 + addrLength).toString();
remotePort = data.readUInt16BE(data.length - 2);
} else if (addrType === 1) {
// ipv4
remoteAddr = data.slice(4, 8).join('.');
remotePort = data.readUInt16BE(data.length - 2);
}
// 连接 target server
const remote = net.createConnection(
{ host: remoteAddr, port: remotePort },
() => {
socket.write(
Buffer.from([
0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
])
); // 发送给 client,表示连接成功了
state = STATE.SENT_CONNECT_RESPONSE;
console.log(`connecting ${remoteAddr}:${remotePort}`);
// 最关键的两行代码:将两个 socket pipe 起来,从而任意一方读取的数据会写入到另一方,实现转发过程
socket.pipe(remote);
remote.pipe(socket);
}
);
remote.on('error', (err) => {
remote.destroy();
closeSocket();
});
break;
}
});
socket.on('error', (err) => {
console.log('socket error', err.message);
});
});
server.listen(port, () => {
console.log(`socks5 proxy server is listening on port ${port}`);
});
将第 5 章提到的 socks5 交互过程翻译成代码就行了。其中最核心的两行就是
socket.pipe(remote);
remote.pipe(socket);
约等于
socket.on('data', data => remote.write(data));
remote.on('data', data => socket.write(data));
也是转发数据的关键部分。
和 http server 不同的是,tcp server 数据交互都是 Buffer,所以从 data 中获取数据,就需要用到 Buffer instance 中 read 相关方法。
将该服务器在本地运行,然后将代理设置为socks5 + 127.0.0.1:4000。可以在系统中设置,也可以使用一些浏览器代理插件(比如 SwitchyOmega )。