第一部分.现有×××实现简述

对于×××而言,关键在于加密和封装,而这二者的前提首先是可以获得原始IP数据包。现在有几种常见的方式:
1.使用专有传输层协议
这类典型例子就是IPSec协议族,包括AH,ESP,IKE等,其中前两者用于封装,后一种用于密钥协商。
2.使用虚拟网卡+路由
典型例子就是Open×××,具体就不多说了。

第二部分.ip_queue引入以及原理简介

其实,还有另一种更加直观的方式,那就是使用ip_queue,这种方式直接“截获”了原始IP数据包到用户空间,然后你可以在用户空间进行加密和封装,然后再将其注入到内核协议栈,一切都是那么的顺理成章,very good!
        ip_queue的实现很简单,使用了Linux内核的Netlink机制与用户态通信,在用户态需要一个进程不断接收被queue的原始数据包,而这一切已经被封装在libipq和libnetfilter_queue里面了,很方便使用。在Netfilter的任意钩子点,都可以使用:
iptables -t XXX -A YYY $several match -j QUEUE
就可以将数据包queue到用户态了,这里只讲原理,因此不会涉及NFQUEUE以及queue-num。用户态进程可以再将包注入到内核,注入到哪里呢?答案是从哪里来,注入到哪里,内核加载了ip_queue之后,会启动一个netlink套接字来接收用户态注入的数据包,最终进入ipq_receive_peer函数:
static int ipq_receive_peer(struct ipq_peer_msg *pmsg,                  unsigned char type, unsigned int len) {     ...         case IPQM_VERDICT:         if (pmsg->msg.verdict.value > NF_MAX_VERDICT)             status = -EINVAL;         else             status = ipq_set_verdict(&pmsg->msg.verdict,                                      len - sizeof(*pmsg));     ... }

最终在ipq_issue_verdict中调用了nf_reinject将数据包注入到它当初被NF_QUEUE的地方。整个过程中正是nf_info结构体记住了当初数据包离开时的信息,那么这个结构体应该在数据包被NF_QUEUE的时候被初始化,然后在nf_reinject中,如果数据包被ACCEPT了,则:
info->okfn(skb);
这就是整个过程。
        对于内核协议栈的处理,仅仅需要明白其原理即可,我们需要做的就是在截取到数据包和重新注入数据包的逻辑之间加入那么一点点代码。不幸的是,关于libipq和libnetfilter_queue的资料很少,文档也不齐全,不幸中的万幸就在于libipq的man手册中给了一个完整的源码,在这个源码的基础上,我们可以将之扩展成任何样子。

第三部分.用户态代码以及内核态规则

下面给出先我的扩展代码:
/*  * This code is GPL.  * 2012/06/24 Modified by 王然  * 标准的代吗来自libipq的man手册的最下方  * 代码中使用了从网上搜来的计算校验码的代码  */ #include <linux/netfilter.h> #include <string.h> #include <stdlib.h> #include <libipq.h> #include <stdio.h> #include <libnetfilter_queue/libnetfilter_queue.h> #include <linux/ip.h>  #define BUFSIZE 2048  static u_int16_t checksum(u_int32_t init, u_int8_t *addr, size_t count) {     u_int32_t sum = init;     while( count > 1 ) {         sum += ntohs(* (u_int16_t*) addr);         addr += 2;         count -= 2;     }     if( count > 0 )         sum += * (u_int8_t *) addr;     while (sum>>16)         sum = (sum & 0xffff) + (sum >> 16);     return (u_int16_t)~sum; }  static u_int16_t ip_checksum(struct iphdr* iphdrp) {     return checksum(0, (u_int8_t*)iphdrp, iphdrp->ihl<<2); }  static void set_ip_checksum(struct iphdr* iphdrp) {     iphdrp->check = 0;     iphdrp->check = htons(checksum(0, (u_int8_t*)iphdrp, iphdrp->ihl<<2)); }  static void die(struct ipq_handle *h) {     ipq_perror("passer");     ipq_destroy_handle(h);     exit(1); }  int main(int argc, char **argv) {     int status;     unsigned char buf[BUFSIZE];     struct ipq_handle *h;      h = ipq_create_handle(0, NFPROTO_IPV4);     if (!h)         die(h);      status = ipq_set_mode(h, IPQ_COPY_PACKET, BUFSIZE);     if (status < 0)         die(h);      do{         status = ipq_read(h, buf, BUFSIZE, 0);         if (status < 0)             die(h);          switch (ipq_message_type(buf)) {             case NLMSG_ERROR:                 fprintf(stderr, "Received error message %d\n",                 ipq_get_msgerr(buf));                 break;              case IPQM_PACKET: {                 ipq_packet_msg_t *m = ipq_get_packet(buf); //注释0                 /*                  * 2012/06/24 by 王然                  * 申请一个IP头,将原始数据“加密后”追加到这个IP头后面                  */                 size_t data_len = m->data_len;                 size_t crypto_len = data_len; //没有做任何加密                 char *crypto_data = m->payload; //没有做任何加密                 struct iphdr *new_hd = (struct iphdr *)malloc(sizeof(struct iphdr)+crypto_len);                 memcpy(new_hd, m->payload, sizeof(struct iphdr));                 memcpy(new_hd+sizeof(struct iphdr), crypto_data, crypto_len);                 new_hd->daddr = inet_addr("11.22.33.44");  //姑且使用这个“隧道对端”地址                 new_hd->saddr = inet_addr("192.168.40.249"); //姑且选择这个地址,注释1                 new_hd->protocol = $任意未使用的号码        //注释2                  set_ip_checksum(new_hd); //重新计算IP头的校验码                 //最后将封装后的数据包重新注入内核                 status = ipq_set_verdict(h, m->packet_id,                         NF_ACCEPT, data_len+sizeof(struct iphdr), (char*)new_hd);                 if (status < 0)                     die(h);                 break;             }              default:                 fprintf(stderr, "Unknown message type!\n");                 break;         }     } while (1);      ipq_destroy_handle(h);     return 0; }

