Node.js实现内网穿透

如何在办公室外调用办公室内一台服务器上部署的服务?

如果办公室有固定的IP地址,而且可以映射到内网的机器,那么这个问题很简单。如果无法通过这种方式访问内网的机器,我们还可以通过内网穿透的办法来解决。

基本的思路是,需要有一台公网的服务器,服务器上运行一个程序,这个程序监听两个端口,分别供外部客户端和内网客户端连接,然后这个程序负责转发双方的通信给对方。

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const net = require('net');

let inSocket, outSocket;
const inServer = net.createServer(function(socket) {
inSocket = socket;
});

inServer.listen(4001);

const outServer = net.createServer(function(socket) {
outSocket = socket;
if (inSocket && outSocket) {
inSocket.pipe(outSocket);
outSocket.pipe(inSocket);
}
});

outServer.listen(4002);

in_client.js 内网的客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const net = require('net');

const socket = net.connect({ port: 4001 }, () => {

});

socket.on('data', (data) => {
console.log(data.toString());
socket.write('Data from in office');
});

socket.on('end', () => {
console.log('on end');
});

out_client.js 外网的客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
const net = require('net');

const socket = net.connect({ port: 4002 }, () => {
socket.write('Data from outside');
});

socket.on('data', (data) => {
console.log(data.toString());
});

socket.on('end', () => {
console.log('on end');
});

先运行server.js,然后运行in_client.js,再运行out_client.js

然后我们可以看到in_client.js和out_client.js互相可以通过server.js通信

这里in_client.js需要先运行,然后再运行out_client.js。类似于我们需要先在内网机器上运行teamviewer,然后再在外部运行teamviewer。

然后我们就可以扩展in_client和out_client的功能,例如当out_client发送特定的命令时,in_client就传输本地的屏幕和键盘事件给对方,这样就能实现类似teamviewer的远程桌面。

回到我们开始的问题,如果内网机器上部署了一个service,如何通过out_client访问到呢?

我们可以在in_client启动时就连接要请求的服务,然后等待外部的访问

1
2
3
4
5
6
7
8
9
10
const net = require('net');

let serviceSocket, socket;

socket = net.connect({ port: 4001 }); // 连接公网服务器

serviceSocket = net.connect({ port: 8080 }); // 假设在http://localhost:8080有一个服务

socket.pipe(serviceSocket); // 从服务器转发过来的数据都会发送给service
serviceSocket.pipe(socket); // service返回的数据都会转发给socket,进而转发到服务器

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const net = require('net');

let inSocket, outSocket;
const inServer = net.createServer(function(socket) {
inSocket = socket;
});

inServer.listen(4001);

const outServer = net.createServer(function(socket) {
outSocket = socket;
if (inSocket && outSocket) {
inSocket.pipe(outSocket);
outSocket.pipe(inSocket);
}
});

outServer.listen(4002);

这时我们访问http://localhost:4002,我们的请求就会被转发到http://localhost:8080

仍然有一个问题是,http请求完成以后,客户端过一段时间以后会把底层的TCP连接关掉,那么我们在server上建立的,inSocket和outSocket之间的pipe就断掉了。

解决这个问题的思路是,在server上再添加一个listen的socket,这个socket专门用来和in_client通信,告诉in_client有新的连接请求,in_client发现有新的连接请求以后,再和server建立一个用于转发tcp的连接,这个连接可以在通信完就关闭掉。

我们通过这个新建立的socket发送控制命令给in_client,告诉它有新的连接了,所以我们把这个socket叫controlSocket

server.js

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
const net = require('net');

let inSocket, outSocket, controlSocket;

// 这里我们添加了controlServer
const controlServer = net.createServer();
controlServer.on('connection', (socket) => {
controlSocket = socket;
});
controlServer.listen(4003);

const inServer = net.createServer();
inServer.on('connection', function(socket) {
inSocket = socket;
// 这里我们先建立了外部的socket,然后才通过控制socket
// 告诉in_client建立内部的socket,所以把pipe的代码放在这里
if (inSocket && outSocket) {
inSocket.pipe(outSocket);
outSocket.pipe(inSocket);
}
});
inServer.listen(4001);

const outServer = net.createServer();
outServer.on('connection', function(socket) {
outSocket = socket;
controlSocket.write('new_conn_cmd');
});
outServer.listen(4002);

in_client.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const net = require('net');

let serviceSocket, outSocket, controlSocket;

controlSocket = net.connect({ port: 4003 });

controlSocket.on('data', (data) => {
// 这里没有处理命令的长度
data = data.toString();
if (data === 'new_conn_cmd') {
outSocket = net.connect({ port: 4001 });
serviceSocket = net.connect({ port: 8080 });
outSocket.pipe(serviceSocket);
serviceSocket.pipe(outSocket);
} else {
console.log('Invalid commadn');
}
});

这时我们再请求http://127.0.0.1:4002/,等一段时间,连接会被断掉,下一次请求的时候会建立新的连接。因为http协议会复用底层的连接,短时间内连续的请求不会建立新的连接