首先思路是客户端发送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);
});
实现客户端与目标服务器之间的双向数据转发。
这里没有贴出加密的代码,这个不难你们可以自己现实一个加密即可,也可以不加密,只是测试用
- 数据转发的工作流程
客户端发送数据:
客户端通过 WebSocket 发送数据(如 HTTP 请求)。
ws.on(‘message’, …) 捕获数据,并通过 targetSocket.write(data) 将数据转发给目标服务器。
目标服务器返回数据:
目标服务器处理请求后,返回数据(如 HTTP 响应)。
targetSocket.on(‘data’, …) 捕获数据,并通过 ws.send(data) 将数据转发给客户端。
双向通信:
通过这两个事件监听器,客户端与目标服务器之间的通信可以实现双向转发。
- 示例场景
假设客户端通过 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) 将响应转发给客户端。
- 总结
const response = socks.generateReply();:生成 SOCKS5 响应,表示连接成功。
ws.send(response);:将 SOCKS5 响应发送给客户端。
数据转发部分:
ws.on(‘message’, …):将客户端的数据转发给目标服务器。
targetSocket.on(‘data’, …):将目标服务器的数据转发给客户端。