使用nodejs实现socks5协议

50次阅读

共计 6511 个字符,预计需要花费 17 分钟才能阅读完成。

本文出处 https://shenyifengtk.github.io/
如有转载,请说明出处

socks5 介绍

socks5s 是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。当防火墙后的客户端要访问外部的服务器时,就跟 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 代理

Var NMETHODS METHODS
1 1 0-255

表格里面的单位表示位数

  • Var 表示是 SOCK 版本,应该是5;
  • NMETHODS 表示 METHODS部分的长度
  • METHODS 表示支持客户端支持的认证方式列表,每个方法占 1 字节。当前的定义是

    • 0x00 不需要认证
    • 0x01 GSSAPI
    • 0x02 用户名、密码认证
    • 0x03 – 0x7F 由 IANA 分配(保留)
    • 0x80 – 0xFE 为私人方法保留
    • 0xFF 无可接受的方法

服务器会响应给客户端

VER METHOD
1 1
  • Var 表示是 SOCK 版本,应该是5;
  • METHOD是服务端选中方法,这个的值为上面METHODS 列表中一个。如果客户端支持 0x00,0x01,0x02,这三个方法。服务器只会选中一个认证方法返回给客户端,如果返回 0xFF 表示没有一个认证方法被选中,客户端需要关闭连接。

我们先用一个简单 Nodejs 在实现 sock 连接握手. 查看客户端发送数据报

const net = require('net');
let server = net.createServer(sock =>{sock.once('data', (data)=>{console.log(data);
});
});
server.listen(8888,'localhost');

使用 curl 工具连接 nodejs

curl -x socks5://localhost:8888 https://www.baidu.com

console 输出

<Buffer 05 02 00 01>

使用账号密码认证

当服务器选择 0x02 账号密码方式认证后,客户端开始发送账号、密码,数据包格式如下: (以字节为单位)

VER ULEN UNAME PLEN PASSWD
1 1 1 to 255 1 1 to 255
  • VER 是 SOCKS 版本
  • ULEN 用户名长度
  • UNAME 账号 string
  • PLEN 密码长度
  • PASSWD 密码 string

可以看出账号密码都是 明文传输 ,非常地不安全。
服务器端校验完成后,会响应以下数据():

VER STATUS
1 1
  • STATUS 0x00 表示成功,0x01 表示失败

封装请求

认证结束后客户端就可以发送请求信息。客户端开始封装请求信息
SOCKS5 请求格式(以字节为单位):

VER CMD RSV ATYP DST.ADDR DST.PORT
1 1 0x00 1 动态 2
  • VER 是 SOCKS 版本,这里应该是 0x05;
  • CMD 是 SOCK 的命令码

    • 0x01 表示 CONNECT 请求

      • CONNECT 请求可以开启一个客户端与所请求资源之间的双向沟通的通道。它可以用来创建隧道(tunnel)。例如,CONNECT 可以用来访问采用了 SSL is a standard protocol that ensures communication sent between two computer applications is private and secure (cannot be read nor changed by outside observers). It is the foundation for the TLS protocol.”) (HTTPS is an encrypted version of the HTTP protocol. It usually uses SSL or TLS to encrypt all communication between a client and a server. This secure connection allows clients to safely exchange sensitive data with a server, for example for banking activities or online shopping.”)) 协议的站点。客户端要求代理服务器将 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 转发
  • RSV 0x00,保留
  • ATYP 类型

    • 0x01 IPv4 地址,DST.ADDR 部分 4 字节长度
    • 0x03 域名,DST.ADDR 部分第一个字节为域名长度,DST.ADDR 剩余的内容为域名,没有 0 结尾。
    • 0x04 IPv6 地址,16 个字节长度。
  • DST.ADDR 目的地址
  • DST.PORT 网络字节序表示的目的端口

示例数据

<Buffer 05 01 00 01 0e d7 b1 26 01 bb>

服务器根据客户端封装数据,请求远端服务器,将下面固定格式响应给客户端。

VER REP RSV ATYP BND.ADDR BND.PORT
1 1 0x00 1 动态 2
  • VER 是 SOCKS 版本,这里应该是 0x05;
  • REP 应答字段

    • 0x00 表示成功
    • 0x01 普通 SOCKS 服务器连接失败
    • 0x02 现有规则不允许连接
    • 0x03 网络不可达
    • 0x04 主机不可达
    • 0x05 连接被拒
    • 0x06 TTL 超时
    • 0x07 不支持的命令
    • 0x08 不支持的地址类型
    • 0x09 – 0xFF 未定义
  • RSV 0x00,保留
  • ATYP

    • 0x01 IPv4 地址,DST.ADDR 部分 4 字节长度
    • 0x03 域名,DST.ADDR 部分第一个字节为域名长度,DST.ADDR 剩余的内容为域名,没有 0 结尾。
    • 0x04 IPv6 地址,16 个字节长度。
  • BND.ADDR 服务器绑定的地址
  • BND.PORT 网络字节序表示的服务器绑定的端口

使用 nodejs 实现 CONNECT 请求

const net = require('net');
const dns = require('dns');
const AUTHMETHODS = { // 只支持这两种方法认证
    NOAUTH: 0,
    USERPASS: 2
}

