頻道欄目
首頁 > 資訊 > 其他 > 正文

如何在VMWare的NAT模式下使用traceroute(解析vmnat的行為)

16-08-18        來源:[db:作者]  
收藏   我要投稿
前面寫過一篇《為什么在VMWare的NAT模式下無法使用traceroute》,本文來破除這個結論,展示一種讓你在VMWare的NAT模式下可以使用traceroute的方法。

可能很多人覺得我無聊,使用Bridge模式不就好了嗎?...其實,我之所以這么做,是認為作為一個這方面技術的愛好者,一定要有一種死磕和較真的精神,你說不行,我偏偏找出一種可行的辦法!促使我寫下本文的動力來自于一個疑問:如果說vmnat作為七層的NAT,而且我們也確認了對于TCP而言,它確實是在應用層接管了整個連接,那么對于UDP和ICMP是不是也是這樣子呢?如果答案為“是”或者“不是”,這又是為什么呢??

......

現在進入正文。
首先,我列出一個關于VMWare和traceroute的帖子,來自VMWare的論壇,請點擊這里,注意里面有一個附件,展示了在NAT模式下使用traceroute的問題,請自行打開閱讀。在前面的文章中,我曾經提到VMWare的NAT是一個七層NAT,通過其打開的socket對數據流進行重新封裝達到NAT的目的,但是如果你抓包的話,發現也不盡然。vmnat進程對待TCP和對待UDP/ICMP是不一致的。
vmnat如何對待TCP?
很簡單,劫持這個Guest OS中發出的連接,并且代表它建立一條到達目標的新連接。
vmnat如何對待UDP/ICMP?
這個就有點復雜了,起碼要比TCP復雜。vmnat并不會劫持一個UDP或者ICMP數據包,對于從Guest OS中發出去的數據包,vmnat只是簡單的修改了源IP地址,并不會觸動IP頭的其它字段,比如TTL(這是可以簡單完成NAT對traceroute支持的關鍵!),但是對于遠端回來的數據包,由于當時的源IP地址已經修改成了Host宿主機的IP地址,因此很顯然地,回來的包自然而然的被路由到本地進程,即vmnat進程,數據到達該進程的時候,經過層層解封裝,已經沒有了IP頭甚至UDP/ICMP之類的信息,如果想完成數據包到Guest OS的轉發,必須重新封裝并發送,因此它會使用本地的即Host OS的協議棧參數對這個轉發數據包進行封裝。所以說,你會發現下面的場景:
在Guest OS內ping www.baidu.com

Guest OS內抓包:

 

 

Host OS內抓包:

 

 

這說明,Guest發往baidu的數據包,vmnat好像只是做了單純的NAT,而baidu發回Host的數據包中,vmnat將整個IP頭都換了。之所以對正向包和反向包采用不同的NAT策略,是有理由的:

 

 

因此,vmnat省去了一半的力氣,它只需抓取VMWare虛擬網卡發出的RAW數據包或者直接PACKET抓取,然后簡單修改IP地址后再RAW/PACKET發出,就不用管了,直到回來的數據包被協議棧路由到vmnat,只剩下裸數據,被剝離了IP頭信息,那么vmnat只需要構造一個IP頭在裸數據上,其中的目標IP改成GuestOS的網卡IP,然后注入到VMNet虛擬網卡中即可。
現在,我們可以解釋本文開頭的問題了:如果說vmnat作為七層的NAT,而且我們也確認了對于TCP而言,它確實是在應用層接管了整個連接,那么對于UDP和ICMP是不是也是這樣子呢?
如果答案為“是”或者“不是”,這又是為什么呢??
答案可能是這樣的:
1.vmnat企圖通過一種不對稱的方式處理NAT,即對于從Guest OS發出的數據包,通過直接抓取VMNet8的裸IP報文,修改源IP地址后發出,對于進入Guest OS的數據包,由于目標地址是本機,理所當然進入vmnat進程,被創建的socket接收,然后通過socket的方式發入VMNet8這個虛擬網卡。
2.但是對于TCP而言,上述的方式不適用!因為在反向方向,vmnat會打斷序列號(數據進入應用層后會剝離IP頭和TCP頭,丟失一切協議元信息),這就會打斷Guest OS到遠端的TCP連接。另外,作為TCP的客戶端,不發起connect是無法單獨創建socket的,因此,簡單的辦法就是直接接管整個TCP。
一個更加詳細的關于vmnat的圖解如下:

 

 

