当前位置: 首页 > 后端技术 > Node.js

使用nodejs实现socks5协议

时间:2023-04-03 21:07:46 Node.js

本文出处https://shenyifengtk.github.io/如转载请注明出处外网服务器中传。当防火墙后面的客户端想要访问外部服务器时,它会与SOCKS代理服务器连接。这个代理服务器控制着客户端访问外网的资格,如果允许,就把客户端的请求发送给外部服务器。按照OSI模型,SOCKS是会话层的协议,位于表现层和传输层之间,也就是说,socks是TCP之上的协议。与HTTP代理相比,HTTP代理只能代理http请求。像TCP和HTTPS这样的协议非常弱并且有一定的局限性。SOCKS的工作级别低于HTTP代理:SOCKS使用握手协议通知代理软件其客户端正在尝试建立连接,然后SOCKS尽可能透明地进行连接,而常规代理可能会解释和重写标头(例如,使用另一个底层协议,例如FTP;但是,HTTP代理只是将HTTP请求转发到所需的HTTP服务器)。尽管HTTP代理有不同的使用模型,但是CONNECT方法允许转发TCP连接;但是,SOCKS代理也可以转发UDP流量和反向代理,而HTTP代理则不能。HTTP代理通常更了解HTTP协议,执行更高级别的过滤(尽管通常仅针对GET和POST方法,而不针对CONNECT方法)。SOCKS协议内容官方协议RFC选择认证方式一般来说,socks连接过程,首先客户端向socks代理发送数据包VarNMETHODSMETHODS110-255表中单位表示位数var表示SOCK版本,应该为5;NMETHODS表示METHODS部分METHODS的长度表示客户端支持的认证方法列表,每个方法占用1个字节。当前定义为0x00不需要认证0x01GSSAPI0x02用户名,密码认证0x03-0x7FIANA分配(保留)0x80-0xFE为私有方法保留0xFF无可接受方法服务器将响应客户端VERMETHOD11Var表示它是一个SOCK版本,应该是5;METHOD是服务器选择的方法,这个的值是上面的METHODS列表之一。如果客户端支持0x00、0x01、0x02,这三种方法。服务器只会选择一种认证方式返回给客户端。如果返回0xFF,表示没有选择认证方式,客户端需要关闭连接。我们先用一个简单的Nodejs来实现sock连接握手。检查客户端是否发送数据报constnet=require('net');letserver=net.createServer(sock=>{sock.once('data',(data)=>{console.log(data);});});server.listen(8888,'localhost');使用curl工具连接nodejscurl-xsocks5://localhost:8888https://www.baidu.comconsoleOutput使用账号密码认证当服务端选择0x02账号密码认证时,客户端开始发送账号和密码,数据包格式如下:(以字节为单位)VERULENUNAMEPLENPASSWD111~25511~255VER为SOCKS版本ULEN用户名长度UNAME账号字符串PLEN密码长度PASSWD密码字符串可以看出账号密码为明文传输,非常不安全。服务器端验证完成后,会响应如下数据():VERSTATUS11STATUS0x00表示成功,0x01表示失败,客户端在封装请求验证完成后可以发送请求信息。客户端开始封装请求信息SOCKS5请求格式(字节):VERCMDRSVATYPDST.ADDRDST.PORT110x001动态2VER为SOCKS版本,这里应该为0x05;CMD是SOCK命令码0x01表示CONNECT请求CONNECT请求可以打开一个客户端A通道,与请求的资源进行双向通信。它可用于创建隧道。例如,CONNECT可用于访问使用SSL的服务器,SSL是一种标准协议,可确保在两台计算机应用程序之间发送的通信是私密且安全的(外部观察者无法读取或更改)。它是TLS协议的基础。”)(HTTPS是HTTP协议的加密版本。它通常使用SSL或TLS来加密客户端和服务器之间的所有通信。这种安全连接允许客户端安全地与服务器交换敏感数据一个服务器,例如用于银行活动或在线购物。))协议站点。客户端请求代理服务器将TCP连接隧道传输到目标主机。之后,服务器将代替客户端与目标主机建立连接.连接建立后,代理服务器向客户端发送或接收TCP报文流,0x02表示目标主机需要主动连接客户端时使用BIND请求Bind方法(ftp协议)。服务端收到的数据包中的CMD为X'02',服务端使用Bind方式作为代理,使用Bind方式代理时,服务端最多需要回复客户端两个数据包。服务器使用TCP协议连接到对应的(DST.ADDR,DST.PORT)。如果失败,则返回失败状态的数据包并关闭会话。如果成功,监听(BND.ADDR,BND.PORT)接受被请求主机的请求,然后返回第一个数据包,用于让客户端发送指定目标主机的数据连接到客户端地址和端口包。目标主机连接到服务器指定的地址和端口成功或失败后,回复第二个数据包。此时(BND.ADDR,BND.PORT)应该是目标主机与服务器建立连接的地址和端口。0x03表示UDP转发RSV0x00,保留ATYP类型0x01IPv4地址,DST.ADDR部分4字节长度0x03域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余内容为域名,没有0结尾。0x04IPv6地址,16字节长。DST.ADDR目的地址DST.PORT网络字节序表示的目的端口示例数据服务器请求远程服务器按照以下固定格式响应客户端客户端的封装数据端。VERREPRSVATYPBND.ADDRBND.PORT110x001Dynamic2VER是SOCKS版本,这里应该是0x05;REP响应字段0x00表示成功0x01正常SOCKS服务器连接失败0x02现有规则不允许连接0x03网络无法访问0x04主机无法访问0x05连接被拒绝0x06TTL超时0x07不支持的命令0x08不支持的地址类型0x09-0xFF未定义RSV0x00,保留ATYP0x01IPv4地址,DST.ADDR部分的4字节长度0x03域名,DST.ADDR部分的第一个字节为域名长度,DST。ADDR的其余部分是域名,末尾不带0。0x04IPv6地址,16字节长。BND.ADDR服务器绑定的地址BND.PORT网络字节序表示的服务器绑定的端口使用nodejs 实现CONNECT请求constnet=require('net');constdns=require('域名系统');constAUTHMETHODS={//只支持这两种认证方式NOAUTH:0,USERPASS:2}//创建socks5监听letsocket=net.createServer(sock=>{//监听错误sock.on('error',(err)=>{console.error('错误代码%s',err.code);console.error(err);});sock.on('close',()=>{sock.destroyed||sock.destroy();});sock.once('data',autherHandler.bind(sock));//进程认证方式});letautherHandler=function(data){letsock=this;console.log('autherHandler',数据);constVERSION=parseInt(数据[0],10);if(VERSION!=5){//不支持其他版本的socks协议sock.destoryed||sock.destory();返回假;}constmethodBuf=data.slice(2);//方法列表letmethods=[];for(leti=0;imethod===AUTHMETHODS.USERPASS);if(kind){letbuf=Buffer.from([VERSION,AUTHMETHODS.USERPASS]);sock.write(buf);sock.once('数据',passwdHandler.bind(sock));}else{kind=methods.find(method=>method===AUTHMETHODS.NOAUTH);if(kind===0){letbuf=Buffer.from([VERSION,AUTHMETHODS.NOAUTH]);sock.write(buf);sock.once('数据',requestHandler.bind(sock));}else{letbuf=Buffer.from([VERSION,0xff]);sock.write(buf);返回假;}}}/***认证账号密码*/letpasswdHandler=function(data){letsock=this;console.log('数据',数据);让ulen=parseInt(data[1],10);让用户名=data.slice(2,2+ulen).toString('utf8');让密码=data.slice(3+ulen).toString('utf8');如果(用户名==='admin'&&密码==='123456'){sock.write(Buffer.from([5,0]));}else{sock.write(Buffer.from([5,1]));返回假;}sock.once('data',requestHandler.bind(sock));}/***处理客户端请求*/letrequestHandler=function(data){letsock=this;常量版本=数据[0];让命令=数据[1];//0x01支持第一个CONNECT连接if(cmd!==1)console.error('不支持其他连接%d',cmd);让flag=VERSION===5&&cmd<4&&data[2]===0;如果(!标志)返回假;让atyp=数据[3];让主机、端口=data.slice(data.length-2).readInt16BE(0);让copyBuf=Buffer.allocUnsafe(data.length);数据.copy(copyBuf);if(atyp===1){//使用ip=hostname(data.slice(4,8))连接到主机;//开始连接主机!连接(主机,端口,copyBuf,袜子);}elseif(atyp===3){//使用域名letlen=parseInt(data[4],10);host=data.slice(5,5+len).toString('utf8');如果(!domainVerify(主机)){console.log('domainisfailure%s',host);返回假;}console.log('host%s',host);dns.lookup(host,(err,ip,version)=>{if(err){console.log(err)return;}connect(ip,port,copyBuf,sock);});}}letconnect=function(host,port,data,sock){if(port<0||host==='127.0.0.1')返回;console.log('host%sport%d',host,port);让socket=newnet.Socket();socket.connect(port,host,()=>{data[1]=0x00;if(sock.writable){sock.write(data);sock.pipe(socket);socket.pipe(sock);}});socket.on('close',()=>{socket.destroyed||socket.destroy();});socket.on('error',err=>{if(err){console.error('connect%s:%derr',ho圣,端口);数据[1]=0x03;如果(sock.writable)sock.end(数据);控制台错误(错误);套接字结束();}})}lethostname=function(buf){lethostName='';如果(buf.length===4){for(leti=0;iconsole.log('socks5proxyrunning...')).on('error',错误=>console.error(err));end结合浏览器使用,发现无法加载斗鱼视频。不知道为什么,优酷没问题。刚学了一些NodeJs的知识点,写的马马虎虎。如有写得不好的地方,请指出。一起讨论吧,双方会保持一个TCP长连接,客户端直接发送封装请求包。实际上,客户端的每一次请求都是从认证开始的,每一次请求都是相互独立的,所以once方式在这里特别适合