可能很多人覺得我無聊,使用Bridge模式不就好了嗎?...其實,我之所以這么做,是認為作為一個這方面技術的愛好者,一定要有一種死磕和較真的精神,你說不行,我偏偏找出一種可行的辦法!促使我寫下本文的動力來自于一個疑問:如果說vmnat作為七層的NAT,而且我們也確認了對于TCP而言,它確實是在應用層接管了整個連接,那么對于UDP和ICMP是不是也是這樣子呢?如果答案為“是”或者“不是”,這又是為什么呢??
......
現在進入正文。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虛擬網卡中即可。
我又一次見證了tun虛擬網卡的思想是多么的偉大!
數據包都拿到手了,還有什么不能做的嗎?話說哪怕數據包不在手上,不也是可以自己構造一個嗎?
#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); } }
這個小程序是非常好用的,可以在虛擬機中使用traceroute了!我之所以寫這么一個小工具,是因為它對于我而言是有用的,因為我的筆記本電腦在公司是DHCP獲取的地址,地址配置在無線網卡上,我無法使用橋接,因為我的虛擬機將無法通過認證(公司網絡隔離的太狠!),所以,我必須使用NAT,而且我必須使用traceroute,所以就有了上面的代碼。