我又一次見證了tun虛擬網卡的思想是多么的偉大!


好了,接著解決NAT模式下的traceroute問題。理解了以上的結構圖,你基本也就知道該怎么做到對traceroute的支持了,vmnat之所以不支持traceroute,只是它沒有處理這類數據包而已,相信以后會處理的,再說了處理一下也不難,當前的辦法就是替它處理掉這類數據包就OK了。辦法非常簡單,就是“把ICMP Time-to-live exceeded消息修改相關的IP地址信息后注入到VMNet8這塊虛擬網卡中”。
由于我們可能無力去修改vmnat的行為,因此嘗試在應用層做這個Hack往往是無助的。但是我們有一個神器,這就是pcap!下面是一個示意圖:

 

 

數據包都拿到手了,還有什么不能做的嗎?話說哪怕數據包不在手上,不也是可以自己構造一個嗎?
如果你已經知道了該怎么辦,請作為練習自行完成后面的編碼調試工作,如果你還不知道或者沒有興趣自己寫,那么請繼續看,以下的篇幅只寫一件事,那就是如何完成代碼。起初,我覺得使用scapy這件事是一件超級簡單且分分鐘搞定的事,但是在Windows上安裝scapy卻是一件作死的事!為了安裝Python以及其pypcap庫,就要各種編譯,你就必須整一個完整的編譯環境,軟粉們都推薦VS,老一代的經理們有的還在使用VC6.0,然而我看到這些就要吐血!終于,我想起了我曾經用過的Dev-C++!
...
既然有了Dev-C++,那么我也就不需要Python了,直接用winpcap會更簡單。需要安裝winpcap,一般而言只要你裝了Wireshark,這個庫就自行安裝好了,接下來你需要安裝的是winpcap的開發包,即WpdPack,我裝的是WpdPack_4_1_2版本,隨便解壓到一個目錄即可。下面是一個Step by Step:
1.用Dev-C++創建一個Windows控制臺工程;
2.點擊“項目”-“項目屬性”-“文件目錄”將WpdPack解壓目錄下的include目錄以及lib目錄添加進去;
3.接著上述步驟2,將WpdPack的lib下的wpcap.lib添加進鏈接庫中;
4.編寫源代碼main.c,僅此一個文件足矣!

前面都是引子,最重要的代碼如下:

 

#include 
#include 
#include 
#include 

#include "pcap.h"

#define PROT_ICMP	1 
#define PROT_UDP	17

#define TYPE_TTLEXCEED	11
#define LEN_ETH			14
#define	LEN_IP			20
#define LEN_MACADDR		6
#define LEN_MAXIP 		16

typedef struct ip_header {
	// from Linux kernel
	u_char	type_and_ver; 
	u_char	tos;
	u_short	tot_len;
	u_short	id;
	u_short	frag_off;
	u_char	ttl;
	u_char	protocol;
	u_short	check;
	u_int32_t	saddr;
	u_int32_t	daddr;
	/*The options start here. */
}__attribute__((packed)) ipheader;

typedef struct icmphdr {
	// from Linux kernel
  u_char		type;
  u_char		code;
  u_short	checksum;
  union {
	struct {
		u_short	id;
		u_short	sequence;
	} echo;
	u_int32_t	gateway;
	struct {
		u_short	__unused;
		u_short	mtu;
	} frag;
  } un;
}__attribute__((packed)) icmpheader;;


pcap_t *hdto = NULL;
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data);

char source[LEN_MAXIP];
char destination[LEN_MAXIP];
char mac[LEN_MACADDR];

// 我使用arp -a|find ...來獲取mac地址,而不是使用Win API,因為我恨它們! 
void get_mac(char *src, char *mac)   
{   
	char result[256] = {0};   
	char cmd[32] = {0};
	FILE *fp;  
	int idx, mac_idx;
	
	sprintf(cmd, "arp -a|find \"%s\"", src);
	if((fp = popen(cmd, "r")) == NULL) {   
		return;
	}
	if (fgets(result, 256, fp) == NULL) {
		return;
	} 
	pclose(fp);     
	for (idx = 15, mac_idx = 0; idx < strlen(result); idx ++) {
		if (result[idx] == '-'){
			char *str;
			char base[4];
			sprintf(base, "0x%c%c", result[idx-2], result[idx-1]);
			mac[mac_idx] = strtol(base, &str, 16);
			mac_idx++;
			if (mac_idx == 5) {
				sprintf(base, "0x%c%c", result[idx+1], result[idx+2]);	
				mac[mac_idx] = strtol(base, NULL, 16);
			}
		} 
	}
}  

