NodeJS实现简单的Socks5代理服务

前言

最近在网上看到一篇文章《你也能写个 Shadowsocks》,于是处于好奇就了解到了SOCKS5协议。

这里我就根据SOCKS5协议,通过NodeJS实现一个最简单的,没有加密,不需要认证Socks5代理服务。

我相信大家都听过Socket,而Socks5协议是一种网络传输协议,常用过代理,之前很有名的shadow socks也是基于此协议实现的。这里大家单词可不要看错了。

完整代码:https://gitee.com/baojuhua/node_simples/tree/master/node_simple_socks5_server

编写TCP服务

我们使用NodeJSnet模块编写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();
});
//....
效果测试

本人目前在杭州,在自己老家有一台香橙派作为自己平时的测试服务器,

这里我通过远程将服务部署在香橙派上作为测试。

参考资料

推荐阅读更多精彩内容