這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
shadowsocks 協議
shadowsocks協議包格式為IV向量加Playload,這是加密後的結果.
+-------+----------+| IV | Payload |+-------+----------+| Fixed | Variable |+-------+----------+
解密後為
+--------------+---------------------+------------------+----------+| Address Type | Destination Address | Destination Port | Data |+--------------+---------------------+------------------+----------+| 1 | Variable | 2 | Variable |+--------------+---------------------+------------------+----------+
服務端解密需要這個IV向量和原來就協商好的密鑰進行解密,握手完成後後續的所有TCP 資料包不會再帶上IV,而是使用握手時協商的那個IV。
源碼解析
這裡分析shadowsocks-server的原始碼,整體流程為:
監聽服務
初始化加密對象
解密用戶端資料
串連遠程主機
建立通訊串連
監聽服務
這雷根據設定檔,進行連接埠監聽,後面就是常規的Accept()等待串連.
for port, password := range config.PortPassword { go run(port, password, config.Auth) if udp { go runUDP(port, password, config.Auth) } }
初始化加密對象
當有連結來到後首先檢查是否已經初始化解密對象,然後封裝net.Conn.這裡的cipher根據不同的密碼編譯演算法實現了流加密解密XORKeyStream(dst, src []byte)介面,具體可以看golang內建標準庫cipher.Stream,當然這個介面要讀取iv向量後才能實現.
//如果沒有初始化,解析初始化. if cipher == nil { log.Println("creating cipher for port:", port) cipher, err = ss.NewCipher(config.Method, password) if err != nil { log.Printf("Error generating cipher for port: %s %v\n", port, err) conn.Close() continue } } //這裡要根據password產生key. key := evpBytesToKey(password, mi.keyLen)
解密用戶端資料
net.Conn被封裝後Read方法讀出的資料即為解密後的資料
func (c *Conn) Read(b []byte) (n int, err error) { //如果解密介面沒有實現,讀取iv向量初始化解密介面. if c.dec == nil { iv := make([]byte, c.info.ivLen) if _, err = io.ReadFull(c.Conn, iv); err != nil { return } //讀取到iv向量後就可以初始化解密介面 if err = c.initDecrypt(iv); err != nil { return } if len(c.iv) == 0 { c.iv = iv } } //如果讀取buff大於內建buff則分配記憶體 cipherData := c.readBuf if len(b) > len(cipherData) { cipherData = make([]byte, len(b)) } else { cipherData = cipherData[:len(b)] } //讀取資料後解密返回 n, err = c.Conn.Read(cipherData) if n > 0 { c.decrypt(b[0:n], cipherData[0:n]) } return}
串連遠程主機
當讀取到解密的資料後就要開始解包了,這裡只複製主要代碼.
//這裡判斷一個位元組的地址類型 addrType := buf[idType] //這雷根據地址類型的不同讀取地址 if _, err = io.ReadFull(conn, buf[reqStart:reqEnd]); err != nil { return } //這裡讀取2個位元組的連接埠 port := binary.BigEndian.Uint16(buf[reqEnd-2 : reqEnd]) //當擷取到地址和連接埠後調用dial串連遠程主機 remote, err := net.Dial("tcp", host)
剩下可能還會有ota資料,根據ota已經被棄用了吧,這裡就不再說明了,有興趣的同學可以自己去查資料.
建立通訊串連
這裡建立通訊串連有點像使用了io.Copy(),分別串連兩個conn.
//PipeThenClose有點像io.Copy if ota { go ss.PipeThenCloseOta(conn, remote) } else { go ss.PipeThenClose(conn, remote) } ss.PipeThenClose(remote, conn) //PipeThenClose的原理就是不斷讀取然後寫入,直到出錯. for{ n, err := src.Read(buf) // read may return EOF with n > 0 // should always process n > 0 bytes before handling error if n > 0 { // Note: avoid overwrite err returned by Read. if _, err := dst.Write(buf[0:n]); err != nil { Debug.Println("write:", err) break } } }
結語
這裡分析了shadowsocks-server的原始碼,還有shadowsocks-local的感覺大同小異就是多了一個加密的過程.