static int cksum(u_short *addr, int len)
{
	int nleft = len;
	u_short *w = addr;
	int sum = 0;
    u_short ret = 0;

    while (nleft > 1)  {
        sum += *w++;
        nleft -= 2;
    }

    if (nleft == 1) {
        *(u_char *)(&ret) = *(u_char *)w ;
        sum += ret;
    }

    sum = (sum >> 16) + (sum & 0xffff);  
    sum += (sum >> 16);         
    ret = ~sum;              
    return ret;
}

int main(int argc, char **argv)
{
	pcap_if_t *alldevs, *phy_if, *virt_if;
	pcap_if_t *dev_if,*to;
	char *phyaddr, *virtaddr;
	pcap_t *hdfrom;
	char errbuf[PCAP_ERRBUF_SIZE];
	struct bpf_program fcode;
	int opt = 0;
	static const char *optString = "p:v:s:d:";
	
	opt = getopt(argc, argv, optString);
    while( opt != -1 ) {
        switch( opt ) {                
            case 'p':
                phyaddr = optarg;
                break; 
            case 'v':
                virtaddr = optarg;
                break;
            case 's':
                strcpy(source, optarg);
                get_mac(source, mac);
                break; 
            case 'd':
                strcpy(destination, optarg);
                break;    
            default:
                printf("XXX -p $物理網卡地址 -v $VMNet8的地址 -s $虛擬機的IP $目標IP\n");
                break;
        }
        opt = getopt( argc, argv, optString );
    }
	
	if(pcap_findalldevs(&alldevs, errbuf) == -1) {
		return -1;
	}
	// 這個在Windows上是一件令人悲傷的事情,在Linux上一個“eth0”就能搞定! 
	for(dev_if = alldevs; dev_if != NULL; dev_if = dev_if->next) {
		struct pcap_addr *addr;
		addr = dev_if->addresses;
		while (addr) {
			if(addr->addr->sa_family == AF_INET) {
				char *straddr = inet_ntoa(((struct sockaddr_in*)addr->addr)->sin_addr);
				if (!strcmp(phyaddr, straddr)) {
					phy_if = dev_if;
				} else if (!strcmp(virtaddr, straddr)) {
					virt_if = dev_if;
				}
			} 
			addr = addr->next;
		} 
	}
	pcap_freealldevs(alldevs);
	if (phy_if == NULL || virt_if == NULL) {
		return -1;
	}
	// 打開Host OS的出口物理網卡設備 
	if ((hdfrom = pcap_open_live(phy_if->name, 65536, 1, 1000, errbuf)) == NULL) {
		pcap_freealldevs(alldevs);
		return -1;
	}
	
	// 打開Host OS在NAT模式下連接Guest OS的VMNet設備,本例為VMNet8  
	if ((hdto = pcap_open_live(virt_if->name, 65536, 1, 1000, errbuf)) == NULL) {
		pcap_close(hdfrom);
		return -1;
	} 
	
	// 設置過濾器,只處理TTL過期的ICMP數據包 
	if (pcap_compile(hdfrom, &fcode, "icmp and icmp[icmptype] == icmp-timxceed", 1, 0xffffffff) < 0){
		pcap_close(hdfrom);
		pcap_close(hdto);
		return -1;		
	} 
	
	if (pcap_setfilter(hdfrom, &fcode) < 0) {
		pcap_close(hdfrom);
		pcap_close(hdto);
		return -1;			
	}
	
	pcap_loop(hdfrom, 0, packet_handler, NULL);
	pcap_close(hdfrom);
	pcap_close(hdto);
	return 0;
}

