Chinaunix首页 | 论坛 | 博客
  • 博客访问: 445706
  • 博文数量: 403
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: -70
  • 用 户 组: 普通用户
  • 注册时间: 2016-09-05 12:45
文章分类

全部博文(403)

文章存档

2014年(3)

2013年(1)

2012年(3)

2011年(21)

2010年(13)

2009年(64)

2008年(9)

2007年(36)

2006年(253)

分类: C/C++

2006-08-07 15:15:44

本文以QQ為對像,教你如何寫一個SOCKS5 PROXY

一、準備工作
===================
1.編譯器:
為提高程序的可移植性和避免MS秋後算帳,本文將使用GCC作為編譯器,Win32版的GCC可到下載,或者用Dev-C++自帶的GCC也行.

2.程序運行環境:
可在Windows 2000/XP和LINUX下運行,Win9x系統沒試過,不知行不行.

3.如何編譯:
在LINUX下用 gcc mysock5.c -o mysock5
在Win32下用 gcc mysock5.c -o mysock5.exe -lwsock32
其中一定要加參數-lwsock32來指明使用Windows Socket相關的庫,否則連結時會出錯,在Linux下編譯不用.


二、基本思路
===================
Proxy是什麼?我想不用我多說吧,還是馬上進入正題吧.SOCKS5是一種Proxy協議,支持TCP和UDP協議,詳情可參閱rfc1928.txt.

1.握手過程
客戶程序要使用PROXY服務,首先要跟PROXY SERVER進行握手,這個過程是基於TCP/IP協議.

2.資料傳輸
QQ是使用UDP來傳送資料的,所以當QQ和SOCKS5 PROXY握手成功後,就會轉向連接一個UDP SOCKET,也就是說我們這個程序首先要建立一個TCP SOCKET和QQ進行握手,然後再要建立一個UDP SOCKET來進行數據中轉,實現代理服務的功能.

這是一個最簡單的PROXY,只支持一個用戶連接一次,連接中止後需退出重新啟動.


三、程序框架
===================
先看看源代碼mysock5.c

#include
#include
#include
#include

//Windows和LINUX系統所提供的Socket API不一樣,需要分別聲明
#ifdef _WIN32
#include
#include
#else
#include
#include
#include
#endif

//定義一段緩衝區
#define BUFSZ 65535
char buf[ BUFSZ ];

int main(int argc, char** argv)
{
//這是Server的資料,包括IP地址以及用作TCP和UDP連接的端口號
short tcp_proxy_port=8888, udp_proxy_port=8811;
char udp_proxy_ip[]="127.0.0.1";
short clt_udp_port;

//在Windows系統下,使用網絡前要先用WSAStartup()來進行初始化,
//WSA是Windows Socket API的意思,本文使用WinSock 2.0版本
//在VC中可以用WINSOCK_VERSION來指定版本,不過GCC FOR WIN32沒有定義這個宏
//我們需要自己指定
#ifdef _WIN32
WSADATA WSAData;
if( WSAStartup(0x0110, &WSAData) ) {
//if( WSAStartup(WINSOCK_VERSION, &WSAData) ) {
p_error("WSA error");
exit(-1);
}
#endif


//啟動一個TCP SOCKET,用來和QQ進行握手,並記錄QQ用來和我們溝通的UDP端口號(clt_udp_port)
Launch_TCP( tcp_proxy_port, udp_proxy_ip, udp_proxy_port, &clt_udp_port );

//握手成功後,啟動一個UDP SOCKET,用作數據傳輸,是真正起PROXY作用的部份
Launch_UDP( udp_proxy_port, udp_proxy_ip, clt_udp_port );

return 0;
}




四、工具函數
===================
正所謂工欲善其事,必先利其器,好的程序當然不能缺乏好的工具函數,有了這些函數,進行調試就更加方便了.

1. p_error
這是一個處理錯誤信息的函數,把所有的錯誤信息集中用一個函數來處理是一個好習慣,目前我們直接用printf()將其輸出,當然也可以輸出到文件或者干脆把它忽略:)

void p_error( const char *err_msg )
{
printf( "ERR=>%s\n", err_msg );
}



2. debug_showbin
用來輸出一段數據的內容(16進制)

void debug_showbin( const char *dbuf, int n, const char *name, const char *end )
{
int i;

printf( "%s ==> %d bytes: ", name, n );

for( i=0; i printf( "(0x%x)", (unsigned char)dbuf[i] );

printf( "%s", end );
}



3. debug_showip
用來顯示sockaddr_in類型數據中的IP和Port

void debug_showip( const struct sockaddr_in *dbuf, const char *name, const char *end )
{
printf("[ %s ==> %s:%u ]%s", name, inet_ntoa(dbuf->sin_addr), ntohs(dbuf->sin_port), end

);
}



五、測試
===================
下載源代碼後可嘗試編譯,然後執行,看看有沒有錯誤信息:)
 
本章主要介紹Launch_TCP()的工作原理

一、握手過程
===================
先看看Proxy的輸出結果:

< TCP/IP Session - START >

