Nodejs实现简单的TCP内网穿透

网上关于TCP内网穿透的软件或是文章挺多的,其中通过TCP端口代理转发可以说是实现穿透最简单的方式了。

注意:文章代码只是原理示例,并没有考虑性能、安全性以及一些异常的处理,请切勿在生产环境中使用。

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

1.原理解析

首先了解一下,普通端口代理转发,与内网穿透的端口代理转发的区别


Nodejs实现简单的TCP内网穿透_第1张图片
端口转发
Nodejs实现简单的TCP内网穿透_第2张图片
内网穿透

为了方便大家理解,我将IP换成应用,效果如下

Nodejs实现简单的TCP内网穿透_第3张图片
内网穿透

整体的效果大概是这个样子的

Nodejs实现简单的TCP内网穿透_第4张图片
内网穿透3

1.1.具体步骤

首先穿透客户端连接上穿透服务端

TCP连接上后提交校验数据,与一些配置信息

Nodejs实现简单的TCP内网穿透_第5张图片

服务端校验成功后,服务端根据配置信息开放对应的端口监听服务

Nodejs实现简单的TCP内网穿透_第6张图片

注意:接下来这一步有两种做法

假设服务端有内网客户端访问到某个端口了

我一开始是直接将请求数据通过已经连接好的Socket连接发送给穿透客户端


Nodejs实现简单的TCP内网穿透_第7张图片

然后,穿透客户端动态创建访问客户端去连接内网的服务或App


Nodejs实现简单的TCP内网穿透_第8张图片

结果我发现,上面这种做法在Socket标识上需要做好多处理,而且可能涉及到修改数据包的操作

于是我改用下面的这种做法

穿透客户端只负责提交配置数据,与接收服务端发送的对应的端口访问请求


Nodejs实现简单的TCP内网穿透_第9张图片

穿透客户端接收到请求后,开放两个新的客户端用于双向的数据传输


Nodejs实现简单的TCP内网穿透_第10张图片

这样服务端与客户端的数据基本就打通了

Nodejs实现简单的TCP内网穿透_第11张图片

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实现简单的TCP内网穿透_第12张图片

相关文章:

  • Nodejs实现简单的UDP打洞(内网穿透)实验
  • NodeJS编写简单TCP/UDP端口代理转发服务

你可能感兴趣的:(Nodejs实现简单的TCP内网穿透)