如果不懂NAT怎么办,其实NAT基本谁都懂,关键是如果不懂配置怎么办,不精通iptables怎么办?幸亏我们玩的是linux内核,我们完全可以自己写一个简单的NAT,通过一个简单的思想就能实现一个简单的NAT,通过扩展这个思想,我们还能实现内容过滤呢。nat简单的说就是地址转换,通过将源地址转换或者目的地址转换可以实现私有网络拓扑隐藏或者网络代理,nat是十分复杂的,这里我就不仔细讲了,但是无论如何还是要说一下在linux中nat的大致实现,2.6内核中netfilter机制中有专门的nat表,由于nat转换了地址,所以这将影响路由选择,所以必须在网络路由之前或者之后进行必要的转换而不能在之中,所以在linux的netfilter机制的5个hook中,pre_routing和post_routing这两个点上分别实现了DNAT和SNAT,因为在路由之前转换目的ip地址,目的ip地址才能参与路由选择,而只有在post_routing中进行源地址转换,转换后的源地址才不会影响之前的路由选择,注意这里的源地址转换就是隐藏了转换前的网络拓扑结构,让后面通路认为数据是post_routing中转换之后的地址,另外还有两个hook点可以安装nat,这就是local_in和local_out,这主要是为了服务本机的,在路由之前转换源地址也就是进行snat可以隐藏信息,但是返回的信息必须在路由之后进行dnat。
好了,大致nat的理论介绍完了,那么下面就实现一个简单的nat吧,注意本文的前提是我不懂netfilter原理,也不会配置iptables,由于不懂netfilter,那么就不能使用linux网络栈中内置的那5个hook,但是只要知道了那5个hook的原理和调用点,实现nat根本不难。
int addr_change_dst(struct sk_buff *skb,int n)
{
struct iphdr *iph;
struct tcphdr *th;
struct li_addr_port data,data1;
char *fp=NULL,*fp1,li_c,*pp;
struct ruler_strings *li_strp = head_strp;
iph = ip_hdr(skb);
if( iph != NULL && iph->protocol==IPPROTO_TCP){
th = (struct tcphdr *)(((int *)iph) + iph->ihl);
data.addr = iph->daddr;
data.port = ntohs(th->dest);
if(addr_find(&data,&data1,0)<0) //如果定义了地址转换,那么就要转换了,这里转换目的
goto continue;
iph->daddr = data1.addr;
if(iph->protocol == 0x06){
th->dest = htons(data1.port);
tcpv4_check_addr ((__u16 *)((char *)iph-14)); //更新校验值
}
ip_send_check(iph);
}
continue:
return 0;
};
int addr_change_src(struct sk_buff *skb,int n)
{
struct iphdr *iph;
struct tcphdr *th;
struct li_addr_port data,data1;
// iph = skb->nh.iph; //早期的内核这样得到ip头结构
iph = ip_hdr(skb); //新近的内核用了一种更方便的方式得到ip头结构
if( iph != NULL && iph->protocol==IPPROTO_TCP){
th = (struct tcphdr *)(((int *)iph) + iph->ihl);
data.addr = iph->saddr;
data.port = ntohs(th->source); //dest port.
if(addr_find(&data,&data1,1) < 0) //如果定义了地址转换,那么就要转换了,这里转换源
goto continue1;
iph->saddr = data1.addr; //将目的地址转换为新的地址
if(iph->protocol == 0x06){ //tcp
th->source = htons(data1.port); //将目的端口转换为新的端口
tcpv4_check_addr((__u16 *)((char *)iph-14)); //由于改变了协议头,因此需要重新生成校验值
}
ip_send_check(iph);
}
continue1:
return 0;
};
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
...
iph = ip_hdr(skb);
//***********************
addr_change_dst(skb,0); //ip_rcv是ip层的第一个函数,在PRE_ROUTING钩子之前有机会修改目的地址
if (iph->ihl < 5 || iph->version != 4)
goto inhdr_error;
...
return NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
...
}
static int ip_finish_output(struct sk_buff *skb)
{
li_addr_change_src(skb,0); //源转换,给客户本机服务的假象,实际上本机就是一个代理
if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
return ip_fragment(skb, ip_finish_output2);
else
return ip_finish_output2(skb);
}
仅仅将addr_change放到ip_rcv还不行,试想如果本机访问会发生什么,数据的源头在本机,并不在别的机器,因此作为路由器的本机根本不会在ip_rcv接收到这个本机访问源,因此为了在本机也达到相同的效果,需要在LOCAL_OUT这个钩子处调用addr_change,其实只要在ROUTE前就可以了,这样的效果就是在本机也成功转换了目的地址和目的端口。
以上的ip_finish_output中做了源转换,将真正的服务器返回给客户的源地址和端口转换为本机的地址和端口,ip_finish_output是无论本机发送还是forward路由模式最终都要调用的函数
struct addr_port_g{
__u32 cl_addr; // 需要转换的地址
__u16 cl_port; // 需要转换的端口
__u32 dt_addr; // 转换后的地址
__u16 dt_port; // 转换后的端口
struct addr_port_g * prep; //前向线索
struct addr_port_g * next; //后向线索
char ctlf;
}
int addr_find(struct li_addr_port * a0,struct li_addr_port * a1,int flag)
{
struct addr_port_g * hp = addr_headp;
spin_lock(&addrnat_lock);
for(;hp!=NULL;){
if(flag==0){ //目的转换
if(hp->ctlf && hp->cl_port==a0->port && hp->cl_addr==a0->addr){
a1->port = hp->dt_port;
a1->addr = hp->dt_addr;
spin_unlock(&addrnat_lock);
return 1;
}else
hp = hp->next;
}else{ //源转换
if(hp->ctlf && hp->dt_port==a0->port && hp->dt_addr==a0->addr){
a1->port = hp->cl_port;
a1->addr = hp->cl_addr;
spin_unlock(&addrnat_lock);
return 1;
}else
hp = hp->next;
}
}
spin_unlock(&addrnat_lock);
return -1;
}
看完了数据结构和查找操作,那么插入和删除操作闭着眼睛也能写出来了
看看用户空间的程序:
...
/****************************************************************
* Usage cmd -sa s_addr -sp s_port -da d_addr -dp d_port -v flag *
*****************************************************************/
#define REQSIZE sizeof(struct request)
char buf[1024];
struct request *req = (struct request *) buf;
main(int argc,char ** argv)
{
struct in_addr addr;
char string[120],li_c='/r',li_c1='/n';
int ret,i,sa=0,sp=0,da=0,dp=0;
char * argp=NULL;
struct addr_port_g *l_argc;
char ppp[10]="good";
{
req->fncode = 0;
req->level = 0;
req->cmd = L_CMD_NAT_ADDRCHANG ;
req->dtype = L_REQTYPE_ADDRNAT;
req->size = 1;
}
l_argc=(struct addr_port_g *)(buf+REQSIZE);
for(i=1;i<11;i+=2){
switch(argv[i][1]){
case 's':
switch(argv[i][2]){
case 'a':
inet_pton(AF_INET,argv[i+1],(void *)&l_argc->cl_addr)
break;
case 'p':
l_argc->cl_port = atoi(argv[i+1]);
break;
default:
exit(0);
}
break;
case 'd':
switch(argv[i][2]){
case 'a':
inet_pton(AF_INET,argv[i+1],(void *)&l_argc->dt_addr)
break;
case 'p':
l_argc->dt_port = atoi(argv[i+1]);
break;
default:
exit(0);
}
break;
case 'v':
l_argc->ctlf = atoi(argv[i+1]);
break;
default:
exit(0);
}
} //以下的实现很龌龊,没有open何来write,实际上可以将WRITE_MYF定义为一个很大的数,然后...
write(WRITE_MYF,(char *)buf,sizeof(struct request)+sizeof(struct addr_port_g))
}
在/fs/read_write.c中:
asmlinkage ssize_t sys_write(unsigned int fd, const char __user * buf, size_t count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
if((int)fd==WRITE_MYF) //在h_input中将用户空间的新添加的数据结构加入到全局的addr_port_g链表中
return(h_input((char *)buf,(unsigned long)count));
...
}
实际上用以上直接write的方式很不正规,虽然也达到了目的,但是这里的write的语义却和真正的write大相径庭,仅仅是利用了系统调用可以将用户数据传递给内核这一特性,正确的方式应该用ioctl来完成,ioctl实际上是一个补充系统调用,可以实现几乎任何策略,只要有一个用户-内核通信的需求,又不值得去实现一个单独的系统调用,那么就可以用ioctl的方式来实现。
上面的内核程序和用户程序都分析过了,下面提出几个场景,通过这几个场景来说明这个机制的可用性以及在何种情况下可用,首先看一下在外网访问内网的情况,接下来看一下内网访问内网的情况:
eth0:192.168.0.150
eth1:61.1.1.0
在192.168.0.0/255网段:
addr_change_dst:192.168.0.159->192.168.0.150 <=>192.168.0.159->61.1.1.2
addr_change_src:61.1.1.2->192.168.0.159 <=> 192.168.0.150->192.168.0.159
以上的场景完全可行,从转换关系就可以看出,但是下面的场景就不可行了:
在61.1.1.0/255网段:由于web服务就在此网段,只有第一次addr_change_dst可以成功:
addr_change_dst:61.1.1.X->192.168.0.150 <=>61.1.1.X->61.1.1.2
addr_change_src:61.1.1.2->61.1.1.X <=> 后面没有机会再转换回去了
在数据包返回的这一步addr_change_src出了问题,问题是什么呢?源地址和目的地址都是一个网段的,所以在web服务器回复客户端时,根本就不需要通过网关,在hub或者交换机处就会被直接发往目的地,目的地的链接是到192.168.0.150的而不是到61网段的,但是tcp连接的发起确实是通过网关的,所以将会导致连接验证失败,最终导致连接无法建立,关键是无法建立一个通信回路,正如电路没有回路灯泡就不会亮一样。
以上的nat实现思想是简单的,主要就是用到了内核的一些惯用数据结构比如链表之类的,而且在5个hook的两处进行了自行到的地址/端口转换,既然能在addr_change_XXX得到ip头和tcp头,那么后面的数据也就可以得到,仅仅就是使用一个偏移指针罢了,既然得到了用户数据,那么就可以随意的进行更改了,当然ssl数据的更改还是需要下一番功夫的,这里不讨论。下面考虑一个防SQL注入系统,完全可以简单的判断web请求中有没有SQL注入串来实现,简单吧!另外考虑一个敏感词汇过滤系统也可以用类似的策略实现,当然啦,在内核中实现如此之策略是不好的,违背了内核实现机制的原则...那么SQL注入串和敏感词汇存放在哪里呢?对于sql注入串,它们肯定有一定的特征,该注入串的特征可以用正则表达式来表示,置于敏感词汇这里就不讨论了。
本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273342