RECV ==> 3 bytes: (0x5)(0x1)(0x0)
SEND ==> 2 bytes: (0x5)(0x0)

RECV ==> 10 bytes: (0x5)(0x3)(0x0)(0x1)(0x0)(0x0)(0x0)(0x0)(0x6)(0x32)
SEND ==> 10 bytes: (0x5)(0x0)(0x0)(0x1)(0x7f)(0x0)(0x0)(0x1)(0x22)(0x6b)

< TCP/IP Session - END >


如果不需要身份驗證的話,SOCK5 PROXY和客戶端的握手過程只有四句,
由於SOCKS5協議包括很多內容,這裏只對本例中所用到的部份作出說明,
餘者可參考rfc1928.txt

以下逐句分析:
1. 第一句,客戶端→PROXY
  (0x5)版本號
  (0x1)代表有1 byte的資料
  (0x0)登入模式,0x0代表不用身份驗證,0x2代表需要usrname/password

2. 第二句,PROXY→客戶端
  (0x5)版本號
  (0x0)成功

3. 第三句,客戶端→PROXY
  (0x5)版本號
  (0x3)要求使用的協議類型,0x3代表UDP
  (0x0)保留字
  (0x1)地址類型,0x1代表IPv4,0x3代表Domain name,0x4代表IPv6
  (0x0)(0x0)(0x0)(0x0)這4個bytes代表客戶端的地址
  (0x6)(0x32)客戶端用作UDP傳輸的端口號

4. 第四句,PROXY→客戶端
  (0x5)版本號
  (0x0)成功
  (0x0)保留字
  (0x1)地址類型
  PROXY提供的UDP SOCKET地址和端口號,一定要準確無誤,客戶端需要連接到這個地址.
  (0x7f)(0x0)(0x0)(0x1)
  (0x22)(0x6b)

●註:如果地址類型為Domain name(0x3)的話,第1 byte是域名長度,之後n bytes就是地址



二、源代碼
===================

void Launch_TCP( int service_port, const char *udp_proxy_ip, int udp_proxy_port, short *clt_udp_port )
{
//port is NOT network orders

struct sockaddr_in servaddr,clientaddr;
int clientlen;
int listenfd, connfd;
int n;

//定義socket, bind, listen, accept,關於這些操作的資料太多了,不詳述
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(service_port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0) {
p_error("socket error");
exit(-1);
}

if( bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 ) {
p_error("bind error");
exit(-1);
}

if( listen(listenfd, 5) < 0 ) {
p_error("listen error");
exit(-1);
}

connfd = accept( listenfd, (struct sockaddr *)&clientaddr, &clientlen );
if( connfd < 0 ) {
p_error("accept error");
exit(-1);
}

printf("< TCP/IP Session - START >\n\n");

//接受第一句請求
n = recv( connfd, buf, BUFSZ, 0 );
debug_showbin( buf, n, "RECV", "\n" );

//目前我們只支持無身份驗證的請求,即"05 01 00"
if( buf[0]==0x5 && buf[1]==0x1 && buf[2]==0x0) {
buf[0] = 0x5;
buf[1] = 0x0;

//返回"05 00",代表成功
send( connfd, buf, 2, 0 );
debug_showbin( buf, 2, "SEND", "\n\n" );
} else {
p_error("Session ERROR!\n");
exit(-1);
}

//接受第二句請求
n = recv( connfd, buf, BUFSZ, 0 );
debug_showbin( buf, n, "RECV", "\n" );

//只處理UDP請求(0x03)
if( buf[0]==0x5 && buf[1]==0x3 ) { //Client request a UDP Proxy

short udp_port;
long udp_ip;

//提取並儲存客戶端的UDP端口號
int seg=4;
if( buf[3] == 0x3 )
seg = buf[4]+1;
memcpy( clt_udp_port, &buf[4+seg], 2 );
*clt_udp_port = ntohs( *clt_udp_port );

buf[0] = 0x5;
buf[1] = 0x0;
buf[2] = 0x0;
buf[3] = 0x1;

//把本機UDP SOCKET的IP和PORT返回給QQ
udp_ip = inet_addr( udp_proxy_ip );
udp_port = htons( udp_proxy_port );
memcpy( &buf[4], &udp_ip, 4 );
memcpy( &buf[8], &udp_port, 2 );

send(connfd, buf, 10, 0 );
debug_showbin( buf, 10, "SEND", "\n\n" );
} else {
p_error("Session ERROR: Client doesn't need a UDP Proxy!\n");
exit(-1);
}

//握手過程完成
close(connfd);
close(listenfd);

printf("< TCP/IP Session - END >\n\n");
}



三、測試
===================
1.現在可以先把程序編譯,運行後程序將會在accept()這句搪塞,直至有請求連入.

2.打開QQ,在[系統參數->網絡設置]中選取使用SOCKS5代理,在地址欄填入127.0.0.1或localhost,端口填8888,切記要把用戶名和密碼欄清空,因我們的程序只能處理無身份驗證的請求(即握手的第一句為"05 01 00"),如果用戶名和密碼欄不為空的話,QQ將會發送"05 01 02"至Proxy.

