計算機網路(一)走近socks5

北島知寒發表於2017-09-25
圖片名稱

最近專案中涉及到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"x05x00");  
            # 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"x05x00x00x01"  
            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"x05x07x00x01" # Command not supported  
                local = remote.getsockname()  
                reply += socket.inet_aton(local[0]) + struct.pack(">H", local[1])  
            except socket.error:  
                # Connection refused  
                reply = `x05x05x00x01x00x00x00x00x00x00`  
            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(`http://127.0.0.1:1080`).text)
if __name__ == `__main__`:  
    main()

7. 參考文獻


相關文章