nodejs用ws模块反向代理socks5数据,即用ws写服务端和客户端及加密

news/2025/2/27 9:59:24

首先思路是客户端发送socks5请求数据——>服务端解密并解析socks5数据是否为真,解析出Host和port,并用net.socket访问目标网站,目标网站返回数据,服务端再用ws发送返回数据给客户端

//解析socks5数据和返回socks5格式响应
//在读下面代码时希望你了解socks5的格式

javascript">+----+-----+-------+------+----------+----------+----------+
|0x05|0x01 | 0x00  | 0x03 | 0x09     | baidu.com| 0x00 0x50 |
+----+-----+-------+------+----------+----------+----------+

下面共7个字节+域名字节长度

偏移量				长度				描述
0						1			版本号 (0x05)
1						1			命令 (0x01: CONNECT)
2						1			保留字段 (0x00)
3						1			地址类型 (0x03: 域名)
4						1			域名长度 (1 字节)
5						可变			域名 (根据长度)
5 + 域名长度				2			端口号 (2 字节)

SOCKS5 请求示例
假设客户端请求连接到 baidu.com:80,SOCKS5 请求的格式如下:

javascript">字段			值		说明
版本号		0x05	SOCKS5 版本号。
命令			0x01	CONNECT 命令。
保留字段		0x00	保留字段,必须为 0x00。
地址类型		0x03	DOMAINNAME 地址类型。
域名长度		0x09	域名长度为 9 字节。
域名			baidu.com	目标域名。
端口号		0x0050	目标端口号(80

SOCKS5 响应示例

javascript">+----+-----+-------+------+----------+----------+----------+
|0x05| rep | 0x00  | 0x01 | 0x00 0x00 0x00 0x00 | 0x00 0x00 |
+----+-----+-------+------+----------+----------+----------+

假设请求成功,SOCKS5 响应的格式如下:

javascript">字段		值			说明
版本号	0x05		SOCKS5 版本号。
响应码	0x00		SUCCEEDED,请求成功。
保留字段	0x00		保留字段,必须为 0x00。
地址类型	0x01		IP_V4_ADDR 地址类型。
绑定地址	0x00000000	绑定地址(通常为 0.0.0.0)。
绑定端口	0x0000		绑定端口(通常为 0)。

//socks.js代码

javascript">'use strict';

const util=require('util');
//socks5的版本号永远是0x05,即5;
const VERSION=5;
//SOCKS5 支持多种认证方法,客户端和服务端在握手阶段协商使用哪种认证方法。
const AUTH={
    NO_AUTH: 0,	//不需要认证。
    GSSAPI: 1,	//使用 GSSAPI(通用安全服务应用程序接口)进行认证。
    USERNAME_PASSWD: 2,	//使用用户名和密码进行认证。
    IANA_ASSIGNED: 3, // 0x03 to 0x7f 	IANA 分配的认证方法
    RESERVED: 0x80, // 0x80 to 0xfe	保留的认证方法(0x80 到 0xFE)
    NO_ACCEPTABLE_METHODS: 0xff	//没有可接受的认证方法
};
//SOCKS5 客户端可以发送以下命令,指示代理服务器执行的操作
const CMD={
    CONNECT:1,	//建立 TCP 连接。通常用于 HTTP、HTTPS 等协议
    BIND:2,		//绑定端口。通常用于 FTP 等需要反向连接的协议。
    UDP_ASSOCIATE:3	//建立 UDP 关联。用于 UDP 协议(如 DNS、QUIC)
};
//SOCKS5 支持多种目标地址类型
const ATYP={
    IP_V4_ADDR:1,	//ip4
    DOMAINNAME:3,	//域名
    IP_V6_ADDR:4	//ip6
};

//SOCKS5 服务器在处理客户端请求后,会返回一个响应码,表示请求的处理结果。
const REP={
    SUCCEEDED:0,	//请求成功。
    SOCKS_SERV_FAILURE:1,	//SOCKS 服务器故障。
    CONN_NOT_ALLOWED:2,		//连接不被允许(例如,目标地址被禁止)
    NETWORK_UNREACHABLE: 3,	//网络不可达
    HOST_UNREACHABLE: 4,	//主机不可达
    CONN_REFUSED: 5,		//连接被拒绝
    TTL_EXPIRED: 6,			//TTL(生存时间)已过期
    CMD_NOT_SURPPORTED: 7,	//命令不支持
    ATYP_NOT_SUPPORTED: 8	//地址类型不支持
};

//解析客户端发送过来的网址请求,是socks5格式的请求
//最后最到host和port,并检查host是域名|ip_V4|ip_V6
function parseRequest(data)
{
    let host,port;
    if(data.length<8)
    {
        return {ok:false};
    }
    let buf=Buffer.from(data);
    let ver=buf.readUInt8(0);
    let cmd=buf.readUInt8(1);
    
    if(ver !=VERSION || cmd != CMD.CONNECT)
    {
        
        return{ok:false};
        //return {ok:false,msg:generateReply(REP.CMD_NOT_SURPPORTED)};
    }

    let atyp=buf.readUInt8(3);
    switch(atyp)
    {
        case ATYP.DOMAINNAME:
            let alen=buf.readUInt8(4);
            host=buf.toString('utf8',5,5+alen);
            port=buf.readUInt16BE(5+alen);
            break;
        case ATYP.IP_V4_ADDR:
            if(data.length<10)
                {
                    
                    return {ok:false,msg:generateReply(REP.CONN_NOT_ALLOWED)};
                }    
            host=util.format('%d.%d.%d.%d',buf[4],buf[5],buf[6],buf[7]);
            port=buf.readUInt16BE(8);
            break;
        case ATYP.IP_V6_ADDR:
            if(data.length<22)
                {
                   
                    return {ok:false,msg:generateReply(REP.CONN_NOT_ALLOWED)}
                } 
            host=util.format('%s:%s:%s:%s:%s:%s:%s:%s', buf.toString('hex',4,6), buf.toString('hex',6,8), buf.toString('hex',8,10), buf.toString('hex',10,12), buf.toString('hex',12,14), buf.toString('hex',14,16), buf.toString('hex',16,18), buf.toString('hex',18,20));
            port=buf.readUInt16BE(20);
            break;
        default:
            
            return {ok:false,msg:generateReply(REP.ATYP_NOT_SUPPORTED)};           
    }
    return {ok:true,host:host,port:port};
}


//返回一个socks5响应,10个字节,可看上面的响应格式
function generateReply(rep=REP.SUCCEEDED)
{
    var rep=Buffer.alloc(10);
    rep.writeUInt8(VERSION);
    rep.writeUInt8(rep,1);
    rep.writeUInt8(ATYP.IP_V4_ADDR,3);
    return rep;
}

function parseNegotiation(data)
{
    if(data.length<3)
    {
        return {ok:false};
    }
    let arr=Buffer.from(data);
    let ver=arr.readUInt8(0);
    let n=arr.readUInt8(1);
    let method=arr.readUInt8(2);
    if(ver != VERSION || n <1 || method!=AUTH.NO_AUTH)
    {
        return {ok:false,msg:Buffer.from([VERSION,AUTH.NO_ACCEPTABLE_METHODS])};
    }
    return {ok:true,msg:Buffer.from([VERSION,AUTH.NO_AUTH])};
}

module.exports={
    parseNegotiation:parseNegotiation,
    generateReply:generateReply,
    parseRequest:parseRequest
};

//服务端代码

javascript">'use strict';
const net=require('net');
const dns=require('dns');
const util=require("util");
const WebSocket=require('ws');
const Cipher=require('./cipher');
const socks=require("./socks");

const TIMEOUT=60000;
const TIMEWAIT=10000;


class Server{
    constructor(options){
        this.key=options.key;
        this.host=options.host;
        this.port=options.port;
        //创建ws服务端
        this.server=new WebSocket.Server({host:options.host,port:options.port});

        this.server.on('error',err=>{
            console.log(err.toString());
        });

        this.server.on('close',()=>{
            console.log('server close');
            this.server.clients.forEach(function each(client){
                if(client && client.readyState===WebSocket.OPEN)
                {
                    client.destroy();
                }
            });
        });    
    }

//运行服务端
    run(){
        const key=this.key;
        const port=this.port;

        this.server.on('connection',(ws,req)=>{
        	//连接到服务端的ip和端口,就是用户的IP和端口
            const peer=req.socket.remoteAddress+":"+req.socket.remotePort;

            console.log('%s -- :%d $ connected',peer,port);
            //创建加密
            let cipher=new Cipher(key);
			//接收到客户端发送过来的请求
            ws.on('message',msg=>{
                console.log('%s -> [%d] :%d',peer,msg.length,this.port);
                let raw;
                try{
                	//解密请求信息
                    raw=cipher.decrypt(msg);
                    
                    console.log(raw);
                }catch(e)
                {
                    console.log(e.toString());
                    setTimeout(()=>ws.close(),Math.random()+TIMEOUT);
                    return;
                }

                //解析SOCKS5请求
                let res=socks.parseRequest(raw);
                
                
                if(!res.ok)
                {
                    console.log('Invalid socks5 request');
                    ws.close();
                    return;
                }
                //解析出客户端请求中的host和端口,就是你要访问的网站的域名和端口
                const {host,port}=res;
                

                //与目标服务器建立连接,就是你请求域名的目标服务器
                const targetSocket=new net.Socket();
                //和目标服务器建立连接,也就是你要的访问网站建立连接
                targetSocket.connect(port,host,()=>{
                    console.log('Connected to target server:',host,port);

                    
                    //返回SOCKS5成功响应,这里要先给客户端返回一个socks5格式数据表示已经和目标网站建立连接
                    const response=socks.generateReply();
                    
                    ws.send(response);

                    //这里收到了客户端返回的数据
                    //也就是这段httpRequest='GET / HTTP/1.1\r\nHost: baidu.com\r\nConnection: close\r\n\r\n';
                    //让targatSocket把这个请求头发给百度的服务器
                    ws.on('message',(data)=>{
                        console.log('这里是转发数据:',data.toString());
                        //这里发给你请求的网站的服务器,如现在请求的百度
                        targetSocket.write(data);
                    });

                    //这里是百度服务器返回的数据
                    targetSocket.on('data',data=>{
                        console.log('这接收到targetSocket过来的数据:',data);
                        //然后转发给我们的客户端
                        ws.send(data);
                    });
                    
                });

                targetSocket.on('error',err=>{
                    console.log('Target server connection error:',err);
                    ws.close();
                });

                targetSocket.on('close',()=>{
                    console.log('Target server connection close:',close);
                    ws.close();
                })

                
            })

            ws.on('close',()=>{
                console.log('%s -- disconnected.',peer);
            });
            ws.on('error',err=>{
                console.error('WebSocket error: ',err);
            });
        })
    }

    
}

const options={
    key:"metadata",
    host:'localhost',
    port:8000
};

let s=new Server(options);
s.run();

//接着是客户端

javascript">
const WebSocket=require('ws');
const Cipher=require("./cipher");

//SOCKS5协议常量
const VERSION=0x05; //socks5版本号
const CMD={
    CONNECT:0x01,   //CONNECT命令
};
const ATYP={
    DOMAINNAME:0x03,    //域名
};

//目录服务器信息
const TARGET_HOST='www.baidu.com'; //目标主机
const TARGET_PORT=80; //目标端口

//创建SOCKS5请求
//这里是硬创建的数据写入内存中,现实中应该用上面的协议常量
function createSocks5Request(host, port) {
    const hostLength = Buffer.byteLength(host);
    const buffer = Buffer.alloc(7 + hostLength);

    // 写入版本号
    buffer.writeUInt8(0x05, 0); // 版本号必须是 0x05

    // 写入命令 (CONNECT)
    buffer.writeUInt8(0x01, 1); // 命令必须是 0x01 (CONNECT)

    // 写入保留字段
    buffer.writeUInt8(0x00, 2); // 保留字段必须是 0x00

    // 写入地址类型 (域名)
    buffer.writeUInt8(0x03, 3); // 地址类型必须是 0x03 (域名)

    // 写入域名长度
    buffer.writeUInt8(hostLength, 4); // 域名长度

    // 写入域名
    buffer.write(host, 5); // 域名

    // 写入端口号
    buffer.writeUInt16BE(port, 5 + hostLength); // 端口号

    return buffer;
}

//连接到服务器
const ws=new WebSocket('ws://localhost:8000');
//加密认证
const cipher=new Cipher("metadata");
ws.on('open',()=>{
    console.log('Connected to WebSocket server');

    //发送SOCKS5请求
    const request=createSocks5Request(TARGET_HOST,TARGET_PORT);
    
    ws.send(cipher.encrypt(request));
});

ws.on('message',data=>{
    //console.log('接收来自服务端数据:',data.toString());

    //如果SOCKS5响应,发送HTTP请求
    //SOCKS5响应为10字节,因为在socks.js中的generateReply()生成的就是10个字节
    if(data.length===10)
    {
        const httpRequest='GET / HTTP/1.1\r\nHost: baidu.com\r\nConnection: close\r\n\r\n';
        ws.send(httpRequest);
    }else
    {
        //打印目标服务器返回的数据,这里返回的就是网站数据即html格式的数据 
        console.log('Respnonse from target server: ',data.toString());
    }
});

ws.on('close',()=>{
    console.log('Connection closed');
});
ws.on('error',(err)=>{
    console.error('WebSocket error: ',err);
});

实现客户端与目标服务器之间的双向数据转发。

这里没有贴出加密的代码,这个不难你们可以自己现实一个加密即可,也可以不加密,只是测试用

  1. 数据转发的工作流程
    客户端发送数据:

客户端通过 WebSocket 发送数据(如 HTTP 请求)。

ws.on(‘message’, …) 捕获数据,并通过 targetSocket.write(data) 将数据转发给目标服务器。

目标服务器返回数据:

目标服务器处理请求后,返回数据(如 HTTP 响应)。

targetSocket.on(‘data’, …) 捕获数据,并通过 ws.send(data) 将数据转发给客户端。

双向通信:

通过这两个事件监听器,客户端与目标服务器之间的通信可以实现双向转发。

  1. 示例场景
    假设客户端通过 SOCKS5 代理访问 baidu.com:

客户端发送 SOCKS5 请求:

客户端发送 SOCKS5 请求到代理服务器,请求连接到 baidu.com:80。

代理服务器建立连接:

代理服务器解析 SOCKS5 请求,并与 baidu.com:80 建立 TCP 连接。

代理服务器返回 SOCKS5 响应:

代理服务器生成 SOCKS5 响应(response),并通过 ws.send(response) 发送给客户端。

客户端发送 HTTP 请求:

客户端发送 HTTP 请求(如 GET / HTTP/1.1)到代理服务器。

代理服务器通过 ws.on(‘message’, …) 捕获请求,并通过 targetSocket.write(data) 将请求转发给 baidu.com。

目标服务器返回 HTTP 响应:

baidu.com 返回 HTTP 响应。

代理服务器通过 targetSocket.on(‘data’, …) 捕获响应,并通过 ws.send(data) 将响应转发给客户端。

  1. 总结
    const response = socks.generateReply();:生成 SOCKS5 响应,表示连接成功。

ws.send(response);:将 SOCKS5 响应发送给客户端。

数据转发部分:

ws.on(‘message’, …):将客户端的数据转发给目标服务器。

targetSocket.on(‘data’, …):将目标服务器的数据转发给客户端。


http://www.niftyadmin.cn/n/5869918.html

相关文章

Spring Boot @Component注解介绍

Component 是 Spring 中的一个核心注解&#xff0c;用于声明一个类为 Spring 管理的组件&#xff08;Bean&#xff09;。它是一个通用的注解&#xff0c;可以用于任何层次的类&#xff08;如服务层、控制器层、持久层等&#xff09;。通过 Component 注解&#xff0c;Spring 会…

C#开发的Base64编码及解码完整源码及注意事项

在软件开发时&#xff0c;经常用Base64编码和解码功能。本文介绍一个简单易用的Base64 编码和解码工具&#xff0c;顾名思义&#xff0c;就是简单快捷地进行 Base64 代码的解码或编码操作。您的数据可以轻松地编码为 Base64 编码&#xff0c;也可以解码为可读的格式。传输数据时…

《论微服务架构及其应用》审题技巧 - 系统架构设计师

论微服务架构及其应用写作框架 一、考点概述 本论题“论微服务架构及其应用”主要考查考生对微服务架构的理解、应用经验以及软件设计实现能力。微服务架构作为当前软件开发领域的重要趋势&#xff0c;其核心思想是将单一应用程序拆分成一组小型、自治的服务&#xff0c;每个…

克隆项目到本地

1、前置 Git安装配置超详细教程_git安装及配置教程-CSDN博客 git clone ssh-key, 挑重点 - 知子 - 博客园 (cnblogs.com) 2、用 git clone ① 在资源管理器中&#xff0c;找到项目要下载的位置 ② 空白处 右键 git bash here ③ 输入指令 git clone -b dev ssh://git58.49…

医院HIS接入大模型:算力基础设施与训练能力的深度剖析与测算

一、引言 1.1 研究背景与意义 在数字化医疗快速发展的当下,医院信息系统(Hospital Information System,HIS)作为医疗信息化的核心枢纽,承载着患者诊疗信息、医院运营管理等关键数据 ,对提升医疗服务质量、优化医院管理流程起着至关重要的作用。然而,传统 HIS 在面对日…

HDFS扩缩容及数据迁移

1.黑白名单机制 在HDFS中可以通过黑名单、白名单机制进行节点管理&#xff0c;决定数据可以复制/不可以复制到哪些节点。 黑名单通常是指在HDFS中被标记为不可用或不可访问的节点列表&#xff0c;这些节点可能由于硬件故障、网络问题或其他原因而暂时或永久性地无法使用。当一…

jmeter聚合报告如何添加单位_性能测试连载(8)jmeter压力测试中的难点解析

概述 新人在用jmeter做压力测试的时候&#xff0c;会被一些性能术语搞懵&#xff0c;直接导致的后果就是对测试出来的结果数据根本不能理解&#xff0c;更谈不上分析。这篇文章着重给大家实例解释一下jmeter压力测试的一些专有名词 问题1&#xff1a;什么是压力测试 问到如何做…

.net websocket 使用

系列文章目录 目录 文章目录 前言 一、websocket是什么&#xff1f; 一、工作原理 二、特点与优势 三、应用场景 四、缺点与挑战 二、使用步骤 1.引入库 2.如何使用 总结 文章目录 websocket 在.net中使用前言一、websocket 是什么&#xff1f;二、使用步骤 1.引入…