// 创建 socks5 监听

let socket = net.createServer(sock => {

        // 监听错误
        sock.on('error', (err) => {console.error('error code %s',err.code);
                        console.error(err);
        });

                sock.on('close', () => {sock.destroyed || sock.destroy();
        });

        sock.once('data', autherHandler.bind(sock)); // 处理认证方式
    });

let autherHandler = function (data) {
    let sock = this;
    console.log('autherHandler', data);
    const VERSION = parseInt(data[0], 10);
    if (VERSION != 5) { // 不支持其他版本 socks 协议
        sock.destoryed || sock.destory();
        return false;
    }
    const methodBuf = data.slice(2); // 方法列表

    let methods = [];
    for (let i = 0; i < methodBuf.length; i++)
        methods.push(methodBuf[i]);
    // 先判断账号密码方式
    let kind = methods.find(method => method === AUTHMETHODS.USERPASS);
    if (kind) {let buf = Buffer.from([VERSION, AUTHMETHODS.USERPASS]);
        sock.write(buf);
        sock.once('data', passwdHandler.bind(sock));
    } else {kind = methods.find(method => method === AUTHMETHODS.NOAUTH);
        if (kind === 0) {let buf = Buffer.from([VERSION, AUTHMETHODS.NOAUTH]);
            sock.write(buf);
            sock.once('data', requestHandler.bind(sock));
        } else {let buf = Buffer.from([VERSION, 0xff]);
            sock.write(buf);
            return false;
        }
    }

}

/**
 * 认证账号密码
 */
let passwdHandler = function (data) {
    let sock = this;
    console.log('data', data);
    let ulen = parseInt(data[1], 10);
    let username = data.slice(2, 2 + ulen).toString('utf8');
    let password = data.slice(3 + ulen).toString('utf8');
    if (username === 'admin' && password === '123456') {sock.write(Buffer.from([5, 0]));
    } else {sock.write(Buffer.from([5, 1]));
        return false;
    }
    sock.once('data', requestHandler.bind(sock));
}

/**
 * 处理客户端请求
 */
let requestHandler = function (data) {
    let sock = this;
    const VERSION = data[0];
    let cmd = data[1]; // 0x01 先支持 CONNECT 连接
        if(cmd !== 1)
          console.error('不支持其他连接 %d',cmd);
        let flag = VERSION === 5 && cmd < 4 && data[2] === 0;
    if (! flag)
        return false;
    let atyp = data[3];
    let host,
    port = data.slice(data.length - 2).readInt16BE(0);
    let copyBuf = Buffer.allocUnsafe(data.length);
    data.copy(copyBuf);
    if (atyp === 1) { // 使用 ip 连接
        host = hostname(data.slice(4, 8));
        // 开始连接主机!connect(host, port, copyBuf, sock);

    } else if (atyp === 3) { // 使用域名
        let len = parseInt(data[4], 10);
        host = data.slice(5, 5 + len).toString('utf8');
        if (!domainVerify(host)){console.log('domain is fialure %s', host);
                        return false;
                }
        console.log('host %s', host);
        dns.lookup(host, (err, ip, version) => {if(err){console.log(err)
                return;
            }            
            connect(ip, port, copyBuf, sock);
        });

    }
}

let connect = function (host, port, data, sock) {if(port < 0 || host === '127.0.0.1')
           return;
    console.log('host %s port %d', host, port);
    let socket = new net.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:%d err',host,port);
            data[1] = 0x03;
                        if(sock.writable)
            sock.end(data);
            console.error(err);
            socket.end();}
    })
}

let hostname = function (buf) {
    let hostName = '';
    if (buf.length === 4) {for (let i = 0; i < buf.length; i++) {hostName += parseInt(buf[i], 10);
            if (i !== 3)
                hostName += '.';
        }
    } else if (buf.length == 16) {for (let i = 0; i < 16; i += 2) {let part = buf.slice(i, i + 2).readUInt16BE(0).toString(16);
            hostName += part;
            if (i != 14)
                hostName += ':';
        }
    }
    return hostName;
}

/**
 * 校验域名是否合法
 */
let domainVerify = function (host) {let regex = new RegExp(/^([a-zA-Z0-9|\-|_]+\.)?[a-zA-Z0-9|\-|_]+\.[a-zA-Z0-9|\-|_]+(\.[a-zA-Z0-9|\-|_]+)*$/); 
    return regex.test(host);
}


socket.listen(8888,() => console.log('socks5 proxy running ...')).on('error', err => console.error(err));
                                                                                                                        

end

和浏览器结合使用的,发现没办法加载斗鱼的视频,不知什么原理,优酷都没有什么问题的.
刚刚学习 NodeJs 一些知识点,写得一般般,有哪里写得不好的,请大家指出来,大家一起讨论。一开始在看协议的时候,以为客户端(浏览器)和服务器在认证请求完后,双方会保持一个 TCP 长连接,客户端直接发送封装请求数据包.实际上客户端每一个请求都是从认证开始的,每一个请求都是相互独立的,所以 once 这个方法特别适合这里

正文完
 0