實現一個簡單的反向NAT並引申一個web請求內容過濾

來源:互聯網
上載者:User

如果不懂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)

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)

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 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

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注入串,它們肯定有一定的特徵,該注入串的特徵可以用Regex來表示,置于敏感詞彙這裡就不討論了。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.