1.概要
相信在IPv6的時代到來之前,NAT仍然是解決大多數人上網的主要途徑,而且它在企業內網Intranet中也扮演著十分重要的角色.
NAT的全稱是Network Address
Translator(網絡地址轉換),其主要作用是把內網IP地址轉換成為全球唯一的可定位的外部IP地址,從而使得局域網內的所有用戶可以通過一個或
者少數幾個IP地址與全球的Internet通信,不僅節約了IP地址,而且在一定程度上保護了內部網絡.
由于工作需要,筆者希望編寫一個具有NAT功能的軟件,將同一個網段內把本機設為網關的擁有私有IP的主機發來的數據包轉發到外部網絡,并把響應信息返回給對應的主機.這個問題在不同的層次上做就有不同的解決方案,由于筆者也是網絡新手,走了不少彎路:
首先,企圖在用戶層利用原始套接字(Raw
Socket)來實現,但是系統擁有對未開放端口的自動復位功能,每當我們轉發一個數據包時,需要占用系統的一個端口,但是這點系統并不知道,它接收到對
于這個端口的回應信息時,會認為本端口不存在,并發送一個帶有復位標志的數據包請求對方斷開連接.這就阻攔了所有非本機請求的連接,所以這個方案首先被否
定了.
隨后,不得不往系統下面走,準備在核心態實現.當然越簡單越好,于是筆者選擇了Filter-Hook驅動.Filter-Hook
Driver, 事實上不是一種新的網絡驅動,它只是擴展了IP過濾驅動(IP Filter
Driver)的功能,是一種內核模式驅動(Kernel Mode Driver). 在Filter-Hook
Driver中我們提供回調函數(callback),然后使用IP Filter Driver注冊回調函數。這樣當數據包發送和接收時,IP
Filter
Driver會調用回調函數。可惜夢想再一次破滅,這個回調函數的返回值只有PF_FORWARD,PF_DROP,PF_PASS三種,并不能把修改后
的數據包主動發送出去.
只有在向底層走了,NDIS應該是必經之路.而且經過兩次失敗,發現閉門造車是不可行的,偶然在網上搜索到了幾篇文章,聽說在NDIS的中間層驅動中可以實現NAT,新的探索之路就這樣開始了......
2.NAT簡介
NAT(Network Address
Translator)的出現并不是偶然的,一方面是由于IPv4的創造者們沒有想到,Internet以及TCP/IP發展如此迅速,在他們還們完全享
受TCP/IP的成功帶來的快感之前,32位的IP地址竟然不夠用了;另一方面我們必須保證某些特殊的主機在于局域網絡連接的同時,保持對外界直接曝光,
但是由需要與外界在受控的情況下通訊.下圖是一個典型的NAT示意.
\ | / . /
+---------------+ WAN . +-----------------+/
|Regional Router|----------------------|Stub Router w/NAT|---
+---------------+ . +-----------------+\
. | \
. | LAN
. ---------------
Stub border
下面舉一個具體的例子說明兩個處于內網的主機是如何通過NAT通信的
\ | /
+---------------+
|Regional Router|
+---------------+
WAN | | WAN
| |
Stub A .............|.... ....|............ Stub B
| |
{s=198.76.29.7,^ | | v{s=198.76.29.7,
d=198.76.28.4}^ | | v d=198.76.28.4}
+-----------------+ +-----------------+
|Stub Router w/NAT| |Stub Router w/NAT|
+-----------------+ +-----------------+
| |
| LAN LAN |
------------- -------------
| |
{s=10.33.96.5, ^ | | v{s=198.76.29.7,
d=198.76.28.4}^ +--+ +--+ v d=10.81.13.22}
|--| |--|
/____\ /____\
10.33.96.5 10.81.13.22
圖
中有兩個殘樁網絡A和B,現在假設A中的一臺主機10.33.96.5需要同B中的10.81.13.22通信,它必須把自己發送的數據報的目的地址設置
為B的一個外網地址198.76.28.4,并在NAT中把源地址轉換成A的外部地址198.76.29.7,才能使數據包順利抵達廣域網中的路由器,并
轉到B,在由B網的NAT把數據包發送給10.81.13.22.
隨著NAT多年的發展,出現了很多不同風格,應用于不同場合的NAT.筆者實現的是傳統NAT中的一種特殊情況NAPT(Network
Address Port Translation),它把所有內網的IP地址都轉換成同一個外部IP地址,并通過不同的端口來區分各個不同的內部主機.
3.中間層驅動NDIS Intermediate Drivers
所謂中間層驅動是指位于微端口和協議之間的驅動,實際上它是微軟在網絡驅動中留出來的接口,便于用戶實現自己對數據包的處理.在協議驅動層看來,它就是微
端口;在微端口看來,它又是協議層驅動.因此,如果需要在此層實現自己對數據包的處理函數,不僅要在上邊緣注冊MiniportXxx
Function,還要在下邊緣注冊ProtocolXxx
Function.一般在這個層次做工作的同志都會學習并了解DDK的一個經典Sample:Passthru.如果對它不了解,可以去看看
Addylee前幾天的文章"基于PassThru的NDIS中間層驅動程序擴展",講得很清晰.
4.NAPT的具體實現
程序整體框架依然是基于PaaThru的,具體要注意的問題有以下幾點:
4.1 轉發表的維護
typedef struct _PortNode
{
USHORT inport; //內網端口
USHORT export; //轉發端口
USHORT report; //遠程端口
ULONG inip; //內網IP
ULONG reip; //遠程IP
struct _PortNode * next; //鏈表指針
}PortNode;
PortNode * first = NULL; //全局變量,轉發表的頭結點
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
......
Status = NdisAllocateMemory(&first,sizeof(PortNode), 0,HighestAcceptableMax);
if(Status == NDIS_STATUS_SUCCESS)
{
NdisZeroMemory(first,sizeof(PortNode));
//首結點的inip表示本主機地址
first->inip = 本主機IP
//首結點的reip表示本主機所在的網絡地址
first->reip = first->inip & 0x00ffffff;
}
......
}
4.2 校驗和的計算
USHORT CheckSum(USHORT *buffer, int size)
{
unsigned long cksum=0;
while(size >1)
{
cksum += * buffer++;
size -=sizeof(USHORT);
}
if(size)
{
cksum += *(UCHAR*)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >>16);
return (USHORT)(~cksum);
}
IP TCP UDP三種包校驗和的計算方法是一致的,本文采用的方法是簡單地重新計算整個包的校驗和,在RFC1631中,作者提出了一種差量計算法以提高計算速度,并且給出了C語言版的源代碼.
4.3 對收到的數據包的過濾和轉發
INT
PtReceivePacket(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_PACKET Packet
)
{
......
PUCHAR pPacketContent;
PUCHAR pBuf;
UINT BufLength;
MDL * pNext;
UINT i,j;
BOOLEAN transflag = FALSE;
PNDIS_BUFFER MyBuffer;
PIP_Header pIPHeader;
......
NdisDprAllocatePacket(&Status,
&MyPacket,
pAdapt->RecvPacketPoolHandle);
if(Status == NDIS_STATUS_SUCCESS)
{
//add by thinking 06.6.3
//把數據包內容從Packet拷貝到pPacketContent
Status= NdisAllocateMemory( &pPacketContent, 2000, 0,HighestAcceptableMax);
if (Status!=NDIS_STATUS_SUCCESS ) return Status;
NdisZeroMemory (pPacketContent, 2000);
NdisQueryBufferSafe(Packet->Private.Head, &pBuf, &BufLength, 32 );
NdisMoveMemory(pPacketContent, pBuf, BufLength);
i = BufLength;
pNext = Packet->Private.Head;
for(;;)
{
if(pNext == Packet->Private.Tail)
break;
pNext = pNext->Next;
if(pNext == NULL)
break;
NdisQueryBufferSafe(pNext,&pBuf,&BufLength,32);
NdisMoveMemory(pPacketContent+i,pBuf,BufLength);
i+=BufLength;
}
if(pPacketContent[12] == 8 && pPacketContent[13] == 0 ) //is ip packet
{
ULONG netip;
pIPHeader = (PIP_Header)(pPacketContent+14);
netip = pIPHeader->ipSource & 0x00ffffff;
//對收到的數據包進行過濾,只轉發需要轉發的包
if(pIPHeader->ipDestination == first->inip && netip != first->reip)
//如果目的地址是本主機,并且源IP不是本網段地址,則轉發給內網主機
{
DbgPrint("\nTransInPacket...\n");
for(j=0;j<=i;j++)
DbgPrint("%x ",pPacketContent[j]);
//修改發給內網的數據包頭
transflag = TransIn(pPacketContent);
}
else if(pIPHeader->ipDestination != 0xffffffff &&
(pIPHeader->ipDestination & 0x00ffffff) != first->reip &&
netip == first->reip)
//如果目的地址不是廣播地址,而且是外網地址,源地址是內網IP,則轉發給外網
{
DbgPrint("\nTransOutPacket...\n");
for(j=0;j<=i;j++)
DbgPrint("%x ",pPacketContent[j]);
//修改發給外網的數據包頭
transflag = TransOut(pPacketContent);
}
}
if(!transflag)
{
//按照原來的方式往上提交
......
}
else
{
//轉發的一段關鍵代碼
NdisAllocateBuffer(&Status,&MyBuffer,pAdapt->SendPacketPoolHandle,pPacketContent,i);
NdisChainBufferAtFront(MyPacket, MyBuffer);
Resvd =(PRSVD)(MyPacket->ProtocolReserved);
Resvd->OriginalPkt = MyPacket;
MyPacket->Private.Head->Next = NULL;
MyPacket->Private.Tail = NULL;
NdisSetPacketFlags(MyPacket, NDIS_FLAGS_DONT_LOOPBACK);
NdisReturnPackets(&Packet, 1);
NdisSend(&Status,pAdapt->BindingHandle,MyPacket);
if(Status != NDIS_STATUS_PENDING)
{
NdisUnchainBufferAtFront(MyPacket ,&MyBuffer); //從MyPacket中解除buffer
NdisQueryBufferSafe(MyBuffer, &pPacketContent, &BufLength,32 );
if(pPacketContent != NULL)
NdisFreeMemory(pPacketContent,BufLength, 0);
NdisFreeBuffer(MyBuffer);
}
return 0;
}
......
}
4.4 數據包頭的修改
根據具體情況修改數據包的IP包頭,TCP包頭,或者UDP包頭,并且在修改的同時繼續維護轉發表,下面只給出TransIn的代碼,TransOut與其原理相同.
BOOLEAN TransIn(PUCHAR pPacketContent)
{
PortNode * inmap;
PIP_Header pIPHeader = (PIP_Header)(pPacketContent+14);
USHORT iphdrlen = (pIPHeader->iphVerLen & 0x0f) * sizeof(ULONG);
UCHAR checkbuff[2000] = {0};
if(pIPHeader->ipProtocol == 6)
{
PTCP_Header pTCPHeader;
USHORT tcphdrlen;
pTCPHeader = (PTCP_Header)(pPacketContent+14 + iphdrlen);
//tcphdrlen = ((pTCPHeader->dataoffset & 0xf0) >> 4) * sizeof(ULONG);
tcphdrlen = htons(pIPHeader->ipLength) - iphdrlen;
inmap = InMapping(pIPHeader->ipSource,pTCPHeader->sourcePort,
pTCPHeader->destinationPort);
if(inmap == NULL)
return FALSE;
//修改目的地址和目的端口,校驗和
pIPHeader->ipDestination = inmap->inip;
pTCPHeader->destinationPort = inmap->inport;
pIPHeader->ipChecksum = 0;
pTCPHeader->checksum = 0;
//填充TCP偽首部
psdhdr.saddr = pIPHeader->ipSource;
psdhdr.daddr = pIPHeader->ipDestination;
psdhdr.len = htons(tcphdrlen);
psdhdr.mbz = 0;
psdhdr.proto = 6;
//計算TCP首部校驗和
NdisMoveMemory(checkbuff,&psdhdr,sizeof(psdhdr));
NdisMoveMemory(checkbuff+sizeof(psdhdr),pTCPHeader,tcphdrlen);
pTCPHeader->checksum = CheckSum((USHORT *)checkbuff,sizeof(psdhdr)+tcphdrlen);
//計算IP首部校驗和
pIPHeader->ipChecksum = CheckSum((USHORT *)pIPHeader,iphdrlen);
return TRUE;
}
else if(pIPHeader->ipProtocol == 17)
{
PUDP_Header pUDPHeader;
USHORT udplen;
pUDPHeader = (PUDP_Header)(pPacketContent+14 + iphdrlen);
udplen = htons(pUDPHeader->len);
inmap = InMapping(pIPHeader->ipSource,pUDPHeader->sourcePort,
pUDPHeader->destinationPort);
if(inmap == NULL)
return FALSE;
//修改目的地址和目的端口,校驗和
pIPHeader->ipDestination = inmap->inip;
pUDPHeader->destinationPort = inmap->inport;
pIPHeader->ipChecksum = 0;
pUDPHeader->checksum = 0;
//填充UDP偽首部
psdhdr.saddr = pIPHeader->ipSource;
psdhdr.daddr = pIPHeader->ipDestination;
psdhdr.len = pUDPHeader->len;
psdhdr.mbz = 0;
psdhdr.proto = 17;
//計算UDP校驗和,包括所有UDP數據
NdisMoveMemory(checkbuff,&psdhdr,sizeof(psdhdr));
NdisMoveMemory(checkbuff+sizeof(psdhdr),pUDPHeader,udplen);
pUDPHeader->checksum = CheckSum((USHORT *)checkbuff,sizeof(psdhdr)+udplen);
//計算IP首部校驗和
pIPHeader->ipChecksum = CheckSum((USHORT *)pIPHeader,iphdrlen);
return TRUE;
}
else
return FALSE;
}
5.小結
本文簡單介紹了傳統NAT在中間層驅動中的實現,很多地方都可以進行改進.例如:校驗和的計算可以采用差量計算法以減少計算延遲;轉發表的維護可以采用樹
型結構(而不是本文中的鏈表)以減少轉發表的查找時間;定時對轉發表進行清理,釋放長時間不用的端口,以節約系統資源;構建ARP機制,并動態維護相關主
機的MAC地址;通過共享內存或者修改驅動對象的DispatchTable與用戶層進行通信,從而動態調整驅動功能.