在大学就一直想写一个局域网IP查看工具,用来查询一个局域网的以使用的IP地址。毕业一年多了,这个网络小工具也没能有时间写。当时由于水平有限未能完成,8月底正好有这个时间,于是花了一个星期的时间来做这件事,最后终于完成了。其间的一些体会写出来和大家分享。此文适合于对TCP/IP协议有所了解和具有一定编程基础的朋友。
非技术篇(关心技术的朋友直接跳过)
一、为什么要写这个东西?
大三下学期时宿舍楼通校园网了,大家是无比的兴奋,但是很快问题就来了。我所在的宿舍楼里,有6层,计算机多达400多台(可能还要多一些),而只分配了一个C类IP段,254个IP地址,网关占一个,所以IP争夺是无法避免了。大家的苦恼都来源于此,而故事就此开始了。
笔者看过一些网络方面的书,所以知道一些做法。第一个办法是很早起来占住一个IP(晚上熄灯)。那么之后的人要面对的问题是:你可以改动IP设置,但是你并不知道那些IP没有被其他机器使用。Window2000可以动态该IP地址,无需重启,WinXP更强,修复一下可以恢复过来。当然要手工操作,有时候你突然死机在重启,IP就不再属于你了!哈哈,你就晕吧!
当然可以通过别的方式来查看这个情况Ping命令是一个,还有一些类似的工具。Ping命令不太可靠,一个一个的Ping,很浪费时间,没有时效性。而且后来天网防火墙等软件可以设置不响应。所以这个办法行不通了,看着别人上网,自己设置一个IP冲突一个,真是郁闷。要是有个东西可以准确的查看局域网的IP使用情况该多好啊!当时在网上搜索了很久,发现了一个基于UDP协议的查看IP的程序,还有源代码。效果改观了很多,它使用一个独立的线程发送,利用socket接受,缺憾是天网可以屏蔽它。当时开始思考这个问题,每台机器的唯一不变的是网卡的物理地址即Mac地址,局域网到底是用什么机制来实现IP地址到Mac的映射了,这可是通信的基本的问题!因为对网络协议感兴趣的原因,后来查阅很多种资料,包括RFC的英文文档,用代理在国外的找的,该文档介绍各种于网络相关的标准,有各种协议的规范。我知道了一些ARP(Address Resolution Protocol,地址解析协议)的知识,后面介绍这个协议。
不多久,真的出现了这么一个小软件。据说是这栋楼的一个VC高手写的,看到这个东西的只需要输入一个查询范围如202.4.151.1-202.4.151.254它就会快速的查询IP地址的使用情况。当时看不到源代码,佩服了半天。有一点可以肯定,它使用的是ARP协议,用到了多线程。当时仅仅是知道一些网络协议,没有编过Winsock方面的程序,多线程也很弱。所以写不出来,但当时就想,以后有机会要写一个类似的小工具。
二、作手准备
时光飞逝,之后考程序员,做毕业设计,工作,再换工作…从机械设计回到软件开发。加班,学习,学习,加班…日子过得真快。后来终于又休息了。《深入浅出MFC》《Windows核心编程》《Windows网络编程技术》看了很长时间了,渐渐的领悟了一些东西,多线程也熟悉了不少。特别是有个项目涉及到了UDP多点传输,当时把Socket深入的看了一些,调试了几个这个示例程序,对这个Socket模型熟悉起来。感觉有足够的知识来写这个东西了。
但我还没有想到具体的思路,于是到网上搜索(在前人的基础上有所超越,而不是作重复的事情),找到了几篇关于ARP的文章,下面的引用到的两篇是其中的。同时也开始接触row socket(原始套接字),研究sniffer(嗅探器)。当时考虑到,Ping的时候,如果不知道相应的IP地址的MAC地址该如何处理,只能回到ARP协议,对了上层协议必须建立在下层协议的基础上工作! Row socket虽然原始,却只能到网络层,而ARP协议在链路层工作,因为在网线或设备识别的只能是MAC地址,同时它不是物理层。此时正好在看Winpcap包帮助文档,它里面已经封装了对设备的操作,它可以直接发送链路层的包。用它可以发送所有的上层的包,而网络协议本身最后传送的包,最后都会包装层底层包。说白了,只要你了解了各种包格式,你就可以实现ISO/OSI的各个协议层的功能!OK,这个东西能够满足要求,Winpcap也了解不少了,可以动手了。
三、编码
Winpcap的使用请参考帮助文档,Web格式的,没有索引(MSDN有)。内容不多,知识却不少。笔者也没有彻底研究,涉及到的代码,有较为详细的注释。首先参考了《手把手教你玩转ARP包》,作者提供了详细的代码,讲的也很详细,于是没有花多少时间就看明白了。但是作者的工程是用VS2003写的,但没有说在哪个Window版本上通过,估计上Win2000,转换到VC6的工程后,在WinXP上却没有出现提到的arp缓冲条目增加的情况。这个问题困扰了两天,最后发现之前下载的文章《详谈调用winpcap驱动写arp多功能工具》有收发arp包的功能。离我的需要很近了。该代码是console工程,必须改造成MFC App。这倒不是很难。简单的设计了界面,具体实现的时候有些问题费了些时间。前后用了一个星期,没有自己的电脑进度慢了很多。
以上算是心得,具体程序的实现在技术篇细细道来。好吧,技术篇!!
言归正传,书回正题!
技术篇
一 ARP协议介绍
原理方面的内容整理出自《手把手教你玩转ARP包》
工作原理
以太网设备比如网卡都有自己全球唯一的MAC地址,它们是以MAC地址来传输以太网数据包的,它们不可能识别IP包中的IP地址,实际上网卡把受到的包提交到协议栈的缓冲,成为链路层包,再分离出IP层的包,再分离出TCP或UDP…。就像剥开竹笋,发送包则是相反的过程。
我们在以太网中进行IP通信的时候需要一个协议来建立IP地址与MAC地址的对应关系,以使IP数据包能发到一个确定的地方去。这就是ARP(Address Resolution Protocol,地址解析协议)。
讲到此处,我们可以在命令行窗口中,输入
arp –a
来看一下效果,类似于这样的条目
210.118.45.100 00-0b -5f -e6-c5-d7 dynamic
就是我们电脑里存储的关于IP地址与MAC地址的对应关系,dynamic表示是临时存储在ARP缓存中的条目,过一段时间就会超时被删除(具体时间不同操作系统不同)。
这样一来,比如我们的电脑要和一台机器比如210.118.45.1通信的时候,它会首先去检查arp缓存,查找是否有对应的arp条目,如果没有,它就会给这个以太网络发ARP请求包广播询问210.118.45.1的对应MAC地址,当然,网络中每台电脑都会收到这个请求包,但是它们发现210.118.45.1并非自己,就不会做出相应,而210.118.45.1就会给我们的电脑回复一个ARP应答包,告诉我们它的MAC地址是xx-xx-xx-xx-xx-xx,于是我们电脑的ARP缓存就会相应刷新,多了这么一条:
210.118.45.1 xx-xx-xx-xx-xx-xx dynamic
为什么要有这么一个ARP缓存呢,试想一下如果没有缓存,我们每发一个IP包都要发个广播查询地址,岂不是又浪费带宽又浪费资源?
我们的网络设备无法识别ARP包的真伪,而且也不能识别,基本的约定都是假的,会带来灾难。如果我们按照ARP的格式来发送数据包,只要信息有效计算机就会根据包中的内容做相应的反应。
这也是以太网可靠通信的基础,但是如果我能操纵网卡设备发送数据和掌握包的格式,我可以做一些欺骗和捣乱。当然,笔者只是理论分析,并不提倡读者这么做。
ARP包的格式
一个ARP包是分为两个部分的,前面一个是物理帧头或EtherHeader,后面一个才是ARP帧或Arp-Frame。
首先,物理帧头,它将存在于任何一个协议数据包的前面,我们称之为DLC Header,因为这个帧头是在数据链路层构造的,并且其主要内容为收发双方的物理地址,以便硬件设备识别。
DLC Header |
|||
字段 |
长度(Byte) |
默认值 |
备注 |
接收方MAC |
6 |
|
广播时,为 ff-ff-ff-ff-ff-ff |
发送方MAC |
6 |
|
|
Ethertype |
2 |
0x0806 |
0x0806是ARP帧的类型值 |
图1 物理帧头格式
图1是需要我们填充的物理帧头的格式,我们可以看到需要我们填充的仅仅是发送端和接收端的物理地址罢了,是不是很简单呢?
接下来我们看一下ARP帧的格式.
ARP Frame |
|||
字段 |
长度(Byte) |
默认值 |
备注 |
硬件类型 |
2 |
0x1 |
以太网类型值 |
上层协议类型 |
2 |
0x0800 |
上层协议为IP协议 |
MAC地址长度 |
1 |
0x6 |
以太网MAC地址长度为6 |
IP地址长度 |
1 |
0x4 |
IP地址长度为4 |
操作码 |
2 |
|
0x1表示ARP请求包,0x2表示应答包 |
发送方MAC |
6 |
|
|
发送方IP |
4 |
|
|
接受方MAC |
6 |
|
|
接受方IP |
4 |
|
|
填充数据 |
18 |
|
因为物理帧最小长度为64字节,前面的42字节再加上4个CRC校验字节,还差18个字节 |
我们可以看到需要我们填充的同样也只是MAC, IP, 再加上一个1或2的操作码而已。
ARP包的填充
1) 请求包的填充:
比如我们的电脑MAC地址为 aa-aa-aa-aa-aa-aa,IP为 192.168.0.1
我们想要查询 192.168.0.99的MAC地址,应该怎么来做呢?
首先填充DLC Header,通过前面的学习我们知道,想要知道某个计算机对应的MAC地址是要给全网发送广播的,所以接收方MAC肯定是 ffffffffffff,发送方MAC当然是自己啦,于是我们的DLC Header就填充完成了,如图,加粗的是我们要手动输入的值(当然我编的程序比较智能,会根据你选择的ARP包类型帮你自动填入一些字段,你一用便知^_^)。
DLC Header |
||
字段 |
长度(Byte) |
填充值 |
接收方MAC |
6 |
Ffffffffffff |
发送方MAC |
6 |
Aaaaaaaaaaaa |
Ethertype |
2 |
0x0806 |
图3 ARP请求包中 DLC Header内容
接下来是ARP帧,请求包的操作码当然是 1,发送方的MAC以及IP当然填入我们自己的。然后要注意一下,这里的接收方IP填入我们要查询的那个IP地址,就是192.168.0.99了,而收方MAC填入任意值就行,不起作用,于是,如图,
ARP Frame |
||
字段 |
长度(Byte) |
填充值 |
硬件类型 |
2 |
1 |
上层协议类型 |
2 |
0x0800 |
MAC地址长度 |
1 |
6 |
IP地址长度 |
1 |
4 |
操作码 |
2 |
1 |
发送方MAC |
6 |
Aaaaaaaaaaaa |
发送方IP |
4 |
192.168.0.1 |
接收方MAC |
6 |
任意值 xxxxxxxxxxxx |
接收方IP |
4 |
192.168.0.99 |
填充数据 |
18 |
0 |
图4 ARP请求包中 ARP帧的内容
如果我们构造一个这样的包发送出去,如果 192.168.0.99存在且是活动的,我们马上就会收到一个192.168.0.99发来的一个响应包,我们可以查看一下我们的ARP缓存列表,是不是多了一项类似这样的条目:
192.168.0.99 bb-bb-bb-bb-bb-bb
是不是很神奇呢?我们再来看一下ARP响应包的构造:
响应包的填充
有了前面详细的解说,你肯定就能自己说出响应包的填充方法来了吧,所以我就不细说了,列两个表就好了
比如说给 192.168.0.99(MAC为 bb-bb-bb-bb-bb-bb)发一个ARP响应包,告诉它我们的MAC地址为 aa-aa-aa-aa-aa-aa,就是如此来填充各个字段
DLC Header |
||
字段 |
长度(Byte) |
填充值 |
接收方MAC |
6 |
Bbbbbbbbbbbb |
发送方MAC |
6 |
Aaaaaaaaaaaa |
Ethertype |
2 |
0x0806 |
图5 ARP响应包中 DLC Header内容
ARP Frame |
||
字段 |
长度(Byte) |
填充值 |
硬件类型 |
2 |
1 |
上层协议类型 |
2 |
0800 |
MAC地址长度 |
1 |
6 |
IP地址长度 |
1 |
4 |
操作码 |
2 |
2 |
发送方MAC |
6 |
Aaaaaaaaaaaa |
发送方IP |
4 |
192.168.0.1 |
接收方MAC |
6 |
Bbbbbbbbbbbb |
接收方IP |
4 |
192.168.0.99 |
填充数据 |
18 |
0 |
图6 ARP响应包中 ARP帧的内容
这样192.168.0.99的ARP缓存中就会多了一条关于我们192.168.0.1的地址映射。
好接下来是编程实现了:
二、编程的具体实现
通过分析发现以下几条是必须实现的,然后每一个功能近可能的封装在一个函数里面。
1 网卡的枚举,设置和缓冲区分配;
2 填充和发送一个ARP包;
3 监听线程(Sniffer);
4 解析ARP包;
5 获得本机IP MAC地址;
6 发送线程和接受线程的协调;
至于如何显示IP和MAC信息,比较简单请参考源码。
为了更方便的调用winpcap库函数,笔者封装了一个CWinPcap类。代码的几个关键之处说明一下,请参考所附源代码:
1网卡的枚举,设置和缓冲区分配
// success return 0
int CWinPcap::OpenPcap()
{
WCHAR buf[1024];
ULONG bufsize;
int res, i = 0;
memset ((void*)adapterlist, 0, sizeof(adapterlist));
res = PacketGetAdapterNames ((char*)buf, &bufsize); // 枚举网卡
if (res == 0)
{
return -1;
}
WCHAR *p1, *p2;
p1 = p2 = buf;
while ((*p1 != '/0') || (*(p1 - 1) != '/0'))
{
if (*p1 == '/0')
{
memcpy (adapterlist[i], p2, 2 * (p1 - p2));
p2 = p1 + 1;
i++;
}
p1++;
}
m_iAdapterNum = i;
m_iSelAdapter = i - 1;
// 打开最后一个网卡,PC机一般是一个网卡
m_lpAdapter = PacketOpenAdapter( ((char *)adapterlist + m_iSelAdapter * 1024) );
if (m_lpAdapter == NULL || (m_lpAdapter->hFile == INVALID_HANDLE_VALUE))
{
return -1;
}
// 分配sender包空间
m_lpPacketSender = PacketAllocatePacket();
if (m_lpPacketSender == NULL)
return -1;
return 0;
}
2 如何填充和发送一个ARP包
首先是包的分组格式定义
// Ether(DLC) header
typedef struct ethdr
{
unsigned char eh_dst[6]; // 以太网目的MAC地址
unsigned char eh_src[6]; // 以太网源MAC地址
unsigned short eh_type; // 帧类型
}ETHDR,*PETHDR;
// ARP分组格式
typedef struct arphdr
{
unsigned short arp_hdr; // 硬件类型
unsigned short arp_pro; // 协议类型
unsigned char arp_hln; // 硬件地址长度
unsigned char arp_pln; // 协议地址长度
unsigned short arp_opt; // ARP/RARP
unsigned char arp_sha[6]; // 发送者地址
unsigned long arp_spa; // 发送者IP地址
unsigned char arp_tha[6]; // 接受者地址
unsigned long arp_tpa; // 接受者IP地址
}ARPHDR,*PARPHDR;
接着是填充和发送一个ARP请求包
/********************************************************************
功能 供发送线程调用,发送一个arp请求包
参数
ipCur 目标IP地址
ipMine 本机IP地址
mac[] 网卡MAC地址
********************************************************************/
int CWinPcap::Sender(ULONG ipCur, ULONG ipMine, BYTE mac[])
{
char pBufSend[1024];
ETHDR eth;
ARPHDR arp;
// Fill the ARP request packets.
int i;
for (i = 0; i < 6; i++)
{
eth.eh_dst[i] = 0xff;
arp.arp_tha[i] = 0x00;
}
// {填充DLC头和ARP头
eth.eh_type = htons(ETH_ARP);
memcpy(eth.eh_src, mac, 6);
arp.arp_hdr = htons(ARP_HARDWARE);
arp.arp_pro = htons(ETH_IP);
arp.arp_hln = 6;
arp.arp_pln = 4;
arp.arp_opt = htons(ARP_REQUEST);
memcpy(arp.arp_sha, mac, 6);
arp.arp_spa = htonl(ipMine);
arp.arp_tpa = htonl(ipCur);
// }
memset(pBufSend, 0, sizeof(pBufSend));
memcpy(pBufSend, ð, sizeof(eth));
// 装配完整arp包
memcpy(pBufSend + sizeof(eth), &arp, sizeof(arp));
PacketInitPacket(m_lpPacketSender, pBufSend, sizeof(eth) + sizeof(arp));
if(PacketSendPacket(m_lpAdapter, m_lpPacketSender,TRUE)==FALSE)
{
return -1;
}
return 0;
}
3监听线程(Sniffer);
线程函数体实际执行的函数:
int CWinPcap::Sniff()
{
static CIPFluxDlg *pdlg = (CIPFluxDlg *)AfxGetMainWnd();
char recvbuf[1024*250];
DWORD res = 0;
// {初始化网卡,设置为混合模式NDIS_PACKET_TYPE_PROMISCUOUS
if(PacketSetHwFilter(m_lpAdapter, NDIS_PACKET_TYPE_PROMISCUOUS)==FALSE)
{
//printf("Warning: Unable to set the adapter to promiscuous mode/n");
}
if(PacketSetBuff(m_lpAdapter, 500*1024)==FALSE)
{
//printf("PacketSetBuff Error: %d/n",GetLastError());
return -1;
}
if(PacketSetReadTimeout(m_lpAdapter, 1)==FALSE)
{
//printf("Warning: Unable to set the timeout/n");
}
if((m_lpPacketReceiver=PacketAllocatePacket())==FALSE)
{
//printf("PacketAllocatePacket receive Error: %d/n",GetLastError());
return -1;
}
PacketInitPacket(m_lpPacketReceiver, (char *)recvbuf, sizeof(recvbuf));
// }
// {接受包
do
{
if(PacketReceivePacket(m_lpAdapter, m_lpPacketReceiver, TRUE) == FALSE)
{
if(GetLastError() == 6)
return 0;
return -1;
}
GetData (m_lpPacketReceiver); // 解析包的内容
if (pdlg->m_bStop == TRUE)
break;
}while (1);
// }
ResetEvent(pdlg->m_hEvent); // 辅助函数
return 0;
}
4 解析ARP响应包
/********************************************************************
功能 从接受到的包中提取和解析出ARP包个字段
参数
lp 网卡接受到的包的缓冲区指针
********************************************************************/
void CWinPcap::GetData(LPPACKET lp)
{
ULONG ulOffset = 0, ulBytesReceived;
char *buf = NULL;
char *pChar, *pBase;
struct bpf_hdr *phdr = NULL;
struct sockaddr_in sin;
ETHDR *pEther;
ARPHDR *pArp;
//IPHDR *pIphdr;
CString strIP, strMAC;
static CIPFluxDlg *pdlg = (CIPFluxDlg *)AfxGetMainWnd();
buf = (char*)lp->Buffer;
ulBytesReceived = lp->ulBytesReceived;
while (ulOffset < ulBytesReceived)
{
phdr = (struct bpf_hdr *)(buf + ulOffset);
ulOffset += phdr->bh_hdrlen;
pChar = (char *)(buf + ulOffset);
pBase = pChar;
ulOffset = Packet_WORDALIGN(ulOffset + phdr->bh_caplen);
pEther = (PETHDR)pChar;
pArp = (PARPHDR)(pChar + sizeof(ETHDR));
// receive ARP reply packets which contain IP address and relative MAC address
// 受到arp响应包,包含IP地址和相关的MAC地址
if (pEther->eh_type == htons(ETH_ARP) && pArp->arp_opt == htons(ARP_REPLY))
{
sin.sin_addr.s_addr = pArp->arp_spa;
strIP.Format("%-16s", inet_ntoa(sin.sin_addr));
CString str;
str.Format("%02X", pEther->eh_src[0]);
strMAC = str;
for (int i = 1; i < 6; i++)
{
str.Format ("-%02X", pEther->eh_src[i]);
strMAC += str;
}
pdlg->ShowSearch(strIP, strMAC);
SetEvent(pdlg->m_hEvent);
}
}
}
5 获得本机IP地址和MAC地址
工程中没有采取Winpcap函数来获得MAC地址,而是采用非常有用的iphelp库函数DWORD
WINAPI GetAdaptersInfo(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen);
/************************************************************************
功能 获得本机网卡的IP地址,物理地址,涉及到
参数
sin IP地址
strMAC MAC地址16进制表达
mac[] MAC地址字节表达
************************************************************************/
void CWinPcap::GetNetInfo(sockaddr_in &sin, CString &strMAC, BYTE mac[])
{
PIP_ADAPTER_INFO pAdapterInfo = NULL;
char ch;
long sizeinfo;
ULONG size = 0;
int res = 0;
sizeinfo = sizeof(m_netinfo);
if (m_lpAdapter)
{
// 先获得ip地址
res = PacketGetNetInfoEx (adapterlist[m_iSelAdapter], &m_netinfo, &sizeinfo);
if (res)
{
sin = *(struct sockaddr_in *)&m_netinfo.IPAddress;
}
else
{
strMAC = "FF-FF-FF-FF-FF-FF";
}
// 试图获得AdapterInfo,size返回需要的缓冲区的大小
res = GetAdaptersInfo ((PIP_ADAPTER_INFO)&ch, &size);
if (res == ERROR_BUFFER_OVERFLOW)
{
pAdapterInfo = (PIP_ADAPTER_INFO)malloc (sizeof(IP_ADAPTER_INFO));
res = GetAdaptersInfo(pAdapterInfo, &size);
if (res == 0)
{
CString str;
BYTE *pch = pAdapterInfo->Address; // 导出mac地址16进制表达
memcpy (mac, pch, 6);
for (int i = 0; i < 6; i++)
{
if (i)
strMAC += "-";
str.Format("%02X", *(pch + i));
strMAC += str;
}
}
free (pAdapterInfo);
}
}
}
6 发送线程和接受线程的协调
只是简单的多个发送线程,一个监听线程。还没有涉及同步问题。
void CIPFluxDlg::OnBtnStart()
{
int nTotal = 0;
ULONG ipFirst, ipLast;
ULONG ulStartIP = 0;
m_bStop = FALSE;
static BOOL bInit = FALSE;
m_IPFirst.GetAddress(ipFirst);
m_IPLast.GetAddress(ipLast);
nTotal = ipLast - ipFirst + 1;
m_lstIP.DeleteAllItems();
GetDlgItem (IDC_BTN_START)->EnableWindow (FALSE);
// 生成一次监听线程
if (bInit == FALSE)
{
m_hSniffer = AfxBeginThread(_tFuncReceiver, (void*)this);
bInit = TRUE;
}
Sleep (300);
m_IPFirst.GetAddress(ulStartIP);
for (int i = 0; i < nTotal; i++)
{
m_ipCur = ulStartIP + i;
m_hSender = AfxBeginThread(_tFuncSender, (void*)this);
// 等待上一个线程结束
WaitForSingleObject (m_hSender, INFINITE);
}
m_bStop = TRUE;
GetDlgItem (IDC_BTN_START)->EnableWindow (TRUE);
}
四、总结
以上代码思路清晰,代码注释较为详细,列出的代码已过六成,工程也并不大。编程水平高的读者甚至可以很快实现类似的程序!当然还有些不足如:不能严格的发送一个请求ARP包收到后立刻解析显示出来,再发送下一个,而是差不多全部发送完毕后,才显示出来。而是release模式有未处理异常,多线程的异常处理还需要深入了解。
WinXP/VC6.0编译通过,WinXP和Win2000下正常运行。
附加说明:
ARP原理说明引用了《手把手教你玩转ARP包》的部分是为了阅读的方便,本文与另外两篇文章侧重不同,读者可自行比较。关于ARP的问题欢迎读者来讨论。
关于Winpcap如何使用,和编译的问题就不再讨论了。请仔细查看Winpcap开发包帮助,引用的两篇文章。
后记:
从协议原理到代码实现,从Row socket到Winpcap…只要你理解了其中的规律你就可以写出一些类似的实用工具来。希望读者能有所收获!
参考文章、资料:
《手把手教你玩转ARP包》
http://blog.csdn.net/PiggyXP/- PiggyXP- 5/31/2004 11:40:00 AM系列文章
《TCP-IP详解卷1:协议ARP章节,卷2:021.pdf》
《详谈调用winpcap驱动写arp多功能工具》
{
Author: TOo2y[原创]
E-mail: [email protected]
Homepage: http://www.safechina.net/
Date: 11-9-2002
}