想像这么一个场景你想截获(hijack)一个本地的出口连接(LOCALOUT)或者转发的连接(PREROUTING),对这个连接的两个方向的内容做修改,比如:1、将这个连接连接到远程socks代理(在通讯头部加上socks通信协议部分)2、对这个连接进行记录(用于协议分析)3、任何你能想到的折腾方式,比如我们叫他tcpgrep,:)。
那么我们该如何做呢?
一、获得连接
首先,我们要能获得这个连接,很容易想到的是用iptables连接REDIRECT方法(和DNAT类似),如果你做过squid的透明代理,应该会觉得很熟悉。
#iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-ports 8010
当然,你应该忽略一些不感兴趣的连接,可以单独建立一个chains,然后配和-j RETURN就可以做到。如果你熟悉iptables,这个应该很容易做到,我就不多说了。
现在呢?
nc -l -p 8010
(注:BSD style 的nc,如果你是GNU nc,用nc -l 8888)
然后nc www.baidu.com 80
很好,连接已经被第一个nc hijack了。但是,问题来了,他要去哪里呢?
二、获得ORGINAL DESTINALTION
这里就是透明代理技术的关键所在了。
0、socks代理方式的这个方式并不是我们所需要的东西,因为这个需要涉及到用户程序的修改。因为用户需要将目的地址发送给服务器(按照socks协议),如果用户程序本身没有考虑支持socks协议那么就没有直接的办法了。
1、方法一:做一个专用内核模块
我曾经做过一个,在http://s5snake.gro.clinux.org有相关信息,是专门用于socks[45]的东西。不过在kernel 2.6.9以后因为设计上的问题,对于LOCALOUT的连接就失效了,现在我已经不维护这个老的模块了。
这种方法太吃力,而且调试困难,需要同时维护netfilter/iptables的内核态模块和用户态模块,所以不推荐再使用了。
2、方法二: PRELOAD方式
通过PRELOAD一个库,修改socks,connect函数的调用来达到目的。
tsocks就是这么做的(http://tsocks.sourceforge.net/)
这样的方式非常像windows下的sockscap32程序,但是有个最大的问题是,只能在本机使用,对转发的连接就不适用了。
3、方式三:SO_ORIGINAL_DST
SO_ORIGINAL_DST是一个socket参数(SOL_IP层的)。
调用方式如下:
getsockopt (clifd, SOL_IP, SO_ORIGINAL_DST, &orig_addr, &sin_size);
clifd是hijack到的客户socket,orig_addr是sockaddr_in结构的参数,sin_size=sizeof(sockaddr_in).
返回0如果成功,-1失败。
如果成功orig_addr将是客户真正需要去的方向(恩……撒个小谎,后面你会看到)。先给段代码吧:
/*
* Simple "Hello, World!" server
* patch: demonstrate SO_ORIGINAL_DST
*/
#include <stdio.h /* */
#include <stdlib.h /* exit() */
#include <string.h /* memset(), memcpy() */
#include <sys/utsname.h /* uname() */
#include <sys/types.h
#include <sys/socket.h
/* socket(), bind(),listen(), accept(),getsockopt() */
#include <linux/netfilter_ipv4.h
#include <netinet/in.h
#include <arpa/inet.h
#include <netdb.h
#include <unistd.h /* fork(), write(), close() */
char message[BUFSIZ];
const int BACK_LOG = 5;
int main(int argc, char *argv[])
{
int serverSocket = 0,
on = 0,
port = 0,
status = 0,
childPid = 0;
struct hostent *hostPtr = NULL;
char
hostname[80] = "";
struct sockaddr_in serverName = { 0 };
struct sockaddr_in originDst
= { 0 };
socklen_t
sin_size
= sizeof(originDst);
if (2 != argc)
{
fprintf(stderr, "Usage: %s portnum\n",
argv[0]);
exit(1);
}
port = atoi(argv[1]);
serverSocket = socket(PF_INET, SOCK_STREAM,
IPPROTO_TCP);
if (-1 == serverSocket)
{
perror("socket()");
exit(1);
}
on = 1;
status = setsockopt(serverSocket, SOL_SOCKET,
SO_REUSEADDR,
(const char *) &on, sizeof(on));
if (-1 == status)
{
perror("setsockopt(...,SO_REUSEADDR,...)");
}
{
struct linger linger = { 0 };
linger.l_onoff = 1;
linger.l_linger = 30;
status = setsockopt(serverSocket,
SOL_SOCKET, SO_LINGER,
(const char *) &linger,
sizeof(linger));
if (-1 == status)
{
perror("setsockopt(...,SO_LINGER,...)");
}
}
/*
* find out who I am
*/
status = gethostname(hostname,
sizeof(hostname));
if (-1 == status)
{
perror("gethostname()");
exit(1);
}
hostPtr = gethostbyname(hostname);
if (NULL == hostPtr)
{
perror("gethostbyname()");
exit(1);
}
(void) memset(&serverName, 0,
sizeof(serverName));
(void) memcpy(&serverName.sin_addr,
hostPtr-h_addr,
hostPtr-h_length);
serverName.sin_addr.s_addr=htonl(INADDR_ANY);
serverName.sin_family = AF_INET;
serverName.sin_port = htons(port);
status = bind(serverSocket,
(struct sockaddr *) &serverName,
sizeof(serverName));
if (-1 == status)
{
perror("bind()");
exit(1);
}
status = listen(serverSocket, BACK_LOG);
if (-1 == status)
{
perror("listen()");
exit(1);
}
for (;;)
{
struct sockaddr_in clientName = { 0 };
int slaveSocket;
socklen_t clientLength =
sizeof(clientName);
(void) memset(&clientName, 0,
sizeof(clientName));
slaveSocket = accept(serverSocket,
(struct sockaddr *) &clientName,
&clientLength);
if (-1 == slaveSocket)
{
perror("accept()");
exit(1);
}
childPid = fork();
switch (childPid)
{
case -1: /* ERROR */
perror("fork()");
exit(1);
case 0: /* child process */
close(serverSocket);
if (-1 == getpeername(slaveSocket,
(struct sockaddr *) &clientName,
&clientLength))
{
perror("getpeername()");
}
else
{
if(getsockopt( slaveSocket, SOL_IP, SO_ORIGINAL_DST, &originDst, &sin_size) == 0){
printf("new connection:%s,%u",inet_ntoa(clientName.sin_addr),ntohs(clientName.sin_port));
printf("-%s,%u\n",inet_ntoa(originDst.sin_addr),ntohs(originDst.sin_port));
}else{
perror("getsockopt SO_ORIGINAL_DST:");
}
}
do{
read(slaveSocket,message,BUFSIZ);
write(1,message,strlen(message));
write(slaveSocket, message,strlen(message));
}while(message[0]);
close(slaveSocket);
exit(0);
default:
}
}
return 0;
}
编译运行前,记得
#iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-ports [端口号]
运行后,如果一切正常,那么你是很幸运,如果得到的是服务器运行的地址和端口,那么你很不幸,你很可能用的是2.6.9-2.6.12之间的
内核,很明显这是一个BUG,见:http://patchwork.netfilter.org/netfilter-devel/patch.pl?id=2676
那么怎么办呢?
1、升级到2.6.13的内核,2.6.13已经合并了上面的那个patch。
2、降级到低版本的内核,前提是有SO_ORIGINAL_DST选项,并测试是否正常
3、手动分析/proc/net/ip_conntrack文件,个人分析过可行性,并认为这种方法是一种可行的补救措施,不过一直没有动手写_=_!
三、恩……随你怎么玩吧
用poll或者select做中间人,做tcp-grep游戏(s/microsft/gnu/g,哈哈),分析QQ协议,等等等等。写个插件式的框架会更好的:)