使用nodejs實現socks5協議 socks5 介紹 SOCKS協議內容

本文出處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 (HTTPS) 協議的站點。客戶端要求代理服務器將 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這個方法特別適合這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章