這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
實現一個代理服務
在天朝做程式員比較讓人蛋疼,比如你想用GOOGLE,你就很蛋疼。原因大家都懂。
然後呢,一開始自己在用GOAGENT, VPN, SSH, ShadowSocks等程式,GOAGENT和SHADOWSOCKS都是非常優秀的。而自己在很早剛開始接觸電腦的時候就有想法自己寫一個代理程式,因為各種各樣的原因總是沒去做,或者說自己的需求總是能夠被滿足,所以沒什麼動力。但是自從學GO語言後,網路程式的開發變的沒有之前做C/C++時那麼蛋疼了,所以試著自己寫一個代理程式,然後也貫徹Eating your own dog food的實踐。這裡把一些心得總結記錄下。GITHUB地址:https://github.com/eahydra/socks
SOCKS5協議
剛開始的時候,是想著做HTTP代理,但是HTTP協議比較複雜,牽扯到的概念比較多,自己又不熟悉,所以就沒選中做HTTP通道方式的代理程式。而代理協議裡用的最多的其實就是SOCKS協議了。SOCKS4,SOCKS5等。因為我一直在用CHROME, CHROME上的外掛程式SwitchySharp也比較好用,也支援SOCKS5協議,更重要的是SOCKS5協議非常簡單。
SOCKS5協議主要分三步,第一步就是握手協商,協商雙方的驗證方式。我的做法簡單粗暴:因為SOCKS5規定的握手協議中,規定了最大的資料長度就是258個直接,所以我在實現的時候,直接申請258位元組的BUFFER,然後讀取。讀取後也不對驗證方式進行判斷是否合法,支援之類的,直接返回0X05 0X00,也就是不需要驗證。
第二步就是擷取用戶端發來的CMD協議。SOCKS5這裡也規定了最長長度,263個位元組。所以也一次申請出來以備後用。感謝GO語言封裝的網路程式庫,不管是啥網域名稱,還是IPV4,IPV6之類的,調用net.Dial的時候傳進去就好,net.Dial函數內部會自己搞定到IP的轉換。然後就根據CMD裡指定的目標地址連結,成功後返回對應的協議資料。目前我只支援了CONNECT,像UDP之類的就沒做支援,遇到了UDP指令直接告訴用戶端不支援,然後斷掉連結。
第三步就是開始接受用戶端發送來的資料,然後發送到遠端伺服器,然後從遠端伺服器讀資料再轉寄到用戶端。這個就簡單了,直接兩行代碼搞定。
go io.Copy(dest, src)io.Copy(src, dest)
部署
寫好程式後,就得部署起來。感謝亞馬遜提供了免費一年的AWS服務。怎麼申請之類的自行搜尋解決。GO的移植性這裡就體現出來了,因為我開發是在WINDOWS下,然後部署到AWS上的UBUNTU系統上,一個go build搞定。然後程式跑起來後,直接設定SwitchySharp到AWS上,效果顯著。不過中間測試的時候,發現連結到http://go-talks.appspot.com/github.com/extemporalgenome/gotalks/error-handling.slide#1 就不行了,抓包發現,從AWS上發來了很多RST,但是我的程式沒有列印出對應的CLOSE資訊。這個時候我就猜測應該是那個啥(你懂的)做了內容檢測,解決的方法之一就是加密。所以我後面就做了加密的功能。
加密
增加加密功能就蛋疼了,因為我可能不只是要一種密碼編譯演算法,可能這會想用RC4,後面就想用AES或者DES之類的。還有就是怎麼把密碼編譯演算法封裝起來,使其對現有的代碼影響較小。本來我是想自己手工封裝成io.Reader和io.Writer的介面,這樣的話就可以組成一個調用鏈,很自然的把網路資料進行加解密。然後我就去GO的標準庫裡找,有密碼編譯演算法,RC4,DES,AES都有提供,然後有個叫crypto/cipher的包,這個包提供了倆介面,一個叫StreamRead,另外一個叫StreamWrite,這倆介面正好可以滿足io.Reader和io.Write。這下子就簡單多了。具體的代碼如下(代碼裡沒有做什麼原廠模式之類的,因為實在是沒啥必要,寫的簡單粗暴點也不會影響其他代碼):
func NewCipherStream(rwc io.ReadWriteCloser, cryptMethod string, password []byte) (*CipherStream, error) { var stream *CipherStream switch cryptMethod { default: stream = &CipherStream{ reader: rwc, writeCloser: rwc, } case "rc4": { rc4CipherRead, err := rc4.NewCipher(password) if err != nil { return nil, err } rc4CipherWrite, err := rc4.NewCipher(password) if err != nil { return nil, err } stream = &CipherStream{ reader: &cipher.StreamReader{ S: rc4CipherRead, R: rwc, }, writeCloser: &cipher.StreamWriter{ S: rc4CipherWrite, W: rwc, }, } } case "des": { block, err := des.NewCipher(password) if err != nil { return nil, err } desRead := cipher.NewCFBDecrypter(block, desIV[:]) desWrite := cipher.NewCFBEncrypter(block, desIV[:]) return &CipherStream{ reader: &cipher.StreamReader{ S: desRead, R: rwc, }, writeCloser: &cipher.StreamWriter{ S: desWrite, W: rwc, }, }, nil } } return stream, nil}
如果沒有指定加密方式或者是不支援的加密方式,還是沿用原有的io.Reader和io.Writer介面。這樣的話就可以減少對上層的影響。
因為支援了多種加密方式,那麼就得用設定檔了。這個就很簡單,直接JSON格式搞定。
另外一個問題是,瀏覽器只認SOCKS協議,你現在加了加密,那你得對瀏覽器隱藏掉,然後就得支援倆模式,一個模式是本地的SOCKS運行方式,對瀏覽器提供的,另外一個模式就是到遠端SOCKS服務的資料要加密。本來是想再寫一個本地SOCKS,但是想了下,實在是毫無必要,因為這個程式的大部分功能在現有的代碼基礎上都能滿足,那麼一種實現方式就是代碼拷貝出來,但是我要維護兩分;另外一種方式就是在原有的代碼上改。決定在現有代碼上改,無非是在用戶端的請求來了後,要先連結到自己的遠端SOCKS服務,然後再提供資料代理服務。這裡就涉及到怎麼和遠端服務互動的問題。
和遠端SOCKS互動的解決方案
為瞭解決這個問題,就得需要訂協議。是否有必要增加協議呢?我第一個想法是好像沒必要,第二個想法是真的沒必要?真的沒必要!因為SOCKS5的協議足夠使用,我也不想因為要連結到遠端還要增加一套額外的協議,而這個協議最終也會和SOCKS5差不多,實現出來的代碼又會很蛋疼。所以決定使用SOCKS5協議,做成一個調用鏈的形式。用戶端來的握手,本地SOCKS也到遠端SOCKS上進行握手。用戶端發來的CMD直接轉寄到遠端SOCKS。然後中間走加密。這下子實現就變的非常簡單了。定義了一個結構體RemoteSocks,匿名組合了*CipherStream,這樣就可以直接走加密了。具體定義代碼如下:
type RemoteSocks struct { conn net.Conn *CipherStream}
然後同步代碼到AWS上,編譯開跑,然後發現很流暢。給基友狗眼坤分享了下,遇到了BUG,在他機器上連結YOUTUBE出現連結錯誤。然後想大概問題在哪,開始做代碼REVIEW,發現自己實現的加密部分,RC4相關的部分讀和寫複用了同一個加密對象,然後這個對象又不是安全執行緒的,問題應該是這裡了,修正掉,然後測試之,OK了。
總結
總算是有了自己的代理程式,用起來很開心,因為是自己做的!Eating your own dog food很重要。自己實現一個程式的時候,可以不用一下子跨很大的步子,那樣容易扯到蛋,可以一點點來。