閱讀778 返回首頁    go 阿裏雲 go 技術社區[雲棲]


計算機網絡(一) 走近socks5

圖片名稱

最近項目中涉及到socket5協議,趁此機會補一下這一塊的空缺。

1. 什麼是socks5

或許你沒聽說過socks5,但你一定聽說過ShadowSocks,ShadowSockS內部使用的正是socks5協議。

socks是"SocketS"的縮寫,因此socks5也叫sockets5。

RFC地址:

socks是一種網絡傳輸協議,主要用於客戶端與外網服務器之間通訊的中間傳遞。根據OSI七層模型來劃分,SOCKS屬於會話層協議,位於表示層與傳輸層之間。

當防火牆後的客戶端要訪問外部的服務器時,就跟socks代理服務器連接。該協議設計之初是為了讓有權限的用戶可以穿過過防火牆的限製,使得高權限用戶可以訪問外部資源。經過10餘年的時間,大量的網絡應用程序都支持socks5代理。

這個協議最初由David Koblas開發,而後由NEC的Ying-Da Lee將其擴展到版本4,最新協議是版本5,與前一版本相比,socks5做了以下增強:

  • 增加對UDP協議的支持;
  • 支持多種用戶身份驗證方式和通信加密方式;
  • 修改了socks服務器進行域名解析的方法,使其更加優雅;

2. socks5使用場景

socks協議的設計初衷是在保證網絡隔離的情況下,提高部分人員的網絡訪問權限,但是國內似乎很少有組織機構這樣使用。一般情況下,大家都會使用更新的網絡安全技術來達到相同的目的。

但是由於socksCap32和PSD這類軟件,人們找到了socks協議新的用途:突破網絡通信限製,這和該協議的設計初衷正好相反。

下麵是兩個典型的運用場景:

  • 美國某網遊的服務器僅允許本國的IP進行連接。非美國玩家為了突破這種限製,可以找一個該地區的socks5代理服務器,然後用PSD接管網遊客戶端,通過socks5代理服務器連接遊戲服務器。這樣遊戲服務器就會認為該玩家的客戶端位於本地區,從而允許該玩家進行遊戲(在天朝也叫科學**,屬於正向代理)。

image.png

  • 某服務器的防火牆僅允許部分端口(如http的80端口)通信,那麼可以利用socks5協議和一個打開80端口監聽的socks5服務器連接,從而可以連接公網上其他端口的服務器。利用一些額外的技術手段,甚至可以騙過內部的http代理服務器,這時在使用內網http代理上網的環境下也可以不受限製的使用網絡服務,這稱之為socks over HTTP(我們常說的穿牆)。

  • 內網穿透:在大學裏,學校給我們提供了很多服務器資源,我們可以在內網使用。但放寒假回家後,無法進入學校內網,也就無法連接上內網的服務器資源。解決辦法:在公網的VPS上搭一個socks代理,並將內網的一台web服務器和該VPS的socks端口打通,通過這台web服務器便可以訪問所有內網服務器資源(常見的花生殼nat穿透和這個類似)。
    image.png

當然,使用代理服務器後,將不可避免的出現通信延遲,所以應該盡量選擇同網絡(通運營商)、距離近的服務器。

3. 與HTTP代理的對比

socks支持多種用戶身份驗證方式和通信加密方式。

socks工作在比HTTP代理更低的網絡層:socks使用握手協議來通知代理軟件其客戶端試圖進行的連接socks,然後盡可能透明地進行操作,而常規代理可能會解釋和重寫報頭(例如,使用另一種底層協議,例如FTP;然而,HTTP代理隻是將HTTP請求轉發到所需的HTTP服務器)。

socks5代理支持轉發UDP報文,而HTTP屬於tcp協議,不支持UDP報文的轉發。

雖然HTTP代理有不同的使用模式,CONNECT方法允許轉發TCP連接;然而,socks代理還可以轉發UDP流量和反向代理,而HTTP代理不能。HTTP代理更適合HTTP協議,執行更高層次的過濾;socks不管應用層是什麼協議,隻要是傳輸層是TCP/UDP協議就可以代理。

4. socks5協議詳解

socks5認證協議

image.png
在客戶端、服務端協商好使用用戶名密碼認證後,客戶端發出用戶名密碼,格式為:

image.png

  • VER:鑒定協議版本
  • ULEN:用戶名長度
  • UNAME:用戶名
  • PLEN:密碼長度
  • PASSWD:密碼

服務器鑒定後發出如下回應:

image.png

  • VER:鑒定協議版本
  • STATUS:鑒定狀態

其中鑒定狀態 0x00 表示成功,0x01 表示失敗。

socks5傳輸協議

創建與socks5服務器的TCP連接後,客戶端需要先發送請求來協商版本及認證方式,格式為:
image.png

  • VER:socks版本(在socks5中是0x05);
  • NMETHODS:在METHODS字段中出現的方法的數目;
  • METHODS:客戶端支持的認證方式列表,每個方法占1字節。

服務器從客戶端提供的方法中選擇一個最優的方法並通過以下消息通知客戶端(貪心算法:雙方都支持、安全性最高):

image.png

  • VER:socks版本(在socks5中是0x05);
  • METHOD:服務端選中的方法(若返回0xFF表示沒有方法被選中,客戶端需要關閉連接);

