1. 程式人生 > >Shadowsocks 原始碼解釋

Shadowsocks 原始碼解釋

https://yveschan.github.io/blog/shadowsocks-analysis/

Preface

去年shadowsocks在V2EX剛釋出的時候,我就已經開始留意這個專案。當時還在用Goagent,但有時候速度確實不咋的,而且重新配置的話會比較麻煩。9月份入手VPS之後開始折騰PPTP VPN,效果相當不穩定。不久後OpenVPN也開始受到干擾了。看來必須尋找比較小眾的方式,避免躺槍。因此,初試shadowsocks(python版),速度或者穩定性都相當好,一直用到現在,未出現過什麼問題。配置也很簡單,唯一的門檻就是需要國外的Linux 主機(VPS)。現在shadowsocks專案已經發展到多語言跨平臺了,社群也比較活躍,主要原因是專案架構簡單,程式碼精簡易於維護。

一週前開始學習python,主要是想用python寫一個爬蟲。大概用了4天的課餘時間把 《Dive Into Python3》過了一遍,瞭解基本語法。結合文件看大牛的原始碼是很好的學習方式,所謂learn by doing嘛。不得不說,shadowsocks的原始碼真心簡潔,再看一下SOCK5協議的報文格式,並沒有花很多時間。貌似說了不少廢話,現在入正題= =!

Socks5

首先介紹一下socks5協議:SOCKS協議位於傳輸層(TCP/UDP等)與應用層之間,其工作流程為

  1. client向proxy發出請求資訊,用以協商傳輸方式
  2. proxy作出應答
  3. client接到應答後向proxy傳送目的主機(destination server)的ip和port
  4. proxy評估該目的主機地址,返回自身IP和port,此時C/P連線建立。
  5. proxy與dst server連線
  6. proxy將client發出的資訊傳到server,將server返回的資訊轉發到client。代理完成

client連線proxy的第一個報文資訊,進行認證機制協商

version nmethod methods
1 Bytes 1 Bytes 1 to 255 Bytes

一般是 hex: 05 01 00即:版本5,1種認證方式,NO AUTHENTICATION REQUIRED(無需認證 0x00)

proxy從METHODS欄位中選中一個位元組(一種認證機制),並向Client傳送響應報文:

version methods
1 1

一般是 hex: 05 00即:版本5,無需認證

認證機制相關的子協商完成後,Client提交轉發請求:

VER CMD RSV ATYP DST.ADDR DST.PORT
1 1 0x00 1 variable 2

前3個一般是 hex: 05 01 00地址型別可以是* 0x01 IPv4地址* 0x03 FQDN(全稱域名)* 0x04 IPv6地址

對應不同的地址型別,地址資訊格式也不同:* IPv4地址,這裡是big-endian序的4位元組資料* FQDN,比如”www.nsfocus.net”,這裡將是:0F 77 77 77 2E 6E 73 66 6F 63 75 73 2E 6E 65 74。注意,第一位元組是長度域* IPv6地址,這裡是16位元組資料。

proxy評估來自Client的轉發請求併發送響應報文

VER REP RSV ATYP BND.ADDR BND.PORT
1 1(response) 0x00 1 variable 2

Proxy可以靠DST.ADDR、DST.PORT、SOCKSCLIENT.ADDR、SOCKSCLIENT.PORT進行評估,以決定建立到轉發目的地的TCP連線還是拒絕轉發。若允許則響應包的REP為0,非0則表示失敗(拒絕轉發或未能成功建立到轉發目的地的TCP連線)。

shadowsocks source code

原始碼方面,主要是由socks5轉發模組和加密解密模組組成

轉發模組感覺比較簡單,但是個人覺得有幾點需要注意的地方,或者說我自己不太明白。(python菜,請諒解)

1
2
3
4
5
6
with open('config.json', 'rb') as f:
    config = json.load(f)
SERVER = config['server']
REMOTE_PORT = config['server_port']
PORT = config['local_port']
KEY = config['password']

client

  • 從main開始,讀取配置。這裡為什麼要用二進位制的方式開啟json檔案呢?
  • 解釋命令列引數 optlist, args = getopt.getopt(sys.argv[1:], 's:p:k:l:'),這個跟Linux的getopt函式差不多,可以自己man一下。
  • 設定logging等級和資訊,生成密文表(包括加密解密)。
  • 執行 ThreadingTCPServer 例項。從定義看, class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer) 子類化ThreadingTCPServer(多重繼承繼承ThreadingMixIn類和TCPServer類),即使用多執行緒處理TCP請求(Mix-in class to handle each request in a new thread),同時設定其類屬性allow_reuse_address。這裡繫結的地址是(”,PORT),意思是該套接字對於本機的任何地址都是可達的。BaseRequestHandler 則由 Socks5Server 子類實現。由於 SocketServer module 包含了很多socket programming的細節,所以程式碼看起來相當簡潔。
  • 每當有請求到達,呼叫 handle 函式,主要是建立proxy到client和server的連線,然後呼叫 handle_tcp 函式來轉發TCP資料包(包括client to server或相反方向),對於server來說,proxy是完全透明的。當然,這裡client和proxy之間的資料互動需要通過加密傳輸。
  • 對於handle_tcp而言,它需要同時處理兩個socket(client & server),這裡使用了I/O multiplexing的方式,選擇select作為實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def handle_tcp(self, sock, remote):
    try:
        fdset = [sock, remote]
        while True:
            r, w, e = select.select(fdset, [], [])
            if sock in r:
                data = sock.recv(4096)
                if len(data) <= 0:
                    break
                result = send_all(remote, self.decrypt(data))
                if result < len(data):
                    raise Exception('failed to send all data')
            if remote in r:
                data = remote.recv(4096)
                if len(data) <= 0:
                    break
                result = send_all(sock, self.encrypt(data))
                if result < len(data):
                    raise Exception('failed to send all data')
    finally:
        sock.close()
        remote.close()

對於高效能併發伺服器而言,這是一種非常重要的手段。當然還是其他實現方式,例如poll,epoll,kqueue等。這裡由於檔案描述符數量較小,所以分別也不大了。更詳細的資訊可以看 C10K problem

server

服務端程式碼與客戶端差不多,主要是資料報文的解釋和轉發問題。主要是處理好在client端傳送過來的自定資料格式,轉發到目的地址server,再將返回的資料轉發給client。

更多的細節我都在原始碼上註釋了,有興趣可以看看。

整個架構圖大概這樣:(就不要吐槽畫的有多醜了= =)

archarch

Reference: