页面加载中...

编写网络拓扑结构查看器

日期:2008年5月9日 评论次数:1 Comment » 浏览次数:

软件介绍
首先介绍我编写的网路拓扑结构查看器:NetExplorer。
开发环境:Windows XP SP2 + Microsoft Visual C ++。
测试环境:Windows XP SP2、Windows 2003 SP1。
使用方式
在本程序开始运行时会弹出属性设置对话框.在该属性设置界面中,我们必须指定初始探测路由器的IP地址,所属的团体名以及递归深度,设定好参数后,单击确定,程序跳转到主界面.
操作界面十分简单,只有“开始探测”,“设置参数”和“群Ping”三个功能,分别对应“空格”,“S”和“P”三个快捷键。其中“设置参数”功能提供更改参数设置的机会,“群Ping”功能可以对指定(用户输入)子网内的主机进行探测。
点击“开始探测”后,本程序开启后台线程,与目的路由器通讯,获得路由器的相关信息,并在屏幕上显示。展示了代表单个路由器的图标和其提示菜单。
该程序会继续获得该路由器路由表中的“下一跳步”列表,并尝试与获得表中的路由器通讯,并在屏幕上绘制代表该路由器的图标。
在任意图标上单击鼠标左键,会弹出显示该路由器详细信息的对话框。

