sock5代理node实现(3)

在前两篇两次握手信息之后,我们这次终于可以到获取数据这一步了。

在客户端收到我们上一篇数据的密码验证成功的信息后,客户端会向服务端发出需要访问的地址,它可能是IP(Ipv4或者Ipv6)和端口号,也有可能是域名和端口号。

具体的数据格式类似下面

+—-+—–+——-+——+———-+———-+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+—-+—–+——-+——+———-+———-+
| 1 | 1 | X’00’ | 1 | Variable | 2 |
+—-+—–+——-+——+———-+———-+

第一个参数VER表示使用的协议版本号

第二个参数CMD表示需要使用的链接方式

  • 0x01 表示CONNECT,比如我们常见的http请求
  • 0x02 表示BIND,比如FTP等请求
  • 0x03 表示UDP。

第三个参数RSV表示单词RESERVED,也就是字面意思保留字段,暂时没有用

第四个参数ATYP表示address type,就是使用的地址类型

  • 0x01 IP V4地址
  • 0x03 域名
  • 0x04 IP V6地址

第五个参数DST.ADDR,表示目标主机的地址

第六个参数DST.PORT,表示目标主机的端口

在我们拿到地址和端口后,我们可以尝试连接目标主机,在我们连接成功后,返回给客户端类似下面的数据

+—-+—–+——-+——+———-+———-+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+—-+—–+——-+——+———-+———-+
| 1 | 1 | X’00’ | 1 | Variable | 2 |
+—-+—–+——-+——+———-+———-+

字段内容和上面基本一样BND.ADDR就是DST.ADDR,BND.PORT就是DST.PORT

唯一不同的就是REP,这个表示服务端去连接目标主机的结果

  • X’00’ 成功
  • X’01’ 普通的SOCKS服务器请求失败
  • X’02’ 现有的规则不允许的连接
  • X’03’ 网络不可达
  • X’04’ 主机不可达
  • X’05’ 连接被拒
  • X’06’ TTL超时
  • X’07’ 不支持的命令
  • X’08’ 不支持的地址类型
  • X’09’ – X’FF’ 未定义

客户端接下来就会把原来的请求完整的发给服务端,服务端直接转发给目标主机就好,其实在我们服务端和客服端相互认证并且服务端与目标主机连接上以后,服务端接下来的任务就相当于在客户端与目标主机充当搬运工的职责,只是负责包客户端的请求内容一样的发给目标主机,再把目标主机的内容发给客户端。

下面是对应的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

const net = require('net');
// 用来解析地址信息
const ReqInfo = require('./reqinfo.js');

async function receiveRequest(chunk) {
let socket = this;
// 获取到地址信息
let reqinfo = await ReqInfo(chunk);

let resp = new Buffer(chunk.length);
// 上面提到的两个数据格式基本一样,只是有一个字段不一样而已,所以为了方便就复制一份
chunk.copy(resp);
resp[0] = 0x05;

// 不代理本地地址
if (reqinfo.dest === '127.0.0.1' || reqinfo.dest === 'localhost') {
resp[1] = 0x02;
socket.write(resp);
return false;
}

try {
// 尝试连接目标主句
let req = net.createConnection(reqinfo.port, reqinfo.dest, (s) => {
log('createConnection success');
// 要在保证连接上目标服务器返回成功数据
resp[1] = 0x00;

socket.write(resp);
// 在客户端接受到连接上服务器的信息,则会发送完整的请求内容,直接转发给req
socket.pipe(req);
req.pipe(socket);
});
// 设置连接目标主机的超时时间
req.setTimeout(60 * 1000);
// 连接超时
req.on('timeout', () => {
resp[1] = 0x06;
socket.write(resp);
req.destroyed || req.destroy();
});
// 连接目标主机失败
req.on('error', () => {
resp[1] = 0x05;
socket.write(resp);
});
} catch (e) {
console.error('未捕捉错误: net.createConnection');
socket.emit('error', e);
}
}

module.exports = receiveRequest;

细心的人肯定看到了,我这边使用了asyncawait,主要是在解析目标主机地址时用到了,主要原因是因为我在写这段代码,调试的时候不知道为什么google就是连接不上,其他的网站都可以,后来发现原来,我所在的地方DNS被劫持了,google.com域名返回的是百度的ip,结果因为https的安全问题(相当于给百度发了google.com的证书),导致一直访问错误。

所以就把DNS放在服务端了,DNS的解析是异步的,所以这里使用asyncawait来解决异步的问题。不过这样会增加服务端的压力,不过这个设置可以在客户端配置。另外肯定也看到了,我这里并没有贴出const ReqInfo = require('./reqinfo.js');的具体代码,具体代码请移步github socks-proxy看吧,这里就不具体解释了。

至此socks5代理的服务端就基本完成了,当然这个只是一个很简单的版本,还需要很多的异常需要处理,仅供娱乐吧。

sock5代理node实现(1)
sock5代理node实现(2)