前面写过一篇《 为什么在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 <stdlib.h>
#include <winsock2.h>
#include <unistd.h>
#include <stdio.h>#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       16typedef struct ip_header {// from Linux kernelu_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 kernelu_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,所以就有了上面的代码。

如何在VMWare的NAT模式下使用traceroute(解析vmnat的行为)相关推荐

  1. vmware中NAT模式下,虚拟机与主机能ping通 为什么虚拟机不能上网

    vmware中NAT模式下,虚拟机与主机能ping通 为什么虚拟机不能上网? 方案一: 1.把虚拟机的网络连接设置为桥接或NAT都可以的 2.把虚拟机和主机设置为同一网段 主机 网络邻居属性 3.双击 ...

  2. VMware在NAT模式下配置静态IP

    1. 虚拟机网络连接方式 安装好虚拟机以后,在网络连接里面可以看到多了两块网卡: 其中VMnet1是虚拟机Host-only模式的网络接口,VMnet8是NAT模式的网络接口. 虚拟机常见有三种网络连 ...

  3. TiDB 如何在 LVS FULL NAT 模式下显示客户端真实 IP

    原文来源: https://tidb.net/blog/1d65166f [是否原创]是 [首发渠道]TiDB 社区 [正文] 作者: 靳献旗 汽车之家 DBA,TUG 2021 MVA 1.背景 公 ...

  4. 教你一招在VMware的NAT模式下,两台电脑如何互相访问对方的虚拟机

    目录 一. 准备工作 二. 开始 2.1 设置NAT模式 2.2 进行NAT模式设置 2.3 XShell访问对方虚拟机 一. 准备工作 需要两台电脑(我准备了两台win10) VMware虚拟机:[ ...

  5. VMWare虚拟机NAT模式下static IP

    为什么80%的码农都做不了架构师?>>>    采用NAT模式后,发现guest的IP经常变化,网上找到的解决办法如下:  来源:http://my.oschina.net/aigu ...

  6. VMware虚拟机NAT模式下连不上网

    NAT模式的具体配置可以参考站内的其他文章: NAT网络配置① NAT网络配置② 遇到的问题可以具体再搜索 我遇到的问题时配置好了网络,虚拟机内部显示有Internet连接,但仍然不能上网,解决方法是 ...

  7. VMware开启NAT模式/仅主机模式后主机ping不通虚拟机的问题

    VMware开启NAT模式/仅主机模式后主机ping不通虚拟机的问题 问题:VMware设置网络模式为NAT模式后,我们使用主机ping虚拟机无法联通,用虚拟机ping主机可以联通. 原因:这是由于虚 ...

  8. CentOs虚拟机NAT模式下静态IP的配置

    NAT连接方式除非手动配置,一般默认为动态ip,在一些场景下,动态IP随机指定显然是不合适的.NAT模式下:网关的配置(manual方式下)要和虚拟网络配置器保持一致. 方法 1: 首先修改:/etc ...

  9. virtualBox使用nat模式下ssh连接

    virtualBox本地虚拟机通过ssh连接一般可通过桥接模式和Nat模式 桥接模式下,共享本地主机网卡,在同一个局域网之下,直接获取Ip地址就可以进行连接了. Nat模式下,获取的Ip与本地主机不是 ...

最新文章

  1. python连接数据库,处理数据结果后生成excel文件
  2. 剑指offer:扑克牌顺子
  3. 连你的免疫系统都拒绝996:半夜吃东西更容易拉肚子 | Cell
  4. nginx学习七 高级数据结构之动态数组ngx_array_t
  5. 蓝桥杯-最短路(floyd算法)
  6. JetBrains 授权服务器(License Server URLS):
  7. 【嵌入式】C语言高级编程-地址对齐(07)
  8. .Net之配置文件自定义
  9. RxJS 系列之一 - Functional Programming 简介
  10. Linux中Docker部署Redis
  11. jeesite 去掉 /a
  12. 启动 Eclipse 弹出“Failed to load the JNI shared library jvm.dll”错误的解决方法!在eclipse.ini中为eclipse指定jdk启动...
  13. 开课吧Java课堂:StringBuffer全解,非常详细
  14. 【渝粤教育】国家开放大学2018年春季 0706-22T行政管理学导论 参考试题
  15. 服务器中C盘的虚拟大文件,查看虚拟机的大文件系统
  16. Rdlc报表出现空白页解决方法
  17. 从零开始撸一个Fresco之gif和Webp动画
  18. 泛运筹理论初探——HANP和BMLPA以及DCLP简介
  19. 23. 电容触摸按键实验
  20. 根据url批量下载图片

热门文章

  1. 去掉屏底部三个虚拟按键 ,默认手势导航,去掉手势导航的底部横线。
  2. BI工具是什么,有什么用?
  3. mobilenet系列之又一新成员---mobilenet-v3
  4. 《30天自制操作系统》U盘启动,真机运行(16天)
  5. v-model与:model
  6. C# PostgreSQL 教程
  7. 请解释一下电子ED病因及治疗方法
  8. 红警快修三星270E4V笔记本故障维修过程
  9. VRAR中的Affordance人机交互设计
  10. Activiti工作流-进阶