這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
前兩天,使用Golang實現了一個簡單的HTTP Proxy,具體實現參見 http://www.flysnow.org/2016/12/24/golang-http-proxy.html,這次使用Golang實現一個Socket5的簡單代理。Socket5和HTTP並沒有太大的不同,他們都可以完全給予TCP協議,只是請求的資訊結構不同,所以這次我們不能像上次HTTP Proxy一樣,解析請求和應答,要按照Socket的協議方式解析。
Socket協議版本
Socket協議分為Socket4和Socket5兩個版本,他們最明顯的區別是Socket5同時支援TCP和UDP兩個協議,而SOcket4隻支援TCP。目前大部分使用的是Socket5,我們這裡只簡單的介紹Socket5協議。
Socket5協議之授權認證
要想實現Socket5之間的串連會話,必須要懂SOcket5協議的實現細節和規範。這就好比我們都用普通話對話一樣,彼此說的都明白,也可以給對方聽得懂的回應。Socket5的用戶端和服務端交流也一樣,他們的語言就是Socket5協議。因為Socket5支援TCP和UDP兩種,這裡只介紹TCP這一種,UDP大同小異。
首先用戶端會給服務端發送驗證資訊,這個是建立串連的前提。比如用戶端:hi,哥們,借個火。服務端要認識它就說:好,給;如果不認識就說:你哪根蔥啊!!用戶端請求的暗號很簡單:
VER |
NMETHODS |
METHODS |
1 |
1 |
1 to 255 |
- 第一個欄位VER代表Socket的版本,Soket5預設為0x05,其固定長度為1個位元組
- 第二個欄位NMETHODS表示第三個欄位METHODS的長度,它的長度也是1個位元組
- 第三個METHODS表示用戶端支援的驗證方式,可以有多種,他的嘗試是1-255個位元組。
目前支援的驗證方式一共有:
- X’00’ NO AUTHENTICATION REQUIRED(不需要驗證)
- X’01’ GSSAPI
- X’02’ USERNAME/PASSWORD(使用者名稱密碼)
- X’03’ to X’7F’ IANA ASSIGNED
- X’80’ to X’FE’ RESERVED FOR PRIVATE METHODS
- X’FF’ NO ACCEPTABLE METHODS(都不支援,沒法串連了)
以上的都是十六進位常量,比如X’00’表示十六進位0x00。
服務端收到用戶端的驗證資訊之後,就要回應用戶端,服務端需要用戶端提供哪種驗證方式的資訊。服務端的回應同樣非常簡潔。
- 第一個欄位VER代表Socket的版本,Soket5預設為0x05,其值長度為1個位元組
- 第二個欄位METHOD代表需要服務端需要用戶端按照此驗證方式提供驗證資訊,其值長度為1個位元組,選擇為上面的六種驗證方式。
舉例說明,比如服務端不需要驗證的話,可以這麼回應用戶端:
這就代表格服務端說:哥們,我沒啥要求,你來吧,我們使用Go實現代碼如下:
123456789101112 |
var b [1024]byten, err := client.Read(b[:])if err != nil {log.Println(err)return}if b[0] == 0x05 { //只處理Socket5協議//用戶端回應:Socket服務端不需要驗證方式client.Write([]byte{0x05, 0x00})n, err = client.Read(b[:])} |
我們這裡以最簡單的不需要驗證的方式為例進行介紹,這種方式進行了上面的一問一答後就可以開始建立串連了。對於其他驗證方式,還需要再進行一次一問一答,主要是用戶端提供驗證資訊,服務端回應驗證是否正確,這些細節可以參考http://www.ietf.org/rfc/rfc1928.txt以及http://www.ietf.org/rfc/rfc1929.txt的Socket5協議定義。
Socket5協議之建立串連。
Socket5的用戶端和服務端進行雙方授權驗證通過之後,就開始建立串連了。串連由用戶端發起,告訴Sokcet服務端用戶端需要訪問哪個遠程伺服器,其中包含,遠程伺服器的地址和連接埠,地址可以是IP4,IP6,也可以是網域名稱。
VER |
CMD |
RSV |
ATYP |
DST.ADDR |
DST.PORT |
1 |
1 |
X’00’ |
1 |
Variable |
2 |
- VER代表Socket協議的版本,Soket5預設為0x05,其值長度為1個位元組
- CMD代表用戶端請求的類型,值長度也是1個位元組,有三種類型
- CONNECT X’01’
- BIND X’02’
- UDP ASSOCIATE X’03’
- RSV保留字,值長度為1個位元組
- ATYP代表請求的遠程伺服器位址類型,值長度1個位元組,有三種類型
- IP V4 address: X’01’
- DOMAINNAME: X’03’
- IP V6 address: X’04’
- DST.ADDR代表遠程伺服器的地址,根據ATYP進行解析,值長度不定。
- DST.PORT代表遠程伺服器的連接埠,要訪問哪個連接埠的意思,值長度2個位元組
從協議裡解析我們需要的遠程伺服器資訊,Go代碼實現如下:
12345678910 |
var host,port stringswitch b[3] {case 0x01://IP V4host = net.IPv4(b[4],b[5],b[6],b[7]).String()case 0x03://網域名稱host = string(b[5:n-2])//b[4]表示網域名稱的長度case 0x04://IP V6host = net.IP{b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19]}.String()}port = strconv.Itoa(int(b[n-2])<<8|int(b[n-1])) |
現在用戶端把要請求的遠程伺服器的資訊都告訴Socket5Proxy 伺服器了,那麼Socket5Proxy 伺服器就可以和遠程伺服器建立串連了,不管串連是否成功等,都要給用戶端回應,其回應格式為:
VER |
REP |
RSV |
ATYP |
BND.ADDR |
BND.PORT |
1 |
1 |
X’00’ |
1 |
Variable |
2 |
- VER代表Socket協議的版本,Soket5預設為0x05,其值長度為1個位元組
- REP代表響應狀態代碼,值長度也是1個位元組,有以下幾種類型
- X’00’ succeeded
- X’01’ general SOCKS server failure
- X’02’ connection not allowed by ruleset
- X’03’ Network unreachable
- X’04’ Host unreachable
- X’05’ Connection refused
- X’06’ TTL expired
- X’07’ Command not supported
- X’08’ Address type not supported
- X’09’ to X’FF’ unassigned
- RSV保留字,值長度為1個位元組
- ATYP代表請求的遠程伺服器位址類型,值長度1個位元組,有三種類型
- IP V4 address: X’01’
- DOMAINNAME: X’03’
- IP V6 address: X’04’
- BND.ADDR表示綁定地址,值長度不定。
- BND.PORT表示綁定連接埠,值長度2個位元組
Go實現的串連和應答如下:
1234567 |
server, err := net.Dial("tcp", net.JoinHostPort(host, port))if err != nil {log.Println(err)return}defer server.Close()client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) //響應用戶端串連成功 |
綁定的地址和連接埠,這兩個響應根據請求的CMD不同而不同,詳細描述參考http://www.ietf.org/rfc/rfc1928.txt
資料轉寄
建立好串連之後,就是資料傳遞轉寄,TCP協議可以直接轉寄。UDP的話需要特殊處理,具體參考協議定義,其實就是一個特殊格式的回應,和上面的一問一答差不多,更多協議細節 http://www.ietf.org/rfc/rfc1928.txt。
TCP直接轉寄非常簡單:
123 |
//進行轉寄go io.Copy(server, client)io.Copy(client, server) |
完整代碼實現
以下是完成的代碼實現。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 |
package mainimport ("io""log""net""strconv")func main() {log.SetFlags(log.LstdFlags | log.Lshortfile)l, err := net.Listen("tcp", ":8081")if err != nil {log.Panic(err)}for {client, err := l.Accept()if err != nil {log.Panic(err)}go handleClientRequest(client)}}func handleClientRequest(client net.Conn) {if client == nil {return}defer client.Close()var b [1024]byten, err := client.Read(b[:])if err != nil {log.Println(err)return}if b[0] == 0x05 { //只處理Socket5協議//用戶端回應:Socket服務端不需要驗證方式client.Write([]byte{0x05, 0x00})n, err = client.Read(b[:])var host, port stringswitch b[3] {case 0x01: //IP V4host = net.IPv4(b[4], b[5], b[6], b[7]).String()case 0x03: //網域名稱host = string(b[5 : n-2]) //b[4]表示網域名稱的長度case 0x04: //IP V6host = net.IP{b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19]}.String()}port = strconv.Itoa(int(b[n-2])<<8 | int(b[n-1]))server, err := net.Dial("tcp", net.JoinHostPort(host, port))if err != nil {log.Println(err)return}defer server.Close()client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) //響應用戶端串連成功//進行轉寄go io.Copy(server, client)io.Copy(client, server)}} |
這裡目前是一個簡易版本的Socket5 代理,還有很多沒有實現,要實現完成的Socket5,可以自己根據他定義的協議試試。