网上关于TCP内网穿透的软件或是文章挺多的,其中通过TCP端口代理转发可以说是实现穿透最简单的方式了。
注意:文章代码只是原理示例,并没有考虑性能、安全性以及一些异常的处理,请切勿在生产环境中使用。
完整代码在:https://gitee.com/baojuhua/node_simples/tree/master/node_simple_tcp_penetration
1.原理解析
首先了解一下,普通端口代理转发,与内网穿透的端口代理转发的区别
为了方便大家理解,我将IP换成应用,效果如下
整体的效果大概是这个样子的
1.1.具体步骤
首先穿透客户端连接上穿透服务端
TCP连接上后提交校验数据,与一些配置信息
服务端校验成功后,服务端根据配置信息开放对应的端口监听服务
注意:接下来这一步有两种做法
假设服务端有内网客户端访问到某个端口了
我一开始是直接将请求数据通过已经连接好的Socket连接发送给穿透客户端
然后,穿透客户端动态创建访问客户端去连接内网的服务或App
结果我发现,上面这种做法在Socket标识上需要做好多处理,而且可能涉及到修改数据包的操作
于是我改用下面的这种做法
穿透客户端只负责提交配置数据,与接收服务端发送的对应的端口访问请求
穿透客户端接收到请求后,开放两个新的客户端用于双向的数据传输
这样服务端与客户端的数据基本就打通了
2.编写穿透客户连接端程序与穿透服务端监听程序
根据原理编写起来其实就很简单了
注意:我这边为了方便理解,在示例代码中使用了json字符串来传输数据,实际情况一般都是使用字节
2.1.穿透客户端
通过net.createConnection
创建Socket连接,在连接成功后发送相关数据到服务端
代码:
const net = require('net');
// 配置
const CONFIG = {
//一些配置....
};
// 用于校验请求的code
let CODE;
// 创建用于连接校验服务端的 客户端连接
let linkClient = net.createConnection({ port: CONFIG.server_port, host: CONFIG.server_addr }, () => {
// 创建用于校验请求的code
CODE = (+new Date());
// 发送数据校验
linkClient.write(JSON.stringify({ token: CONFIG.token, code: CODE, type: 'register', binds: CONFIG.binds }));
console.log(`[${(new Date()).toLocaleString()}] 正在尝试连接...`);
});
linkClient.setTimeout(CONFIG.timeout);
linkClient.on('data', (data) => {
try {
data = JSON.parse(data);
// 校验请求
if (data.code == CODE) {
if (data.type == 'register') {
console.log(`[${(new Date()).toLocaleString()}] 已连接到服务器 ${CONFIG.server_addr}:${CONFIG.server_port}`);
} else {
// ....
}
return;
}
} catch (error) {
// 异常
}
return linkClient.end();
});
linkClient.on('error', (err) => {
});
linkClient.on('end', () => {
});
2.2.穿透服务端
通过net.createServer
创建端口监听服务
在接收到数据对数据进行校验与绑定处理
const net = require('net');
// 配置
const CONFIG = {
//...
};
// 客户端链接集
let linkClients = {};
// 客户端绑定的应用
let clientBings = {};
// 连接记录
let socketRecords = {};
// 远程监听访问 用于穿透
const listenServer = net.createServer((socket) => {
let id = [socket.remoteAddress, socket.remoteFamily, socket.remotePort].join("_");
console.log(`[${(new Date()).toLocaleString()}] ${socket.remoteAddress}:${socket.remotePort} 上线...`);
if (!linkClients[id]) {
// 保存连接
linkClients[id] = socket;
socket.on('data', (data) => {
try {
data = JSON.parse(data);
let [token, type, code, binds] = [data.token, data.type, data.code, data.binds];
if (token != token) return socket.end();
if (type == 'register') {// 客户端注册
for (let k in binds) {
let d = binds[k];
if (!clientBings[k]) {
[d.code, d.name, d.linkSocket, d.linkId] = [data.code, k, socket, id];
//d.forwardServer = createForwardServer(d);//绑定客户端App对应服务端开放的端口服务
clientBings[k] = d;
}
}
return socket.write(JSON.stringify({ code: code, type: type, id: id }));
}
} catch (error) {
//
removeInfo(id);
}
socket.end();
});
socket.on('error', (err) => {
})
socket.on('end', (data) => {
});
}
});
// 启动服务端监听
listenServer.listen(CONFIG.server_port, CONFIG.server_addr);
3.完善功能
3.1.给穿透客户端添加动态创建Socket连接与异常重试功能
动态创建Socket很简单,就是根据服务端数据new Socket
两个,一个连接服务端,一个用来连接局域网的应用。
而异常重试功能只不过是加了个定时器进行定时状态检查罢了
代码:
const net = require('net');
// 配置
const CONFIG = {
server_addr: "xxx.xxx.xxx.xxx",//服务端地址
server_port: 10001,//服务端端口
token: "hello server",//连接令牌
timeout: 3000,//请求超时时间
interval: 5000,//异常重试
binds: {//绑定内网应用
ssh: {
local_ip: "192.168.199.193",//当前内网被转发的ip
local_port: 22,//当前内网被转发的端口
remote_port: 10002,//服务端映射的端口
},
aria2: {
local_ip: "192.168.199.193",//当前内网被转发的ip
local_port: 6800,//当前内网被转发的端口
remote_port: 10003,//服务端映射的端口
},
aria2Ng: {
local_ip: "127.0.0.1",//当前内网被转发的ip
local_port: 80,//当前内网被转发的端口
remote_port: 10004,//服务端映射的端口
}
}
};
// 用于校验请求的code
let CODE;
let linkClient;
function listenClient() {
// 创建用于连接校验服务端的 客户端连接
linkClient = net.createConnection({ port: CONFIG.server_port, host: CONFIG.server_addr }, () => {
// 创建用于校验请求的code
CODE = (+new Date());
// 发送数据校验
linkClient.write(JSON.stringify({ token: CONFIG.token, code: CODE, type: 'register', binds: CONFIG.binds }));
console.log(`[${(new Date()).toLocaleString()}] 正在尝试连接...`);
});
linkClient.setTimeout(CONFIG.timeout);
linkClient.on('data', (data) => {
try {
data = JSON.parse(data);
// 校验请求
if (data.code == CODE) {
if (data.type == 'register') {
console.log(`[${(new Date()).toLocaleString()}] 已连接到服务器 ${CONFIG.server_addr}:${CONFIG.server_port}`);
} else {
// 请求标识
let key = data.key;
// 应用名称
let name = data.name;
// 本地的应用
let localApp = CONFIG.binds[name];
if (!localApp) linkClient.end();
// 创建服务端用的Socket
let serverClient = new net.Socket();
serverClient.setTimeout(CONFIG.timeout);
// 创建局域网内的Socket
let localClient = new net.Socket();
localClient.setTimeout(CONFIG.timeout);
// 连接服务端
serverClient.connect(CONFIG.server_port, CONFIG.server_addr, function () {
serverClient.write(JSON.stringify({ type: 'connect', key: key }));
// 连接本地服务器
localClient.connect(localApp.local_port, localApp.local_ip, function () {
console.log(`[${(new Date()).toLocaleString()}] [${name}] ${localApp.local_port}<===>${localApp.remote_port}`);
});
// 本地数据转发服务端
localClient.pipe(serverClient);
localClient.on('end', function (data) {
serverClient.end();
});
})
serverClient.on('error', function (err) {
console.error(`[${(new Date()).toLocaleString()}] 访问服务器异常,${err.message}`);
localClient.end();
})
localClient.on('error', function (err) {
console.error(`[${(new Date()).toLocaleString()}] 局域网访问异常,${err.message}`);
serverClient.end();
});
// 服务端数据转发本地
serverClient.pipe(localClient);
serverClient.on('end', function (data) {
localClient.end();
});
}
return;
}
} catch (error) {
// 异常
}
return linkClient.end();
});
linkClient.on('error', (err) => {
console.error(`[${(new Date()).toLocaleString()}] 异常:` + err.message);
});
linkClient.on('end', () => {
console.log(`[${(new Date()).toLocaleString()}] 已从服务器 ${CONFIG.server_port}:${CONFIG.server_port} 断开`);
});
}
listenClient();
//异常重试
setInterval(() => {
if (linkClient.readyState == "closed") {
linkClient.end();
console.log(`[${(new Date()).toLocaleString()}] 正在重新连接服务器...`);
listenClient();
}
}, CONFIG.interval);
3.2.给穿透服务端添加绑定端口转发服务
服务端在Socket管理上,已经数据转发的过程上理解起来比客户端稍微复杂
代码:
const net = require('net');
// 配置
const CONFIG = {
server_addr: "0.0.0.0",//服务端地址 一般是0.0.0.0或127.0.0.1
server_port: 10001,//服务端端口
token: "hello server",//连接令牌
};
// 客户端链接集
let linkClients = {};
// 客户端绑定的应用
let clientBings = {};
// 连接记录
let socketRecords = {};
// 删除相关信息
const removeInfo = function (id) {
if (linkClients[id]) {
linkClients[id] = null;
delete linkClients[id];
}
for (let k in clientBings) {
let d = clientBings[k];
if (id == d.linkId) {
d.forwardServer.close();
clientBings[k] = null;
delete clientBings[k];
}
}
};
// 端口转发服务 用于开放访问
function createForwardServer(conf) {
let forwardServer = net.createServer((socket) => {
// 判断客户是否在线
if (!linkClients[conf.linkId]) return socket.end();
let id = [socket.remoteAddress, socket.remoteFamily, socket.remotePort].join("_");
if (!socketRecords[id]) socketRecords[id] = { bind: socket };
// 发送需要数据 告诉客户端 开启新的连接用于访问
conf.linkSocket.write(JSON.stringify({ key: id, name: conf.name, code: conf.code }));
socket.on('error', () => {
socket.end();
});
socket.on('end', () => {
if (socketRecords[id]) {
if (socketRecords[id].bind) socketRecords[id].client.end();
if (socketRecords[id].client) socketRecords[id].bind.end();
delete socketRecords[id];
}
})
});
forwardServer.listen(conf.remote_port, () => {
console.log(`[${(new Date()).toLocaleString()}] [${conf.name}] 转发服务开启 ${conf.local_port}<===>${conf.remote_port}`);
});
return forwardServer;
}
// 远程监听访问 用于穿透
const listenServer = net.createServer((socket) => {
let id = [socket.remoteAddress, socket.remoteFamily, socket.remotePort].join("_");
console.log(`[${(new Date()).toLocaleString()}] ${socket.remoteAddress}:${socket.remotePort} 上线...`);
if (!linkClients[id]) {
// 保存连接
linkClients[id] = socket;
socket.on('data', (data) => {
try {
try {
data = JSON.parse(data);
} catch (error) {
return;
}
let [token, type, code, binds] = [data.token, data.type, data.code, data.binds];
if (token != token) return socket.end();
if (type == 'register') {// 客户端注册
for (let k in binds) {
let d = binds[k];
if (!clientBings[k]) {
[d.code, d.name, d.linkSocket, d.linkId] = [data.code, k, socket, id];
d.forwardServer = createForwardServer(d);//绑定客户端App对应服务端开放的端口服务
clientBings[k] = d;
}
}
return socket.write(JSON.stringify({ code: code, type: type, id: id }));
} else if (type == 'connect') {// 客户端连接
let id = data.key;
let socketRecord = socketRecords[id];
if (socketRecord && socketRecord.bind && !socketRecord.client) {
socket.firstConnection = false;
socketRecord.client = socket;//设置 客户端连接soket
// 数据转发
socket.pipe(socketRecord.bind);
socketRecord.bind.pipe(socket);
socketRecord.bind.on('end', () => {
socket.end();
});
socketRecord.bind.on('error', (err) => {
socket.end();
});
return;
}
}
} catch (error) {
//
removeInfo(id);
}
socket.end();
});
socket.on('error', (err) => {
//
removeInfo(id);
})
socket.on('end', (data) => {
removeInfo(id);
});
}
});
// 启动服务端监听
listenServer.listen(CONFIG.server_port, CONFIG.server_addr);
4.测试
相关文章:
- Nodejs实现简单的UDP打洞(内网穿透)实验
- NodeJS编写简单TCP/UDP端口代理转发服务