METHOD字段的值可以取如下值:

  • X'00' NO AUTHENTICATION REQUIRED
  • X'01' GSSAPI
  • X'02' USERNAME/PASSWORD
  • X'03' to X'7F' IANA ASSIGNED
  • X'80' to X'FE' RESERVED FOR PRIVATE METHODS
  • X'FF' NO ACCEPTABLE METHODS

之後客戶端和服務端根據選定的認證方式執行對應的認證。認證結束後客戶端就可以發送請求信息(如果認證方法有特殊封裝要求,請求必須按照方法所定義的方式進行封裝)。

socks5請求格式:

image.png

  • VER:socks版本(在socks5中是0x05)
  • CMD:SOCK的命令碼:

    • CONNECT X'01'
    • BIND X'02'
    • UDP ASSOCIATE X'03'
  • RSV:保留字段

  • ATYP:地址類型:

    • IP V4地址: X'01'
    • 域名地址: X'03'
    • IP V6地址: X'04'
  • DST.ADDR:目的地址

  • DST.PORT:目的端口

服務器按以下格式回應客戶端的請求:

image.png

  • VER:socks版本(在socks5中是0x05)
  • REP:應答狀態碼:

    • X'00' succeeded
    • X'01' general socks server failure
    • X'02' connection not allowed by ruleset
    • X'03' Network unreachable
    • X'04' Host unreachable
    • X'05' Connection refused
    • X'06' TTL expired
    • X'07' Command not supported
    • X'08' Address type not supported
    • X'09' to X'FF' unassigned
  • RSV:保留字段(需設置為X'00')

  • ATYP:地址類型:

    • IP V4 address: X'01'
    • DOMAINNAME: X'03'
    • IP V6 address: X'04'
  • BND.ADDR:服務器綁定的地址

  • BND.PORT:服務器綁定的端口

如果被選中的方法包括有認證信息的封裝、完整性和/或機密性相關檢查,則server端在發送響應包時也需要把這些響應消息封裝進去。

5. ​SOCKS相關工具

SOCKS服務器

部分SOCKS服務器軟件:

SOCKS客戶端

一般情況下應用程序會內嵌對SOCKS協議的支持。

客戶端 許可證 版本 發布日期 平台 支持協議
Dante client BSD/CMU 1.1.18 09/2005 Linux v4, v5
FreeCap GPL 3.18 02/2005 Windows -
Hummingbird socks - - - Windows -
ProxyCap - 2.03 - Windows -
SocksCap Non-Comercial home use - - - v5
Super Socks5Cap - 1.5.3 - Windows -
tsocks GPL 1.8 10/2002 - -
nylon - - 06/2003 OpenBSD -

6. python實現socks5代理

為了方便,我們直接使用python中的SocketServer庫,直接運行以下程序即可在本機建立了一個socks5的代理服務器:

import socket, sys, select, SocketServer, struct, time  

class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass  
class Socks5Server(SocketServer.StreamRequestHandler):  
    def handle_tcp(self, sock, remote):  
        fdset = [sock, remote]  
        while True:  
            r, w, e = select.select(fdset, [], [])  
            if sock in r:  
                if remote.send(sock.recv(4096)) <= 0: break  
            if remote in r:  
                if sock.send(remote.recv(4096)) <= 0: break  
    def handle(self):  
        try:  
            print 'socks connection from ', self.client_address  
            sock = self.connection  
            # 1. Version  
            sock.recv(262)  
            sock.send(b"\x05\x00");  
            # 2. Request  
            data = self.rfile.read(4)  
            mode = ord(data[1])  
            addrtype = ord(data[3])  
            if addrtype == 1:       # IPv4  
                addr = socket.inet_ntoa(self.rfile.read(4))  
            elif addrtype == 3:     # Domain name  
                addr = self.rfile.read(ord(sock.recv(1)[0]))  
            port = struct.unpack('>H', self.rfile.read(2))  
            reply = b"\x05\x00\x00\x01"  
            try:  
                if mode == 1:  # 1. Tcp connect  
                    remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
                    remote.connect((addr, port[0]))  
                    print 'Tcp connect to', addr, port[0]  
                else:  
                    reply = b"\x05\x07\x00\x01" # Command not supported  
                local = remote.getsockname()  
                reply += socket.inet_aton(local[0]) + struct.pack(">H", local[1])  
            except socket.error:  
                # Connection refused  
                reply = '\x05\x05\x00\x01\x00\x00\x00\x00\x00\x00'  
            sock.send(reply)  
            # 3. Transfering  
            if reply[1] == '\x00':  # Success  
                if mode == 1:    # 1. Tcp connect  
                    self.handle_tcp(sock, remote)  
        except socket.error:  
            print 'socket error'  
def main():  
    server = ThreadingTCPServer(('', 1080), Socks5Server)  
    server.serve_forever()  
if __name__ == '__main__':  
    main()  

客戶端實現代碼:

import socket, socks, requests

def main():  
    socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 1080)
    socket.socket = socks.socksocket
    print(requests.get('https://127.0.0.1:1080').text)
if __name__ == '__main__':  
    main()

7. 參考文獻

最後更新:2017-09-25 16:33:28

  上一篇:go  雲棲長卷:一張圖看懂雲棲七年
  下一篇:go  阿裏雲前端周刊 - 第 26 期