在任意图标上单击右键,会弹出一个菜单,包含对该路由器的相关操作选项。
“显示路由器详细信息”功能与单击鼠标左键相同;“群Ping该路由器所属网段中的主机”会基于ICMP协议探测与路由器处于同一网段中的主机信息;“更改探测使用的团体号”可以设置新的团体号,并以新的团体号开始继续探测路由器拓扑信息。
在程序主界面的左侧,是通过树状结构显示的网络中路由器和主机分布情况,鼠标单击相应子网程序会自动获得对应子网内全部主机相关信息。
以上就是程序的使用方法,下面该介绍程序的编写过程了。在介绍编写过程之前,首先还是要介绍一下SNMP协议——也就是我这个程序和路由器通讯使用的协议。
小知识:简单网络管理协议(SNMP:Simple Network Management Protocol)是由互联网工程任务组(IETF:Internet Engineering Task Force )定义的一套网络管理协议。该协议基于简单网关监视协议(SGMP:Simple Gateway Monitor Protocol)。利用SNMP,一个管理工作站可以远程管理所有支持这种协议的网络设备,包括监视网络状态、修改网络设备配置、接收网络事件警告等。虽然SNMP开始是面向基于IP的网络管理,但作为一个工业标准也被成功用于电话网络管理。
SNMP采用了Client/Server模型的特殊形式:代理/管理站模型。对网络的管理与维护是通过管理工作站与SNMP代理间的交互工作完成的。每个SNMP代理负责回答SNMP管理工作站关于MIB定义信息的各种查询。
管理信息数据库(MIB)是一个信息存储库,它包含了管理代理中的有关配置和性能的数据,有一个组织体系和公共结构,其中包含分属不同组的许多个数据对象。MIB数据对象以一种树状分层结构进行组织,这个树状结构中的每个分枝都有一个专用的名字和一个数字形式的标识符。
SNMP协议定义了数据包的格式,及网络管理员和管理代理之间的信息交换,它还控制着管理代理的MIB数据对象。因此,可用于处理管理代理定义的各种任务。SNMP协议之所以易于使用,这是因为它对外提供了三种用于控制MIB对象的基本操作命令。它们是:Set 、Get 和 Trap : 
1. Set:它是一个特权命令,因为可以通过它来改动设备的配置或控制设备的运转状态。
2. Get:它是SNMP协议中使用率最高的一个命令,因为该命令是从网络设备中获得管理信息的基本方式。
3. Trap:它的功能就是在网络管理系统没有明确要求的前提下,由管理代理通知网络管理系统有一些特别的情况或问题发生了。 
在我们的程序中,主要使用Get命令从各个路由器获得相关信息。
小提示:SNMP协议是一种基于UDP的协议,采用的是一问一答模式,结构简单。所以也易于我们操作。
程序编写
1.主体结构
本程序在设计上采用了分而治之的思路,在底层负责数据的构造、存储、管理和析构,中层负责维护图状数据结构和判断回路等功能,上层负责绘制屏幕,工作者线程负责与路由器通讯并获得相关信息,尽量降低各层之间的耦合度,简化了编写难度。
2.执行流程
下面依次介绍各个功能模块的执行流程:
首先介绍工作者线程的工作流程,传递给该线程的参数包括如下几项:初始路由器IP地址、团体名、当前递归深度和现在待绘制节点的父节点(若待绘结点为根节点,也就是探测的初始节点,则调用的参数为NULL)。
在确定本节点是否为父节点后,尝试获得本路由器的IP地址列表,并在文档中生成相应的数据对象,放在文档的RouterEnty列表末尾,然后向视图发送消息通知视图取得文档的RouterEnty列表末尾元素,并绘制该元素;
此后本线程尝试获得该路由器的下一条步地址列表,并且对该列表中获得的IP地址递归调用此函数。
小提示:这里有一点需要注意,这里提到的递规调用,并不是传统意义上的递规,而是通过开启新线程进行调用,这样的调用方式可以使对同一路由器不同相邻路由器的探测并行进行,极大地提高了效率。
另外两个参数分别传入本次递归生成节点的指针和下次递归的深度。
代码如下:
DWORD WINAPI RecursionFun(LPVOID lpParam )
{
……………… //省略非重要代码若干,详细代码请参见光盘中相关文档。
if(TheTool.GetNextHopTable(NextHopTable,Length))//get next hop table success!
{
HANDLE *ThreadHandle=new HANDLE[Length];
for(int i=0;i{
RecursionFunStruct * Param=new RecursionFunStruct;
Param->CurrentDepth=CurrentDepth+1;
Param->DestIp=NextHopTable;
Param->TheParament=&TheParament;
Param->Father=This;
DWORD dwThreadId;
ThreadHandle=CreateThread(NULL, 0,RecursionFun, Param,0,&dwThreadId); //针对当前路由器下一跳步的每一项内容创建一个线程并行探测
}
WaitForMultipleObjects(Length,ThreadHandle,TRUE,INFINITE);
delete []ThreadHandle;
return 1;
}
else
{
return 0;
}
}//end of this node is not the head!!
}
}
之后是CNetExplorerView,该类主要负责绘制工作,它提供两项服务,一是绘制代表路由器的图标,二是在Invalid之后对屏幕进行重绘。
首先介绍第一项工作,该类执行流程比较简单,故不提供流程图。CNetExplorerView内部维护一个数组,每个元素对应屏幕上路由器图标相应列的当前深度,每次接到重绘图标的消息后,读取CNetExplorerDoc中的相关链表,取得链表末尾元素。判断该元素在屏幕所处的列(根据该元素的递归深度),并且根据该列的当前深度为该图标分配位置。在屏幕上绘制图标,最后设置Invalid。
第二项工作是在Invalid后重新绘制代表路由器连接信息的直线,在OnDraw函数中,取得CNetExplorerDoc中的保存连线的链表,并且依次获得连线位置,在屏幕上绘制;此外,该类还有一项额外的功能,就是在滚动条被拖动的同时重新计算个路由器图标的新坐标。
下面介绍“Class DistributeDate”,这个类主要负责维护数据结构,在该类内部,储存着标识路由器结构的链表。每次添加新的结构对象,都会遍历原有的对象,判断是否存在相同路由器,从而避免环路造成的无穷递归,如果没有重复,则直接将新节点连接到其对应父节点的子节点序列上。否则只添加一条连接对应的连线,不进行其它操作,这样就维护了路由器拓扑图的生成树结构。
最后是“Class NetExplorerDoc”,它用来维护各类数据的构造和析构,在需要添加操作时,使用New操作动态生成,并返回相关指针。在程序退出的时候,统一Delete,确保内存不泄漏。
3.相关代码介绍
首先介绍与SnmpAgent通讯的工具类.
class SnmpTool
{
public :
SnmpTool(ULONG DestIP,char* CommunityName );
bool GetEnterfaceDescription(CString *Tab,int &Length);
bool GetIpTable(ULONG *Tab,int &Length);
bool GetNetMaskTable(ULONG *Tab,int &Length);
bool GetNextHopTable(ULONG *Tab,int &Length);
bool GetEnterfaceSpeed(ULONG*Tab,int &Length);
~SnmpTool();
private:

AsnObjectIdentifier IpTable;
AsnObjectIdentifier NextHop;
AsnObjectIdentifier EthDescription;
AsnObjectIdentifier EthSpeed;
AsnObjectIdentifier NetMask;
char CommunityName[64];
ULONG DestIP;
LPSNMP_MGR_SESSION m_lpMgrSession;
bool Error;
};
IpTable、NextHop、EthDescription、EthSpeed、NetMask这五个成员变量,分别存储获得IP地址列表,下一跳步列表,接口描述,接口速度和子网掩码的MIB标识符。DestIP和CommuityName是目的路由器的IP地址和团体号,剩下的是两个用来内部计算的临时变量。
另外五个函数分别用来获得接口描述符、IP地址列表、子网掩码、下一跳地址列表和接口速度。原理大同小异,都是使用相关的MIB标识符调用系统API SnmpMgrRequest(),并且解析其返回的数值。
bool GetNextHopTable(ULONG *Tab,int &Length)
{
if(this->Error)//处理初始化错误情况
{
return false;
}
AsnInteger errorStatus=0;
AsnInteger errorIndex=0;
SnmpVarBindList snmpVarList;
snmpVarList.list = NULL;
snmpVarList.len = 0;
snmpVarList.len++;
snmpVarList.list = (SnmpVarBind *)SNMP_realloc(snmpVarList.list, sizeof(SnmpVarBind)*snmpVarList.len);
//上面8行初始化一些调用系统API所需的变量
if(snmpVarList.list==NULL)
{
return false;
}
SnmpUtilOidCpy(&snmpVarList.list[0].name,&(this->NextHop));
snmpVarList.list[0].value.asnType = ASN_NULL;
//确定所需获得的MIB元素为Next Hop
ULONG IpTable[256];
int Len=0;
int Fault=0;
while(1)
{
//开始循环获得Next Hop变量 if(SnmpMgrRequest(m_lpMgrSession,SNMP_PDU_GETNEXT,&snmpVarList,&errorStatus,&errorIndex)==0)
{
Fault++;
if(Fault==3)//出错最多重新尝试三次
{
return false;
}
}
   if(SnmpUtilOidNCmp(&snmpVarList.list[0].name,&(this->NextHop),this->NextHop.idLength))//是否获取完毕
{
break;
}
if(snmpVarList.list[0].value.asnType!=ASN_RFC1155_IPADDRESS)
{//是否出错
return false;
}
else
{
if(snmpVarList.list[0].value.asnValue.address.length!=4)
{//也是错误判断
return false;
}
else
{
ULONG Tmp=(*(ULONG*)(snmpVarList.list[0].value.asnValue.address.stream));//取道所获得的值
if(Tmp==inet_addr("127.0.0.1")||Tmp==0)
{
continue ;
}
else
{
IpTable[Len]=Tmp;
}
}
}
Len++;
if(Len==Length)
{
return false;
}
}
SnmpUtilOidFree(&snmpVarList.list[0].name); //释放SnmpUtilOidCpy调用时分配的空间
SNMP_free(snmpVarList.list); //释放SNMP_realloc分配的空间
Length=Len;
for (int i=0;i{
Tab=IpTable;
}
return true;
}

小知识:SNMP协议唯一的身份标识就是团体号,默认为Public。
接下来介绍CrouterButton、路由器的图标的拖动、Tip提示符,右键菜单等功能的实现类:
class CRouterButton : public CButton
{
DECLARE_DYNAMIC(CRouterButton)
public:
CRouterButton();
virtual ~CRouterButton();
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
BOOL ChangeTipText(char * PTipString);
afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
afx_msg void OnGetRouterParticularInformation();
afx_msg void OnPingAllHost();
afx_msg void OnChangeGroupID();
virtual BOOL PreTranslateMessage(MSG* pMsg);
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
BOOL MyCreate(CPoint CreatePoint, CBitmap &RouterBmp,char * PTipString ,CWnd *Father);
CResultOfPing *ResultOfPingDlg;
long OldPositionX,OldPositionY;
long Heigh,Width;
CToolTipCtrl Tip;
char *TipString;
CWnd *Father;
CPoint *PReferencePoint;
bool *PScrolled;
CList IPList;
CRouoterInfoDialog *RouterInfoDialog;
ULONG ValidIp;
char ValidCommunityName[64];
protected:
DECLARE_MESSAGE_MAP()
private:
CPoint OldPoint;//拖动过程中的中转数据
bool Eraser;//拖动过程中的中转数据
};
这个类比较复杂,是因为它继承自MFC基类CButton,我们简单介绍其中主要功能函数,详情请参阅光盘代码。
afx_msg void OnMouseMove(UINT nFlags, CPoint point):绘制线框,代表拖动过程;
BOOL ChangeTipText(char * PTipString):更改该路由器图标的Tip提示;
afx_msg void OnRButtonDown(UINT nFlags, CPoint point):打开功能菜单,见第三页图;
afx_msg void OnGetRouterParticularInformation():显示该路由器的特定信息;
afx_msg void OnPingAllHost():使用ICMP协议探测路由器所在子网的所有主机;
afx_msg void OnChangeGroupID():使用新的团体号探测该路由器;
virtual BOOL PreTranslateMessage(MSG* pMsg):提供Tip初始化的某些功能
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct):调用基类的相应函数,并且设置初始的Tip;
afx_msg void OnLButtonDown(UINT nFlags, CPoint point):和下面的函数一起,计算移动的终止位置,并且完成移动;
afx_msg void OnLButtonUp(UINT nFlags, CPoint point):见上;
BOOL MyCreate(CPoint CreatePoint, CBitmap &RouterBmp,char * PTipString ,CWnd *Father):在相应位置绘制代表路由器的图标;

