可能很多人觉得我无聊,使用Bridge模式不就好了吗?...其实,我之所以这么做,是认为作为一个这方面技术的爱好者,一定要有一种死磕和较真的精神,你说不行,我偏偏找出一种可行的办法!促使我写下本文的动力来自于一个疑问:如果说vmnat作为七层的NAT,而且我们也确认了对于TCP而言,它确实是在应用层接管了整个连接,那么对于UDP和ICMP是不是也是这样子呢?如果答案为“是”或者“不是”,这又是为什么呢??
......
Guest OS内抓包:
Host OS内抓包:
#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);
}
}