Linux下的透明代理技术

原文地址

http://linux.chinaitlab.com/set/36965.html


想像这么一个场景你想截获(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协议,等等等等。写个插件式的框架会更好的:)

你可能感兴趣的:(Linux下的透明代理技术)