這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
之前自己實現了一個代理服務,當時考慮的是只要支援SOCKS5就好了,因為我經常用CHROME,配合著SwitchySharp,體驗還是很棒的。但是我現在有點討厭CHROME,它現在太龐大了,佔用資源太多了。而且我有鎖定網頁的習慣,一開啟CHROME,就十幾個甚至二十幾個進程起來,讓我很不爽。但是不得不說CHROME的安全設計還是非常棒的。然後我就試了下FireFox,額,我覺著它和IE差不多.然後就放棄了,然後看看了手頭上的IE已經到11了,平時用起來感覺還是很不錯的,所以我想支援IE的代理。
IE的代理機制比較囧,比如說它只支援SOCKS4,不支援SOCKS5,然後又分為HTTP代理,HTTPS代理,還有FTP代理。也沒有像CHROME提供強大的代理外掛程式機制。雖然IE提供了PAC機制,但是不得不說,這個機制也很雞肋,沒有像SwitchySharp那樣可以做到即時的增減規則。針對以上原因,我就在原有的代碼基礎上增加了上面的幾種代理,不過沒支援FTP代理。
SOCKS4代理
SOCKS4協議比較簡單,可以參考的文檔是WIKI的這篇,還有OpenSSH的這篇。後面還有個SOCKS4A協議,不過這個SOCKS4A基本上沒見到人用過。SOCKS4協議的CONNECT命令格式很簡單,就一個請求包和回應包。請求包的第一個欄位是版本號碼,佔用1個位元組,就是0x04,第二個欄位是命令類型,佔用1個位元組,0x01表示CONNECT命令,即請求連結哪個IP : PORT,0x02是BIND,一般用於FTP情境,我沒有實現。第三個欄位是對端連接埠,佔用2個位元組,位元組序是網路位元組順序;第四個欄位是對端IP,佔用4個位元組,位元組序是網路位元組順序;第五個欄位是USERID,可變長度,以0x00結尾。這裡要注意的是,在IE11下,USERID為目前使用者名,不會為空白。所以要讀取完整的USERID和最後的0x00。
回應包第一個欄位佔用一個位元組,資料為0;第二個欄位佔用一個位元組,表示狀態,0X5A表示成功,0X5B表示拒絕或者失敗等等;第三個位元組和第四個欄位一共6個位元組,會被忽略,直接填0即可。
整個協議簡單很多,比SOCKS5簡單多,但是沒有SOCKS5強大。因為SOCKS4隻支援IP : PORT方式,也就意味著IEFQ的時候,會自己先走本地DNS,然後拿到地址後才去走SOCKS代理。這裡帶來的問題是,如果DNS被汙染了,就意味著FQ失敗了。所以還得用後面的HTTP代理和HTTP隧道。
HTTP Tunnel (HTTP隧道)
HTTP隧道比較簡單。就是用戶端通過HTTP協議連結到服務端,請求服務端去連結某個網域名稱或者IP的某個連接埠。協議非常簡單,即用戶端發送CONNECT Domain : Port HTTP/1.0\r\n\r\n。服務端收到該請求後會去連結指定的網域名稱和連接埠,連結成功後,會回複用戶端HTTP/1.0 200 Connection established\r\n\r\n 用戶端收到該回複後,就開始把資料通過代理轉寄過去。這個時候的代理是盲轉,和SOCKS協議一樣。
用GO實現的時候也相對來說比較簡單,通過net/http包即可完成。自己實現一個ServeHTTP方法,然後發現是CONNECT方法的請求就把串連Hijacked掉。具體代碼如下:
hj, ok := response.(http.Hijacker)if !ok { http.Error(response, "Hijacker failed", http.StatusInternalServerError) return}conn, _, err := hj.Hijack()if err != nil { http.Error(response, err.Error(), http.StatusInternalServerError) return}defer conn.Close()
要注意的一點是Hijack後,如果要回複HTTP協議格式的資料,就要自己去操作了,沒有辦法再使用net/http.ResponseWriter提供的方法了。好在GO的fmt包提供了Fprint/Fprintf函數,所以操作起來也還算簡單。
另外一點是,這個HTTP隧道允許在CONNECT發起時,在BODY裡攜帶額外資料以達到最佳化的目的。所以還要在建立遠端連結後,檢查是否還有BODY資料,如果有的話,就把資料發出去。
HTTP Proxy
我原先認為的是既然有了HTTP隧道方式的代理機制了,那就都用這套唄,結果IE不這樣,HTTP隧道只用在了HTTPS類型的URL,而普通的HTTP URL則走的是普通的HTTP代理機制。HTTP普通的請求類似於下面這樣:GET /xxx/yyyy/zzzz.html HTTP/1.0,而HTTP代理則是GET http://www.qqqq.com/xxx/yyy/zzz.html HTTP/1.0,然後還會增加一個額外的HTTP首部Proxy-Connection。這個首部用來幹嘛的自行GOOGLE。處理用戶端發來的HTTP代理請求時,我的做法是把URL替換為正常的相對URI,然後檢查是否存在Proxy-Connection,如果存在,則擷取對應的值,並刪除該首部,並添加Connection首部,其值為原Proxy-Connection對應的值。然後轉寄到對端伺服器。 GO提供了一個包net/http/httputil,其中封裝了一個反向 Proxy的實現。只需要提供建立連結的函數以及對http.Request處理的函數即可。代碼具體如下:
func NewHTTPProxy(remoteSocks, cryptoMethod string, password []byte) *HTTPProxy { return &HTTPProxy{ ReverseProxy: &httputil.ReverseProxy{ Director: director, Transport: &http.Transport{ Dial: func(network, addr string) (net.Conn, error) { return dial(network, addr, remoteSocks, cryptoMethod, password) }, }, }, }}func dial(network, addr, remoteSocks, cryptoMethod string, password []byte) (net.Conn, error) { tcpAddr, err := net.ResolveTCPAddr(network, addr) if err != nil { return nil, err } remoteSvr, err := NewRemoteSocks(remoteSocks, cryptoMethod, password) if err != nil { return nil, err } // version(1) + cmd(1) + reserved(1) + addrType(1) + domainLength(1) + maxDomainLength(256) + port(2) req := []byte{0x05, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} copy(req[4:8], []byte(tcpAddr.IP.To4())) binary.BigEndian.PutUint16(req[8:10], uint16(tcpAddr.Port)) err = remoteSvr.Handshake(req) if err != nil { remoteSvr.Close() return nil, err } conn := &HTTPProxyConn{ RemoteSocks: remoteSvr, } return conn, nil}func director(request *http.Request) { u, err := url.Parse(request.RequestURI) if err != nil { return } request.RequestURI = u.RequestURI() v := request.Header.Get("Proxy-Connection") if v != "" { request.Header.Del("Proxy-Connection") request.Header.Del("Connection") request.Header.Add("Connection", v) }}
總結:
本質上HTTP代理和HTTP隧道可以通過同一個連接埠實現的,但是我沒有這樣去做,因為我覺著代碼分開更方便測試和修改。可以省去很多的麻煩。不過通過簡單的組合也一樣可以複用同一個連接埠,我後面會試著去修改。HTTP PROXY和TUNNEL現在在同一個連接埠實現,通過簡單的組合就實現了對應的功能。而SOCKS4和SOCKS5按理來說也是可以用同一個連接埠的,但是考慮到代碼中要判斷版本之類的問題,我覺著這樣還不如直接分開實現來的簡單。
後面還可以考慮的是把SwitchySharp的Proxy 原則移植到該代理服務上,然後再寫個IE外掛程式用來實作類別似SwitchySharp的功能,這樣的話,會方便很多。順便說下,其實現在IE做的很不錯。