注释0:关于ipq_packet_msg_t结构体
这个结构体在ipq_get_packet的man手册中有所介绍:
typedef struct ipq_packet_msg {     unsigned long packet_id;        /* ID of queued packet */     unsigned long mark;             /* Netfilter mark value */     long timestamp_sec;             /* Packet arrival time (seconds) */     long timestamp_usec;            /* Packet arrvial time (+useconds) */     unsigned int hook;              /* Netfilter hook we rode in on */     char indev_name[IFNAMSIZ];      /* Name of incoming interface */     char outdev_name[IFNAMSIZ];     /* Name of outgoing interface */     unsigned short hw_protocol;     /* Hardware protocol (network order) */     unsigned short hw_type;         /* Hardware type */     unsigned char hw_addrlen;       /* Hardware address length */     unsigned char hw_addr[8];       /* Hardware address */     size_t data_len;                /* Length of packet data */     unsigned char payload[0];       /* Optional packet data */ } ipq_packet_msg_t;
个人觉得,有了这个结构体,基本不需要什么libnetfilter_queue了,该结构体很底层,用起来也比较方便,比如可以直接取出原始数据包以及其长度,相反,使用libnetfilter以及libipq的一大堆函数更容易把人们搞晕掉。
注释1:关于源地址的填写
在填写源地址的时候,注意不能直接填写本机的IP地址,因为这样的话数据包将路由不出去,具体参见fib_validate_source函数:
if (res.type != RTN_UNICAST)     goto e_inval_res;
意思是说,在route_input进行反向路由查找的时候,结果必须是UNICAST,如果源地址填写了本机地址,那么结果将是LOCAL,系统会在rtstat的in_marti-an_src中记下一笔。那么怎么填写源地址呢?有两种方式:
a>.填写一个本网段的非本机地址,没有被其他机器使用,这个地址需要申请,并且要告知×××隧道对端,以便对端填写目标IP地址;
b>.使用iproute2将本机的一个被×××使用的地址从local表删除。

注释2:关于协议号
如果你真的使用IPSec协议封装了数据包,那么请在此填写ESP或者AH的协议号,如果是别的,那么请自己定义一个。一定要修改它,否则就会出现莫名其妙的问题,比如原始数据封装了ICMP数据,如不修改protocol字段,那么你抓包会发现有以下的错误:
ICMP echo reply, id 0, seq 0, length 40 (wrong icmp cksum 0 (->ffff)!)
        有了用户态代码,接下来需要设置内核态的规则了,很简单,把需要引入隧道的感兴趣流queue到用户态即可,比如我们此时的感兴趣流是所有到达192.168.60.0/24网段的流量,那么就设置下面的规则:
iptables -t mangle -A PREROUTING -d 192.168.60.0/24 -j QUEUE
之所以使用mangle表,是因为在mangle里面引导数据包到用户态修改它更加符合其字面含义,其实凡是路由前的PREROUTING的任何位置都可以,也可以自己写一个module...这样我们就实现了一个完全在用户态实现的类似IPSec的×××雏形,它完全可以实现IPSec的所有语义,并不像Open×××那样使用TCP或者UDP来封装。由于使用iptables的queue target或者自定义module的target(如果你不想被人们用iptables-save看到你的规则的话),而不像Open×××引入了虚拟网卡这个概念,个人觉得这种方式更加直接些,而不是通过路由来截获数据包。

第四部分.如何加密

在第三部分的代码中,我没有给出加密数据包的代码,因为这太复杂了,如果希望封装成IPSec,那么光IPSec协议本身就有很多内容,还包括密钥协商等复杂的主题。实际上,你可以使用任意方式对数据进行加密,简单的可以用B64编码一下,复杂的就多了。更加吸引人的是,这段代码可以和Open×××结合起来,这就是说,不再将数据包重新注入内核协议栈,而是交给Open×××,让Open×××加密封装后再通过TCP或者UDP发送出去,这样一来ip_queue的作用就是Open×××的tap字符设备的作用了,Open×××也就多了一种获取数据的方式,不是通过路由而是通过ip_queue。
        最一般的方式就是使用libcryto库中大量的加密接口来对数据进行加密。在Linux上,总是有那么多东西可以直接使用,几乎可以满足你的所有需求,既然我们已经顺利地将数据包截获并且也可以重新封装后注入内核,那么加密这一块为何不使用另一个现成易用的libcrypto呢?具体怎么做就不写了,用就是了。

第五部分.如何和网络组合起来

到目前为止,我没有给出隧道对端如何定义,也没有给出对端的代码而仅仅给出了一端的代码,因为根本没有必要操心对端。我们使用ip_queue截获了数据包,我们需要将其目标地址封装成可以经过对端设备的任意地址,只要对端设备设置了相应的规则将该目标地址的包queue到用户态解封装即可,如果我们使用这种方式实现了IPSec,那么对端就可以是一台IPSec×××设备,比如Cisco的设备,如果我们实现了Open×××的协议,那么对端可以是一个运行Open×××的机器。总之,使用ip_queue实在太灵活了,它甚至可以兼容任何的×××协议,只要我们能搞到这种×××的协议即可。
        如果在非Linux平台,怎么做呢?使用PACKET套接字吧,可以使用scapy来做实验...何必呢?还是用Linux吧,何必引入那么复杂东西呢?Linux几行命令,现成的代码,为何不用呢?

神说:学好×××,别看书,要折腾...