下面来介绍本程序至关重要的几个数据结构:
struct Line
{
void *PFrom;
void *PTo;
};
标识用户区代表路由器之间连接的连线,两个指针分别是起点和终点对应的RouterEnty指针;
struct RouterEnty
{
CRouterButton ButtonEnty;
CList ChildList;
CList RouterIP;
int CurrentPosition;

};
该结构的每一个对象代表一个路由器,内部有用来绘制图标类对象的ButtonEnty,连接其所有子节点的Line的列表,IP地址列表,和在屏幕上所处的列的相关信息;
class DistributeDate
{
public:
CCriticalSection Section;
RouterEnty* AddChild(RouterEnty *Father,CList &Child);
Line* NewLine(RouterEnty *PFrom,RouterEnty *PTo);
RouterEnty* NewRouterEnty(CList &RouterIp);
CList AllLine;
CList AllRouterEnty;
DistributeDate();
~DistributeDate();
};
这个类用来维护上述两个结构AllLine, AllRouterEnty储存了所有的Line和RouterEnty元素的指针,他们分别由NewLine ()和NewRouterEnty()动态生成;并且在析构函数中统一delete。而AddChild()完成给某元素添加子节点的工作。上文已经说过,本程序维护一个路由器连接拓扑图的生成树,此函数就用来判断添加该节点是否会生成回路并执行相应操作的。
4.特定子功能模块分析
在这里我想着重介绍群Ping某网段主机的模块,这是因为它使用了异步过程调用技术(APC),这种技术可以使用单线程并行处理多个来源的IO数据,使并行操作的系统代价较之多线程大大减小,是编写高性能多客户端后门程序的必备之选。
群Ping某子网内主机的功能是基于微软库函数IcmpSendEcho2实现的。
此功能模块使用了Windows异步过程调用机制APC,大大提高了程序效率,每次发送数据包完成后,系统在收到对应的回复数据包后会给该线程发送APC请求。在SleepEx的过程中,线程处于Alterable状态。此时,APC请求的到来就会激活该线程,完成对应的APC操作,通过这种机制实现收发异步,极大的提高了程序运行效率。
我们来结合代码分析一下:
void ApcFunc(void *i)//这就是收到数据包后处于Alterable状态的线程自动调用的函数
{
IcmpThreadStruct* PIcmpThreadStruct=((ApcParament2*)i)->PIcmpThreadStruct;
if(*PIcmpThreadStruct->Stop==false)
{
ICMP_ECHO_REPLY* P_Icmp_Echo_Option=(ICMP_ECHO_REPLY*)i;
ULONG DestIP=(ULONG)P_Icmp_Echo_Option->Address;
hostent *HostInfo=NULL;
tool TheTool;
if(P_Icmp_Echo_Option->RoundTripTime<100000)
{
sprintf((char*)PIcmpThreadStruct->Buffer,"host Address: %-15s; Time Spend: %d ms\r\n",
TheTool.NetIpToStr((ULONG)P_Icmp_Echo_Option->Address),
P_Icmp_Echo_Option->RoundTripTime);
}
else
{
sprintf((char*)PIcmpThreadStruct->Buffer,"host Address: %-15s; Time Out\r\n",TheTool.NetIpToStr((ULONG)P_Icmp_Echo_Option->Address));
}
PIcmpThreadStruct->MessageHandler->SendMessage(WM_PING,PIcmpThreadStruct->wp,PIcmpThreadStruct->lp);//向显示探测结果的对话框发送消息
}
}
int SearchTheLan(ULONG DestIP,ULONG NetMask,IcmpThreadStruct* PIcmpThreadStruct)
{
PIcmpThreadStruct->ApcRoutine=(FARPROC)&ApcFunc;;
WSADATA tmp;
if(WSAStartup(MAKEWORD(2,1),&tmp)!=0)
{
return -1;
}
HMODULE hInst=LoadLibrary("iphlpapi.dll");
if(!hInst)
{
return -1;
}
//依次获得所需的三个函数指针
PIcmpThreadStruct->IcmpCreateFile=(PIcmpCreateFile)GetProcAddress(hInst,"IcmpCreateFile");
PIcmpThreadStruct->IcmpSendEcho2=(PIcmpSendEcho2)GetProcAddress(hInst,"IcmpSendEcho2");
PIcmpThreadStruct->IcmpCloseHandle=(PIcmpCloseHandle)GetProcAddress(hInst,"IcmpCloseHandle");
if(PIcmpThreadStruct->IcmpCreateFile==NULL||PIcmpThreadStruct->IcmpSendEcho2==NULL||PIcmpThreadStruct->IcmpCloseHandle==NULL)
{
return -1;
}
HANDLE IcmpHandle=0;
IcmpHandle=PIcmpThreadStruct->IcmpCreateFile();//打开ICMP句柄
if(IcmpHandle==0)
{
return -1;
}
else
{

IP_OPTION_INFORMATION IpOption;// 该结构用来控制所发ICMP数据包的IP头的相应字段值
IpOption.Flags=0;
IpOption.OptionsData=NULL;
IpOption.OptionsSize=0;
IpOption.Tos=0;
IpOption.Ttl=123;
char *SendData = "DF is the best!";
ApcParament2 *ReplyBuffer=new ApcParament2[~ntohl(NetMask)];
ULONG DestAddress=ntohl(NetMask&DestIP);
int NumberOfIP=((~(ntohl(NetMask)))-1);
for(int i=0;i{
ReplyBuffer.PIcmpThreadStruct=PIcmpThreadStruct;
DestAddress++;
int Res=0;
Res=PIcmpThreadStruct->IcmpSendEcho2(IcmpHandle,NULL,PIcmpThreadStruct->ApcRoutine,(void*)(&ReplyBuffer),htonl(DestAddress),SendData,(WORD)strlen(SendData),&IpOption,ReplyBuffer.Buffer,512,35000);
SleepEx(1,true);//中间进行一下等待处理已经收到的数据包
if(*PIcmpThreadStruct->Stop)
{
break;
}
}//end of while
}
while((*PIcmpThreadStruct->Stop)==false&&SleepEx(5000,true)==WAIT_IO_COMPLETION );//确保收到全部的数据包
PIcmpThreadStruct->IcmpCloseHandle(IcmpHandle);
WSACleanup();
if(*PIcmpThreadStruct->Stop!=true)
{
PIcmpThreadStruct->MessageHandler->SendMessage(WM_PING_FINISH,PIcmpThreadStruct->wp,PIcmpThreadStruct->lp);
}
return 0;
}
小结
现在还有很多学校网管的安全性很差,他们配置的路由器团体号还都是默认的Public。在这种情况下,使用本程序可以轻而易举的获得学校网络的拓扑结构。即便没有设置成Public,大多数团体名也是可以通过嗅探等方法得到,怎么样?这次在局域网中探险有地图了吧。

1 Comments.

  1. lily 说道:
    1楼

    很想看看作者的源码哦,我看到类似源码,但是资源文件找不到 无法实现完成编译!<br/>可以联系我 <a href="mailto:zifengling_813@163.com">zifengling_813@163.com</a>

Leave a Reply

回到顶部