void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)
{
	u_char *buff = NULL;
	ipheader *iph = (ipheader*)(pkt_data + LEN_ETH);
	if (iph->protocol == PROT_ICMP) {
		icmpheader *icmp = (icmpheader*)(pkt_data + LEN_ETH + LEN_IP);
		if (icmp->type == TYPE_TTLEXCEED) {
			ipheader *iph_1, *iph_2 ;		
			buff = malloc(header->caplen);
			if (!buff) {
				goto out;
			}
			memcpy(buff, pkt_data, header->caplen);
			memcpy(buff, mac, LEN_MACADDR);
			// 獲取IP頭,進行地址轉換。對于TTL exceeded消息而言,地址轉換要轉兩層,一層是外部的,另外引發這條TTL exceeded消息的內部源報文的地址也要轉換。 
			iph_1 = (ipheader*)(buff + LEN_ETH);
			// 實際的地址轉換,將目標地址轉換成Guest OS的IP地址并重算校驗和。 
			iph_1->daddr = inet_addr(source);
			iph_1->check = 0;
			iph_1->check = cksum((unsigned short*)iph_1, LEN_IP);
			
			// 獲取TTL過期消息中封裝的“引發該消息的”原始IP數據報的協議頭,越過TTL exceeded報文的前8字節,直達原始報文。 
			iph_2 = (ipheader*)(buff + LEN_ETH + LEN_IP + 8);
			// 轉換內部原始IP報文的目源地址為Guest OS的IP地址并重算校驗和。 
			iph_2->saddr = inet_addr(source);
			iph_2->check = 0;
			iph_2->check = cksum((unsigned short*)iph_2, LEN_IP);
			
			// traceroute有兩種方式,Linux平臺默認使用UDP,因此里面封裝的是一個UDP報文。 
			if (iph_2->protocol == PROT_UDP){
				//TODO
				// 重新計算校驗和,注意偽頭部 
			} else if (iph_2->protocol == PROT_ICMP){
				// 否則,如果使用了-I選項,則使用ICMP Echo reuqest進行trace。 
				// 
			} else {
				goto out;
			}
			// 將TTL exceeded消息經過NAT后發送到VMNet8這個NAT設備,隨后它將會把數據包轉給同網段的Guest OS網卡 
			if (pcap_sendpacket(hdto, buff,	header->caplen) != 0) {
				goto out;
			} 
		}
	}
out:
	if (buff) {
		free(buff);
	}
}

注意這個代碼,其實它在Linux下稍微改下頭文件也可以輕松編譯成功并運行,在寫這個代碼的時候,最困難的是選擇pcap設備的環節,Linux中可以直接使用網卡的name字段,比如“eth0”輕松打開一個pcap句柄,但是在Windows平臺,一個網卡的name并不是一個方便可讀的字符串,因此就不得不先調用pcap_findalldevs枚舉出所有的設備,然后去比對IP地址來獲取設備,這樣反而要比直接使用網卡的名字來打開句柄方便很多。
個人認為,Windows平臺使用網卡(另外還有磁盤)的名字之所以不方便是因為Windows程序中很少使用命令行操作,大多數都是GUI,而GUI幾乎就是讓你點擊各種空間去選擇網卡的,因此一個pcap_if_t對象的name和description字段可能更好的去展示完整的信息,雖然它不像eth0那么簡短和直接!

用法很簡單。我以我的環境為例來說明。我的機器配置如下:
Host OS配置:
物理網卡地址-192.168.199.195
VMNet8虛擬網卡地址-192.168.44.1
Guest OS配置:
Eth3物理網卡地址-192.168.44.100
traceroute目標-14.215.177.37
運行我的程序:
D:\dev\natex -p 192.168.199.195 -v 192.168.44.1 -s 192.168.44.100 -d 14.215.177.37
此時,Guest Linux上,運行traceroute,結果如下:

 

這個小程序是非常好用的,可以在虛擬機中使用traceroute了!我之所以寫這么一個小工具,是因為它對于我而言是有用的,因為我的筆記本電腦在公司是DHCP獲取的地址,地址配置在無線網卡上,我無法使用橋接,因為我的虛擬機將無法通過認證(公司網絡隔離的太狠!),所以,我必須使用NAT,而且我必須使用traceroute,所以就有了上面的代碼。
相關TAG標簽
上一篇:臺積電:絕大多數7nm客戶都會轉向6nm_IT新聞_博客園
下一篇:最后一頁
相關文章
圖文推薦

關于我們 | 聯系我們 | 廣告服務 | 投資合作 | 版權申明 | 在線幫助 | 網站地圖 | 作品發布 | Vip技術培訓 | 舉報中心

版權所有: 紅黑聯盟--致力于做實用的IT技術學習網站

美女MM131爽爽爽毛片