天天看點

代理服務擴充

之前自己實作了一個代理服務,當時考慮的是隻要支援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,其中封裝了一個反向代理的實作。隻需要提供建立連結的函數以及對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的代理政策移植到該代理服務上,然後再寫個IE插件用來實作類似SwitchySharp的功能,這樣的話,會友善很多。順便說下,其實作在IE做的很不錯。

上一篇: 2014年總結

繼續閱讀