3.按一下[測試],看看成不成功,再自己研究一下握手的內容:)
 
本章主要介紹Launch_UDP()的工作原理

一、SOCKS5 UDP封包結構
===========================
順序為:
2 Bytes 保留字,一定要為0x0
1 Bytes Current fragment number
1 Bytes 地址類型
X Bytes 目的地地址
2 Bytes 目的地端口號
N Bytes 數據


二、源代碼
===========================

void Launch_UDP( int udp_proxy_port, const char *udp_proxy_ip, int clt_udp_port )
{
//port is NOT network orders

//記錄本機,客戶端,遠端服務器和封包來源地址
struct sockaddr_in servaddr,clientaddr,remoteaddr,inaddr;
int inlen;
int listenfd;
int n;
fd_set set;
//把接收來的數據寫在緩衝區第11個Byte之後,前10 Bytes用來存放Header
char *thisbuf = &buf[10];
int thissize = BUFSZ - 10;

printf("< UDP Session - START >\n\n");

//建立一個UDP SOCKET,注意UDP協議不需要listen, accept和conenct
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons( udp_proxy_port );
servaddr.sin_addr.s_addr = htonl( INADDR_ANY );

memset(&remoteaddr, 0, sizeof(remoteaddr));
remoteaddr.sin_family = AF_INET;

listenfd = socket(AF_INET, SOCK_DGRAM, 0);
if(listenfd < 0) {
p_error("socket error");
exit(-1);
}

if( bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 ) {
p_error("bind error");
exit(-1);
}

//使用select來監控Socket是否有資料可讀
FD_ZERO(&set);
FD_SET(listenfd, &set);

while( 1 ) {

if( select( listenfd+1, &set, NULL, NULL, NULL) < 0 ) {
p_error("select error");
exit(-1);
}

if( FD_ISSET( listenfd, &set ) ) {
//UDP協議可使用recvfrom()來接收數據,並獲得來源地地址
n = recvfrom( listenfd, thisbuf, thissize, 0, (struct sockaddr *)&inaddr, &inlen );

if( n >=0 ) {

debug_showip( &inaddr, "Received From", "\n" );

//資料來自客戶端
if( (thisbuf[0]==0x0) && (thisbuf[1]==0x0) &&
(htons(inaddr.sin_port)==clt_udp_port) )
{
//保存客戶端的地址
memcpy( &clientaddr, &inaddr, sizeof(clientaddr) );

if( thisbuf[3] != 0x1 ) {
//如果目的地地址類型為域名,先進行解析獲得IP再發送
struct hostent *h;
char tmp[256];
int seg;

strncpy( tmp, &thisbuf[5], thisbuf[4] );
tmp[ thisbuf[4] ] = 0;

h = gethostbyname ( tmp ); //

if( h == NULL )
p_error("unknown domain name\n");
else
{
remoteaddr.sin_addr.s_addr = (*(struct in_addr*)h->h_addr).s_addr;

seg = thisbuf[4]+1;
memcpy( &remoteaddr.sin_port, &thisbuf[4+seg], 2 );

debug_showbin( thisbuf, 4+seg+2, "RECV CLIENT [ Header ]","\n");
debug_showbin( &thisbuf[4+seg+2], n-(4+seg+2), "RECV CLIENT [ Data ]","\n");
debug_showip( &remoteaddr, "Send to DOMAIN", "\n\n");

sendto( listenfd, &thisbuf[4+seg+2], n-(4+seg+2), 0, (struct sockaddr*)&remoteaddr,sizeof(remoteaddr));
}
} else {
//目的地地址為IPv4,直接把資料發送過去
memcpy( &remoteaddr.sin_port, &thisbuf[8], 2 );
memcpy( &remoteaddr.sin_addr.s_addr, &thisbuf[4], 4 );

debug_showbin( thisbuf, 10, "RECV CLIENT [ Header ]","\n");
debug_showbin( &thisbuf[10], n-10, "RECV CLIENT [ Data ]","\n");
debug_showip( &remoteaddr, "Send to IP", "\n\n");

sendto( listenfd, &thisbuf[10], n-10, 0, (struct sockaddr*)&remoteaddr,sizeof(remoteaddr));
}
}
else
{ //資料來自遠端服務器
debug_showbin(thisbuf, n, "RECV REMOTE","\n");
debug_showip(&clientaddr, "Send to CLT","\n\n");

//編寫Header
buf[0] = 0x0;
buf[1] = 0x0;
buf[2] = 0x0;
buf[3] = 0x1;
memcpy( &buf[4], &udp_proxy_ip, 4 );
memcpy( &buf[8], &udp_proxy_port, 2 );

//發送到客戶端
sendto( listenfd, buf, n+10, 0, (struct sockaddr*)&clientaddr,sizeof(clientaddr));
} } }
}

close(listenfd);

printf("< UDP Session - END >\n\n");
}


三、測試
===================
到目前為止,整個PROXY已經完成,可以用QQ來測試一下,連接後QQ與遠端服務器之間傳輸的資料都會顯示在屏幕上,我們還可以對數據進行截留,從而把煩人的廣告全去掉:)
阅读(1833) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~