PACKET套接字在用户态实现跨OS跨协议的NAT
一般而言,NAT功能需要操作系统内核协议栈的支持,并且在用户态的配置还很不一样,如果能在用户态实现一个通用的NAT软件,那就再好不过了,由于库函数的跨平台特性,那么这种NAT也将会是跨平台的。那么需要做的工作就是抽出各个OS中共用的网络库的支持,最简单也是最显然的就是PACKET套接字了,因为NAT的目的就是修改IP地址,本质上就是修改IP数据包然而重放之,既然要修改,起码我们要先拿到这个IP数据包,也就是完全截获它,而不仅仅是监听它。第一步就是禁用OS的路由功能,在Linux中就是将ip_forward设置为0,这样数据包就不会溜走了;第二步就是用PACKET套接字将本要通过路由溜走的数据报给截获到用户态;第三步就是修改它的IP地址信息(或许还有端口信息?这没关系,包已经拿到了,怎么改随你!);第四步就是将修改后的数据包再通过PACKET套接字发出去。以上的步骤中,需要注意的有以下几点:
1.修改后的数据包的校验码需要重新计算,而这并不难,因为校验码的计算算法都是公开的;
2.由于PACKET套接字需要你完全构造整个数据包,包括以太头,那么源和目的MAC地址如何填写将是一个问题,我是这么做的:
2.1.源MAC地址修改成发出包的那个网口的MAC地址;
2.2.目标MAC地址要分类讨论,如果目标是直连网段,那么就需要首先在用户态arp一下目标或者是直接从内核取出arp信息;如果目标不是直连网段的,需要将目标MAC填充成到达目标的网关下一跳的MAC地址,这就需要一次路由查找过程以及一次arp获取过程,而这都不难,在Linux上可以通过netlink套接字得到,Windows平台也有相关的API。
以上就是全部的过程了。
实际上,ip_queue也可以实现这个,然而ip_queue是内嵌于Linux的Netfilter框架内部的,脱离了Linux将很难移植到其它的OS,即便都在Linux上,如果没有Netfilter的支持也不行,因此PACKET套接字将是一种更加通用的方式实现NAT(或者VPN,总而言之都是修改原始的IP数据包)。
另外,如果能搭配BPF(伯克利包过滤)就更好了,tcpdump就是使用BPF来设置到底哪些包需要抓取而哪些包不需要抓取的。本文没有使用BPF,而是一下子将所有的包都抓过来。
这种用户态的实现是跨平台的,因为几乎所有的OS都有对PACKET套接字的支持,并且如果说你觉得想支持IPv6协议的话,改起来也比较简单,还是上述几个步骤,只是需要你对协议头有不太深入的理解即可。最终我觉得这种实现有个副作用,那就是必须关掉路由功能,这样那些不需要NAT的数据包也过不去了,但是还能怎样呢,任何事情都不是免费的啊,姑且用这个做一个单纯的NAT网关好了,最后,性能因素,交给越来越好的硬件吧...
最终还是给出一个能跑的代码,先来展示一下实现的可行性吧,代码十分简单:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <ifaddrs.h>
#define MAX_SIZE 4096
struct iphdr {
__u8 ihl:4,
version:4;
__u8 tos;
__be16 tot_len;
__be16 id;
__be16 frag_off;
__u8 ttl;
__u8 protocol;
__sum16 check;
__be32 saddr;
__be32 daddr;
};
#define ETHER_HEADER_LEN 14
#define ICMP_PROTO 1
struct match {
__be32 saddr; //匹配源IP地址
__be32 daddr; //匹配目标IP地址
__be32 t_saddr; //修改成的源IP地址
__be32 t_daddr; //修改后的目标IP地址
int opt; //SNAT=1/DNAT=0
};
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));
}
int main(int argc, char **argv) {
int sock, sd_lan ;
char buffer;
struct ifreq ethreq;
struct sockaddr_ll sa;
struct ifaddrs *ifa = NULL;
struct match mt = {0};
mt.saddr = inet_addr(argv);
mt.daddr = inet_addr(argv);
mt.t_saddr = inet_addr(argv);
mt.t_daddr = inet_addr(argv);
mt.opt = atoi(argv);
ock=socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP));
strncpy(ethreq.ifr_name,"eth0",IFNAMSIZ);
ioctl(sock, SIOCGIFFLAGS, ðreq);
ethreq.ifr_flags|=IFF_PROMISC;
ioctl(sock,SIOCSIFFLAGS, ðreq);
memset( &sa, 0, sizeof(sa) );
sa.sll_family = AF_PACKET;
sa.sll_protocol = htons(ETH_P_IP);
sa.sll_ifindex = if_nametoindex("eth0");
bind(sock, (struct sockaddr*)&sa, sizeof(sa));
getifaddrs(&ifa);
while (1) {
ssize_t len = 0;
struct ethhdr *eh = NULL;
struct iphdr *ip_header = NULL;
len = recvfrom(sock,buffer, MAX_SIZE, 0, NULL, NULL);
//ETH (Bridge ?)
eh = (struct ethhdr*)buffer;
//暂且写死了这个目标MAC,实际上应该从内核arp表获取的
char dst_mac = {0x00,0x0c,0x29,0x90,0x66,0xcf};
memcpy(eh->h_dest, dst_mac, ETH_ALEN);
//设置源MAC的时候,注意要越过loopback口
memcpy(eh->h_source, ((struct sockaddr_ll*)ifa->ifa_next->ifa_addr)->sll_addr, ETH_ALEN );
ip_header = (struct iphdr*)(buffer + ETHER_HEADER_LEN);
//作为一个例子只是针对ICMP
if (ip_header->protocol != ICMP_PROTO) {
continue;
}
if (ip_header->daddr == mt.daddr &&
ip_header->saddr == mt.saddr) {
if (mt.opt == 1)
ip_header->saddr = mt.t_saddr;
else
ip_header->daddr = mt.t_daddr;
}
//重新计算校验码
ip_header->check = htons(checksum(0, (u_int8_t*)ip_header, ip_header->ihl<<2));
len = send(sock, buffer, len, 0);
}
}
页:
[1]