一、SOCKS5協議解析
轉載 http://blog.chinaunix.net/uid-26548237-id-3434356.html
1、介紹
防火牆的使用,有效的隔離了機構內部網絡和外部網絡,這種類型的Internet架構變得越來越流行了。這種防火牆系統大都充當着網絡之間的應用層網關的角色,通常提供經過控制的Telnet、FTP和SMTP訪問。爲了推動全球信息的交流,更多的新的應用層協議的推出,這就有必要提供一個總的架構使這些協議能夠更明顯和更安全的穿過防火牆,也有必要在實際上爲它們穿過防火牆提供一個更強的認證機制。這種需要源於客戶機-服務器聯繫在不同組織網絡之間的實現,而這種聯繫需要得到控制並有安全的認證。
在這兒所描述的協議框架是爲了讓使用TCP和UDP的客戶/服務器應用程序更方便地使用網絡防火牆所提供的服務所設計的。這個協議從概念上來講是介於應用層和傳輸層之間的“中介層”,因而不提供如傳遞ICMP信息之類由網絡層網關的所提供的服務。
2、現有的協議
當前存在一個協議socks4,它爲Telnet、FTP、HTTP、WAIS和GOPHER等基於TCP協議的客戶/服務器程序提供一個不安全的防火牆。而這個新協議擴展了socks4,以使其支持UDP框架規定的安全認證方案、地址解析方案中所規定的域名和IPV6。爲了實現這個socks協議,通過需要重新編譯或者重新鏈接基於TCP的客戶端應用程序以使用socks庫中相應的加密函數。
注意:
除非特別註明,所有出現在數據包格式圖中的十進制數字均以字節表示相應域的長度。如果某域需要給定一個字節的值,用X'hh'來表示這個字節中的值。如果某域中用到單詞'Variable',這表示該域的長度是可變的,且該長度定義在一個和這個域相關聯(1-2個字節)的域中,或一個數據類型域中。
3、基於TCP協議的客戶
當一個基於TCP協議的客戶端希望與一個只能通過防火牆可以到達的目標(這個由實現決定的)建立連接,它必須先建立一個與socks服務器上socks端口的TCP連接。通常這個TCP端口是1080。當連接建立後,客戶端進入協議的“握手”過程:認證方式的選擇,根據選中的方式進行認證,然後發送轉發的請求。sockds服務器檢查這個請求,根據結果,或建立合適的連接,或拒絕。
除非特別註明,所有出現在數據包格式圖中的十進制數字均以字節表示相應域的長度。如果某域需要給定一個字節的值,用X'hh'來表示這個字節中的值。如果某域中用到單詞'Variable',這表示該域的長度是可變的,且該長度定義在一個和這個域相關聯(1-2個字節)的域中,或一個數據類型中。
客戶端連到服務器後,然後就發送請求來協商版本和認證方法:
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1 to 255 |
+----+----------+----------+
VER(版本)在這個協議版本中被設置爲X'05'。NMETHODS(方法選擇)中包含在METHODS(方法)中出現的方法標識的數據(用字節表示)。
服務器從METHODS給出的方法中選出一種,發送一個METHOD(方法)選擇報文:
+----+--------+
|VER | METHOD |
+----+--------+
| 1 | 1 |
+----+--------+
如果所選擇的METHOD的值是X'FF',則客戶端所列出的方法是沒有可以被接受的,客戶機就必須關閉連接。
當前被定義的METHOD的值有:
-> X'00' 無驗證需求
-> X'01' 通用安全服務應用程序接口(GSSAPI)
-> X'02' 用戶名/密碼(USERNAME/PASSWORD)
-> X'03' 至 X'7F' IANA 分配(IANA ASSIGNED)
-> X'80' 至 X'FE' 私人方法保留(RESERVED FOR PRIVATE METHODS)
-> X'FF' 無可接受方法(NO ACCEPTABLE METHODS)
其中,IANA是負責全球Internet上的IP地址進行編號分配的機構。
於是客戶端和服務器進入方法細節的子商議。方法選擇子商議描述於獨立的文檔中。
欲得到該協議新的METHOD支持的開發者可以和IANA聯繫以求得METHOD號。已分配號碼的文檔需要參考METHOD號碼的當前列表和它們的通訊協議
如果想順利的執行則必須支持GSSAPI和支持用戶名/密碼(USERAE/PASSWORD)認證方法。
4、需求
一旦方法選擇子商議結束,客戶機就發送請求細節。如果商議方法包括了完整性檢查的目的或機密性封裝,則請求必然被封在方法選擇的封裝中。
SOCKS請求如下表所示:
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
其中:
o VER protocol version:X'05'
o CMD
o CONNECT X'01'
o BIND X'02'
o UDP ASSOCIATE X'03'
o RSV RESERVED
o ATYP address type of following address
o IP V4 address: X'01'
o DOMAINNAME: X'03'
o IP V6 address: X'04'
o DST.ADDR desired destination address
o DST.PORT desired destination port in network octet order
5、地址
在地址域(DST.ADDR,BND.ADDR)中,ATYP域詳細說明了包含在該域內部的地址類型:
o X'01'
該地址是IPv4地址,長4個八位組。
o X'03'
該地址包含一個完全的域名。第一個八位組包含了後面名稱的八位組的數目,沒有中止的空八位組。
o X'04'
該地址是IPv6地址,長16個八位組。
注:我在應用該協議時,糾結了好久,當時選擇X'03',填寫相應字段時,到底該怎麼填寫呢?是寫socks服務器的還是寫請求的?現在想想,自己好無知啊。。如我們想發生HTTP請求:www.baidu.com,則DST.ADDR爲: 13www.baidu.com0x000x50。其中,13是www.baidu.com的長度,然後接www.baidu.com,然後是兩個字節的端口號,這裏爲80端口。
6、迴應
到SOCKS服務器的連接一經建立,客戶機即發送SOCKS請求信息,並且完成認證商議。服務器評估請求,返回一個迴應如下表所示:
+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
其中:
o VER protocol version: X'05'
o REP Reply field:
o X'00' succeeded
o X'01' general SOCKS server failure
o X'02' connection not allowed by ruleset
o X'03' Network unreachable
o X'04' Host unreachable
o X'05' Connection refused
o X'06' TTL expired
o X'07' Command not supported
o X'08' Address type not supported
o X'09' to X'FF' unassigned
o RSV RESERVED
o ATYP address type of following address
o IP V4 address: X'01'
o DOMAINNAME: X'03'
o IP V6 address: X'04'
o BND.ADDR server bound address
o BND.PORT server bound port in network octet order
標誌RESERVED(RSV)的地方必須設置爲X'00'。
如果被選中的方法包括有認證目的封裝,完整性和/或機密性的檢查,則迴應就被封裝在方法選擇的封裝套中。
CONNECT
在CONNECT的迴應中,BND.PORT包括了服務器分配的連接到目標主機的端口號,同時BND.ADDR包含了關聯的IP地址。此處所提供的BND.ADDR通常情況不同於客戶機連接到SOCKS服務器所用的IP地址,因爲這些服務器提供的經常都是多址的(muti-homed)。都期望SOCKS主機能使用DST.ADDR和DST.PORT,連接請求評估中的客戶端源地址和端口。
BIND
BIND請求被用在那些需要客戶機接受到服務器連接的協議中。FTP就是一個衆所周知的例子,它通過使用命令和狀態報告建立最基本的客戶機-服務器連接,按照需要使用服務器-客戶端連接來傳輸數據。(例如:ls,get,put) 都期望在使用應用協議的客戶端在使用CONNECT建立首次連接之後僅僅使用BIND請求建立第二次連接。都期望SOCKS主機在評估BIND請求時能夠使用ST.ADDR和DST.PORT。
有兩次應答都是在BIND操作期間從SOCKS服務器發送到客戶端的。第一次是發送在服務器創建和綁定一個新的socket之後。BIND.PORT域包含了SOCKS主機分配和偵聽一個接入連接的端口號。BND.ADDR域包含了關聯的IP地址。
客戶端具有代表性的是使用這些信息來通報應用程序連接到指定地址的服務器。第二次應答只是發生在預期的接入連接成功或者失敗之後。在第二次應答中,BND.PORT和BND.ADDR域包含了欲連接主機的地址和端口號。
UDP ASSOCIATE(不太懂)
UDP連接請求用來建立一個在UDP延遲過程中操作UDP數據報的連接。DST.ADDR和DST.PORT域包含了客戶機期望在這個連接上用來發送UDP數據報的地址和端口。服務器可以利用該信息來限制至這個連接的訪問。如果客戶端在UDP連接時不持有信息,則客戶端必須使用一個全零的端口號和地址。
當一個含有UDP連接請求到達的TCP連接中斷時,UDP連接中斷。
在UDP連接請求的迴應中,BND.PORT和BND.ADDR域指明瞭客戶端需要被髮送UDP請求消息的端口號/地址。
迴應過程
當一個迴應(REP值非X'00')指明失敗時,SOCKS主機必須在發送後馬上中斷該TCP連接。該過程時間必須爲在偵測到引起失敗的原因後不超過10秒。
如果迴應代碼(REP值爲X'00')時,則標誌成功,請求或是BIND或是CONNECT,客戶機現在就可以傳送數據了。如果所選擇的認證方法支持完整性、認證機制和/或機密性的封裝,則數據被方法選擇封裝包來進行封裝。類似,當數據從客戶機到達SOCKS主機時,主機必須使用恰當的認證方法來封裝數據。
7.基於UDP客戶機的程序
一個基於UDP的客戶端必須使用在BND.PORT中指出的UDP端口來發送數據報到UDP延遲服務器,而該過程是作爲對UDP連接請求的迴應而進行的。如果所選擇的認證方法提供認證機制、完整性、和/或機密性,則數據報必須使用恰當的封裝套給予封裝。每一個UDP數據報攜帶一個UDP請求的報頭(header):
+----+------+------+----------+----------+----------+
|RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
+----+------+------+----------+----------+----------+
| 2 | 1 | 1 | Variable | 2 | Variable |
+----+------+------+----------+----------+----------+
UDP請求報頭是:
o RSV Reserved X'0000'
o FRAG Current fragment number
o ATYP address type of following addresses:
o IP V4 address: X'01'
o DOMAINNAME: X'03'
o IP V6 address: X'04'
o DST.ADDR desired destination address
o DST.PORT desired destination port
o DATA user data
當一個UDP延遲服務器決定延遲一個UDP數據報時,它會按兵不動,對客戶機無任何通報。類似的,它會將它不能或不打算延遲的數據報Drop?掉。當一個UDP延遲服務器接收到一個來自遠程主機的延遲數據報,它必須使用上面的UDP請求報頭來封裝該數據報,和任何認證方法選擇的封裝。
一個UDP延遲服務器必須從SOCKS服務器獲得所期望的客戶機的IP地址,而該客戶機要發送數據報到BND.PORT--在至UDP連接的迴應中已經給出。UDP延遲服務器還必須drop掉除了特定連接中的一條記錄之外的其它的所有源IP地址。
FRAG域指出了數據報是否爲大量的數據片(flagments)中的一片。如果標明瞭,高序(high-order)位說明是序列的結束段,而值爲X'00'則說明該數據報是獨立的。值介於1-127之間片斷位於數據片序列中間。每一個接收端都有一個和這些數據片相關的重組隊列表(REASSEMBLY QUEUE)和一個重組時間表(REASSEMBLY TIMER)。重組隊列必須被再次初始化並且相關聯的數據片必須被丟掉,而無論該重組時間表是否過期,或者一個新的攜帶FRAG域的數據報到達,並且FRAG域的值要小於正在進行的數據片序列中的FRAG域的最大值。且重組時間表必須不少於5秒。無論如何最好避免應用程序直接與數據片接觸(?)。
數據片的執行是可選的,一個不支持數據片的執行必須drop掉任何除了FRAG域值爲X'00'了數據報。
一個利用SOCKS的UDP程序接口必須預設有效的緩衝區來裝載數據報,並且系統提供的實際緩衝區的空間要比數據報大:
o if ATYP is X'01' - 10+method_dependent octets smaller
o if ATYP is X'03' - 262+method_dependent octets smaller
o if ATYP is X'04' - 20+method_dependent octets smaller
8、安全性考慮
這篇文檔描述了一個用來透過IP網絡防火牆的應用層協議。這種傳輸的安全性在很大程度上依賴於特定實現所擁有以及在SOCKS客戶與SOCKS服務器之間經協商所選定的特殊的認證和封裝方式。系統管理員需要對用戶認證方式的選擇進行仔細考慮。
9、引用
[1] Koblas, D., "SOCKS", Proceedings: 1992 Usenix Security Symposium.
Author's Address
Marcus Leech
Bell-Northern Research Ltd
P.O. Box 3511, Stn. C,
Ottawa, ON
CANADA K1Y 4H7
Phone: (613) 763-9145
EMail: [email protected]
10、譯者
譯者:Radeon(Radeon [email protected])
二、ESOCKS原理解析
先來科普下 GFW (長城防火牆)是怎麼擋住我們的:(轉載)
[*]關鍵字過濾
[*]IP 封鎖
[*]DNS 污染、劫持
[*]特定端口封鎖
[*]加密連接的干擾
我們一條條來看:
[*]關鍵字過濾
大家都知道,比如 Http 協議數據包頭部是明文的,所以 GFW 一旦發現連接有敏感詞,馬上就會僞裝成連接兩方,向真正的對方發送 RST 數據包,真正的雙方一看,出現異常了,那把連接關閉吧。
所以,有時候你會發現有的頁面正在打開,然後過了一會又沒了,顯示無法連接。
[*]
IP 封鎖
GFW 可以在出境的網關上加一條僞造的路由規則,這樣對於一些被過濾了的 IP 的數據包就無法正確地被送達,所以也就無法訪問了。
GFW 封路由可是很兇殘的,直接封獨立 IP ,這樣可能因爲某個敏感站點,導致跟他同一臺主機的其他站點也無法訪問,理解起來就像旁註。
[*]
DNS 污染、劫持
DNS 也就是域名解析服務,GFW 會對所有經過骨幹出口路由的在 UDP 的 53 端口上的域名查詢進行檢測,一旦發現有黑名單裏的域名,它就會僞裝成目標域名的解析服務器給查詢者返回虛假結果。由於 UDP 是一種無連接不可靠的協議,查詢者只能接受最先返回的結果。
而即便我們用可靠的 TCP 協議來查詢,雖然 GFW 不能污染 DNS 了,但是可能會被重置(發送 RST),查詢者也無法得到返回的 IP 。
[*]
特定端口封鎖
對於一些特點的 IP ,GFW 會丟棄特定端口上的數據包,使得某些功能無法使用,比如 443端口SSL,22端口的SSH。
GWF 曾經幹過一件事,針對 Google 的一些 IP 上的443端口,實施間歇性封鎖,不明所以的用戶就會覺得這是 Google 抽風了,久而久之自然不能忍受 “老是出問題” 的產品。
[*]
加密連接的干擾
加密連接不總是加密的,公鑰還是明文的,所以 GFW 就能識別出特定服務的證書。然後在遇到 “黑名單” 加密連接時,它會發送RST數據包,干擾雙方正常的 TCP 連接,進而切斷加密連接的握手。
三、源碼
server.cpp (與客戶端 與web服務器通信)
#include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <assert.h> #include <unistd.h> #include <sys/wait.h> #include <netdb.h> #include "protocol.h" #include "comm.h" #include "transfer.h" using namespace std; int proc_socks(int in) { cout<<"start service"<<endl; //build with client request HelloReq req; assert(0==recv_n(in,&req,sizeof(req))); assert(req.ver==5); char method[256]; assert(0==recv_n(in,method,req.method_n)); assert(0==method[0]); //build with client response HelloRep rep; rep.ver=5; rep.method=0; assert(0==send_n(in,&rep,sizeof(rep))); //build complata recv des_server AddrReq areq; assert(0==recv_n(in,&areq,sizeof(areq))); assert(areq.ver==5&&areq.cmd==1&&areq.rsv==0); //manage request sockaddr_in raddr; raddr.sin_family=AF_INET; //get addr if(areq.atype==1){//request addr=ip in_addr_t addr; assert(0==recv_n(in,&addr,sizeof(addr)));//recv ip memcpy(&raddr.sin_addr.s_addr, &addr, sizeof(addr)); } if(areq.atype==3){//request addr=domian char len; assert(0==recv_n(in,&len,1)); char domian[128]; assert(0==recv_n(in,domian,len)); domian[len]=0; // cout<<domian<<endl; hostent *host=gethostbyname(domian);//get ip by domian assert(host && host->h_addrtype == AF_INET && host->h_length >= 1); memcpy(&raddr.sin_addr.s_addr, host->h_addr_list[0], sizeof(raddr.sin_addr.s_addr)); } else{ cout<<"err ip/domian"<<endl; return -1; } cout<<"ip:"<<raddr.sin_addr.s_addr<<endl; short rport;//get request port assert(0 == recv_n(in, &rport, sizeof(rport))); raddr.sin_port = rport; cout << "connect to peer"<< inet_ntoa(raddr.sin_addr)<< ":" << ntohs(raddr.sin_port) << endl; //connect int out=socket(AF_INET,SOCK_STREAM,0); assert(out!=-1); assert(0==connect(out,(sockaddr*)&raddr,sizeof(raddr))); //replay AddrRep arep; arep.ver=5; arep.rep=0; arep.rsv=0; arep.atype=1; sockaddr_in local; socklen_t slen=sizeof(sockaddr); assert(0==getsockname(out,(sockaddr*)&local,&slen)); memcpy(&arep.addr, &local.sin_addr.s_addr, sizeof(arep.addr)); memcpy(&arep.port, &local.sin_port, sizeof(arep.port)); assert(0 == send_n(in, &arep, sizeof(arep))); //transfer ThreadParam tp1={in,out,0}; ThreadParam tp2={out,in,0}; pthread_t tid; assert(0==pthread_create(&tid,NULL,transfer,(void*)&tp1)); tp2.tid=tid; assert(0==pthread_create(&tp1.tid,NULL,transfer,(void*)&tp2)); pthread_join(tp1.tid,NULL); pthread_join(tp2.tid,NULL); return 0; } int main(int argc,char* argv[]) { if(argc!=2) { cout<<"Usage: [./bin] [port]"<<endl; exit(-1); } signal(SIGPIPE,SIG_IGN); int listener=socket(AF_INET,SOCK_STREAM,0); sockaddr_in sa; sa.sin_family=AF_INET; sa.sin_addr.s_addr=INADDR_ANY; sa.sin_port=htons(atoi(argv[1])); assert(0==bind(listener,(sockaddr*)&sa,sizeof(sa))); assert(0==listen(listener,5));//server start listen cout<<"start server..."<<endl; while(true){//2 times fork(),init manage child sockaddr_in client; socklen_t len=sizeof(client); int cli=accept(listener,(sockaddr*)&client,&len); cout<<"accept..."<<endl; pid_t id=fork(); if(id==0){ if(fork()==0){ //do thing; return proc_socks(cli); } else exit(0); } else{ waitpid(id,NULL,0); } } return 0; }
client.cpp(客戶端 -》瀏覽器連接對象)
#include <iostream> #include <assert.h> using namespace std; #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <netdb.h> #include <string.h> #include "comm.h" #include "protocol.h" #include "transfer.h" #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int proc_forward(int in,char* rip,char* rport)//connect server { cout<<"start proc_forward"<<endl; int out=socket(AF_INET,SOCK_STREAM,0);//server fd assert(out != -1); sockaddr_in raddr; raddr.sin_family = AF_INET; raddr.sin_addr.s_addr = inet_addr(rip); raddr.sin_port = htons(atoi(rport)); assert(0 == connect(out, (sockaddr *)&raddr, sizeof(raddr))); //transfer ThreadParam tp1 = {in, out, 0}; ThreadParam tp2 = {out, in, 0}; assert(0 == pthread_create(&tp2.tid, NULL, transfer, &tp1)); assert(0 == pthread_create(&tp1.tid, NULL, transfer, &tp2)); pthread_join(tp1.tid, NULL); pthread_join(tp2.tid, NULL); return 0; } int main(int argc,char* argv[5]) { if(argc!=4){ cout<<"Usage: [./bin] [local_port] [remote_ip] [remote_port]"<<endl; return -1; } int listener=socket(AF_INET,SOCK_STREAM,0); assert(listener>=2); sockaddr_in sa; sa.sin_family=AF_INET; sa.sin_addr.s_addr=INADDR_ANY; sa.sin_port=htons(atoi(argv[1])); assert(0==bind(listener,(sockaddr*)&sa,sizeof(sa))); assert(0==listen(listener,5));//server start listen cout<<listener<<" start client..."<<endl; while(true){//2 times fork(),init manage child sockaddr_in client; socklen_t len=sizeof(client); int cli=accept(listener,(sockaddr*)&client,&len); cout<<cli<<" accept"<<endl; pid_t id=fork(); if(id==0){ if(fork()==0){ //do thing; return proc_forward(cli,argv[2],argv[3]); } else exit(0); } else{ waitpid(id,NULL,0); } } return 0; }
comm.h /comm.cpp (客戶端 與 服務端公共的 傳輸 加密接口)
#ifndef __COMM_H__ #define __COMM_H__ int recv_n(int sock,void* buf,int len);// int send_n(int sock,void* data,int len); int enx(void *data,int len);//encrypt #endif
#include <iostream> #include <sys/types.h> #include <sys/socket.h> #include "comm.h" using namespace std; int recv_n(int sock,void* buf,int len) { char* cur=(char* )buf; int left=len; while(left>0){ int ret=recv(sock,cur,left,0); if(ret<=0){ return -1; } left-=ret; cur+=ret; } enx(buf,len);//encryption return 0; } int send_n(int sock,void* data,int len) { enx(data,len);//decryption const char* cur=(const char*)data; int left=len; while(left>0){ int ret=send(sock,cur,left,0); if(ret<=0){ return -1; } left-=ret; cur+=ret; } return 0; } int enx(void *data,int len)//encrypt/decrypt { for(int i=0;i<len;++i){ ((char*)data)[i]^=6; } }
transfer.h/transfer.cpp (客戶端與服務端 通信接口)
#ifndef __TRANSFER_H__ #define __TRANSFER_H__ #include <pthread.h> struct ThreadParam { //thread_func's fd_parameter int in; int out; pthread_t tid; }; void *transfer(void *p);//thread_func #endif
#include "transfer.h" #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include "comm.h" void *transfer(void *p) { ThreadParam *tp=(ThreadParam*)p; int in=tp->in; int out=tp->out; pthread_t tid=tp->tid; char buf[1024*1024]; while(true) { int ret=recv(in,buf,1024*1024,0); if(ret<=0) break; enx(buf,ret); ret=send(out,buf,ret,0); if(ret<=0) break; } close(in); close(out); pthread_cancel(tid); return NULL; }
protocol.h(客戶端與服務端 使用socks5協議通信的結構體聲明)
#ifndef __PROTOCOL_H__ #define __PROTOCOL_H__ #pragma push() #pragma pack(1) struct HelloReq{//build request struct char ver;//version 05 char method_n;//consult test method }; struct HelloRep{//build response struct char ver; char method; }; struct AddrReq{//client service request sturct char ver; char cmd;//UDP or TCP method 03:udp 01:connect 02:bind char rsv; char atype; }; struct AddrRep{//server service response stuct char ver; char rep;//manage result char rsv;//00 char atype;//addr type domain name or ip in_addr_t addr;//des addr short port;//des port }; #pragma pop() #endif