网络相关 – 涛哥的blog http://www.138soft.com Wed, 06 Dec 2017 15:16:54 +0000 zh-CN hourly 1 https://wordpress.org/?v=6.2.2 http://www.138soft.com/wp-content/uploads/2018/03/cropped-timg-32x32.jpg 网络相关 – 涛哥的blog http://www.138soft.com 32 32 关于Winsock LSP、SPI、NSP、网游加速器那些事 http://www.138soft.com/?p=501 http://www.138soft.com/?p=501#comments Wed, 16 Aug 2017 13:53:49 +0000 http://www.138soft.com/?p=501         最近VPN都被封了,无法使用Google,只好自己动手,丰衣足食了。

        一个是基于ifslsp的,支持TCP重定向:DeProxifier1.0

        另外一个是基于nonifslsp的,支持TCP和UDP重定向:DeProxyCap1.0

        有需要的朋友请点击上面的链接下载使用吧。嗯,跟本站所有软件一样,是完全免费的。当然,这种软件杀毒软件是百分之百报毒的,介意的朋友就不要用了。

        关于LSP,也不打算多做介绍了,请自行搜索吧。如果你正在开发LSP和NSP,那么可以往下看看,我会说说里面的几个要点。如果你没有这方面的基础,看了也是没有用的。

一、关于TCP重定向的实现。

        TCP重定向的实现方法有很多,有些方法是先强制把原来的Socket改为阻塞模型(例如:对于消息模型的,先调用WSPAsyncSelect(s, hWnd, 0, 0, err)取消消息映射;对于事件模型的,先调用WSPEventSelect(s, 0, 0, err)取消事件),跟代理服务器握手成功后,再恢复原来的模型;有些方法是使用状态机;而最简单和最流行的方法是:把连接重定向到127.0.0.1本地自己程序监听的端口,由该程序来完成握手。之所以需要考虑这些问题,是因为当connect函数返回后,原来的程序便开始收发数据了,而LSP层还在和代理服务器协商,因为使用的是同一个socket句柄,所以这两个过程的数据收发是可能导致数据混乱的。
        目前基本上所有LSP程序都是使用最后一个方法,因为还有个原因:对于网络游戏,如果你重定向到真正代理服务器,这个过程是可能消耗一定时间的,可能会导致网络游戏认为连接超时而断开连接,把连接重定向到127.0.0.1就没事了。

二、关于UDP重定向的实现。

        UDP的重定向不像TCP仅处理WSPConnect和WSPConnectEX即可,它涉及到数据的收发,对应的两个函数是:WSPSendto和WSPRecvFrom,我们需要在发送的时候将真正的地址加到原数据头,然后将发送地址改为代理服务器的;在接收返回后,从头数据取出真正服务器的IP并去掉该头数据。我们先来看看,如果使用API HOOK是如何处理接收函数的:

        如果需要截获实际接收的数据的话,单纯的通过WSPRecvFrom这个函数是很片面的,需要分几种情况说明:
        1、如果lpOverlapped为nil,那么是阻塞式的,直接在原函数返回后处理即可。
        2、如果lpOverlapped不为nil,且lpCompletionRoutine不为nil,那么需要使用自己的函数替换lpCompletionRoutine;并在调用后执行调用用户原函数。因为这时是完成例程通知模型。
        3、如果lpOverlapped不为nil,且lpCompletionRoutine为nil,并且lpOverlapped的hEvent不为nil,那么需要hook WSAGetOverlappedResult。因为这时是事件通知模型。
        4、如果lpOverlapped不为nil,且lpCompletionRoutine为nil,并且lpOverlapped的hEvent为nil,那么需要hook GetQueuedCompletionStatus,因为这时是完成端口的方式。

        现在的问题在于,LSP并不提供类似GetQueuedCompletionStatus这种API函数的处理,所以如果仅使用LSP,而且是ifslsp模型的话,那么对于非阻塞模型的UDP都是无法解决的事情。幸好微软还提供了一个nonifslsp的框架,里面自带了一个透明代理层。

        这里额外说说一些常识(摘录自网上):“LSP分两种:一种是IFS LSP,一种是non IFS LSP.简单地说, IFS LSP制作简单,可以完成大部分的数据包监听工作; non IFS LSP制作复杂,但是可以进行一些特殊的overlapped I/O操作,如在overlapped初始化完成后,调用WSPSend (WriteFile), WSPSendTo, WSPRecv (ReadFile), WSPRecvFrom, or WSPIoctl之前,对数据进行一些处理工作.LSP相互之间可以叠加,但在non IFS LSP之上不可以叠加IFS LSP.也就是说,如果一个BSP是non IFS,则第三方提供的LSP必须是non IFS,否则无法安装在SPI上.”当然,世事无完美,nonifslsp也存在一些兼容性问题,例如会导致SetFileCompletionNotificationModes函数运行不正确。详见微软介绍:https://support.microsoft.com/en-hk/help/2568167/setfilecompletionnotificationmodes-api-causes-an-i-o-completion-port-n

三、关于NSP重定向的实现。

        NSP重定向DNS虽然都是在NSPLookupServiceNext里面返回域名对应的IP地址,但是实际上也有很多方法。一种是使用UDP自己构造DNS请求包返回IP,然后替换。这个比较消耗时间。另外一种是远程连接模式,例如:当应用程序解释域名www.138soft.com的时候,NSPLookupServiceNext返回127.8.0.0(这个IP是递归的),同时在内存里面添加一个对应的映射记录:www.138soft.com:127.8.0.0,当应用程序连接到127.8.0.0时,127.8.0.0这个连接与远程代理服务器协商,告诉对方连接到www.138soft.com(代理服务器基本上都支持IP重定向和域名重定向),这时候使用的实际上就是代理服务器那边的DNS了。

]]>
http://www.138soft.com/?feed=rss2&p=501 4
Winsock异步转换域名到IP的一个需要注意的地方 http://www.138soft.com/?p=434 http://www.138soft.com/?p=434#comments Sat, 06 Dec 2014 15:03:16 +0000 http://www.138soft.com/?p=434         转换域名到IP是网络编程中使用的很频繁的功能,一般情况下gethostbyname即可,但这个函数是阻塞的,如果你的网络程序框架是基于异步的(例如消息socket、完成端口等),那么可能需要一个异步的函数。微软本身提供了一个异步的函数叫WSAAsyncGetHostByName,调用的时候,需要提供一个窗口的句柄和消息定义,当异步返回的时候,会触发该窗口的消息。问题在于,这个函数是顺序的。举个例子:你按顺序查询www.163.com、www.sina.com……,假如前面的163没有返回,实际上后面的sina也需要等待。也就是说,它其实是按顺序查询的。或许针对每个查询新建一个窗口来接收消息可以避免这个问题,感兴趣的朋友可以尝试一下。另外一个解决方案是自己构造DNS查询包来实现异步查询功能。

]]>
http://www.138soft.com/?feed=rss2&p=434 4
WinPcap权威指南(四):UDP与DNS欺骗 http://www.138soft.com/?p=420 http://www.138soft.com/?p=420#comments Thu, 07 Aug 2014 07:41:10 +0000 http://www.138soft.com/?p=420         上一节介绍了ARP协议之后,群里面有朋友说ARP欺骗不是很简单么?实际上,实现ARP欺骗是很简单的,难点在于数据转发的速度。2004年我们做隔山打牛的时候,曾经去过一家网吧测试,刚开始是开了ARP欺骗几分钟就大量主机掉线,客人骂声一片(幸好是朋友的网吧,否则估计会被老板丢出去)。当然,那时候网上还没有ARP欺骗的介绍,更加没有什么ARP防火墙,ARP欺骗的防范是几年后的事情了。后来我们改进了算法,经过实际测试,250台电脑,原来的ICMP速度是多少(ping),开启ARP欺骗后就是多少,没有任何延迟,机器更加不会掉线。因为我们的代码仅出于演示目的,所以捕获线程和分析线程共用了一个List,然后加锁,如果你直接拿去网吧用,百分之百是被网吧老板抓住狂揍一顿的。

        群里还有朋友说cain一开,整个局域网会掉线,这个实际上是cain处理的不好:它启动ARP欺骗后,并没有对现有连接的seq和ack等做转换,另外还有一种解决方法是被动欺骗。不过实际上,数据修改并不一定要上ARP欺骗这种中间人方式,不使用ARP欺骗而达到数据修改,至少有三种方法,后面我们会介绍一两种,至于第三种,因为命中率百分之百,为了造成不良后果,以后有机会再说。

        上一节我们说了,网络通信的第一步是通过ARP协议获取目标电脑的物理地址—局域网内通信是获取目标电脑的,互联网是获取网关的,那第二步呢?答案当然是IP地址。所有基于IP的网络协议(例如icmp、tcp、udp)都必须拥有这个,但是因为ip难以记忆,而且有可能改变,所有实际应用中一般使用域名协议。例如,我们打开cmd,然后ping 一下我们的网站www.138soft.com,结果如下图:

xwinpcap8

        从上图可以看到,ICMP协议会先获取www.138soft.com对应的ip地址,然后再跟它收发数据,用过浏览器的朋友们,实际上当你们输入域名的时候,浏览器也会有这个过程。写过网络程序的朋友就清楚的多,因为这个就是gethostbyname函数。这个过程,实质上就是DNS协议。

        DNS协议的规范定义这里不作介绍了,我们只从程序的角度来说一下。DNS协议一般使用UDP,而且端口一般是53。DNS数据包分为查询包和应答包,其中查询包包含了这个包的ID(用于区别其它的查询包)、包的类型(这里为查询)、查询的类型(比如说:IP地址是A记录、MX发信地址是MX记录),DNS服务器的反馈包结构,前面是这个请求包(但包类型修改为反馈包),后面紧跟着具体的应答包。下面我们通过自己写的一个程序来重现这个过程(实际上,这个就是系统的gethostbyname函数的实现)。首先,我们使用iphlp库来获取本机的DNS服务器IP:

function GetDNSServerIP: AnsiString;
var
  dwBuffSize: Cardinal;
  pFinxedInfo: PFIXED_INFO;
  pIPAddr: PIP_ADDR_STRING;
begin
  Result := '';

  if (GetNetworkParams(nil, dwBuffSize) <> ERROR_BUFFER_OVERFLOW) then Exit;
  GetMem(pFinxedInfo, dwBuffSize);
  if pFinxedInfo = nil then Exit;
  if (GetNetworkParams(pFinxedInfo, dwBuffSize) <> ERROR_SUCCESS) then
  begin
    FreeMem(pFinxedInfo);
    Exit;
  end;
  pIPAddr := @pFinxedInfo^.DnsServerList;
  while (pIPAddr <> nil) do
  begin
    if Result = '' then Result := Format('%s', [pIPAddr^.IpAddress.S]);
    pIPAddr := pIPAddr^.Next;
  end;
  FreeMem(pFinxedInfo);
end;

        有个DNS地址,就可以构造请求包发送和接收了:

procedure TForm1.Button1Click(Sender: TObject);
//这里省略部分内部函数,具体看代码,函数来源于Indy
var
  AUDPSocket: TSocket;
  SockAddrIn: TSockAddrIn;
  strHostName: AnsiString;
  nDNSPort: Integer;
  QueryID: Word;
var
  nLen, nRet: Integer;
begin
  (Sender as TButton).Enabled := False;
  try
    if Trim(Edit_Host.Text) = '' then
    begin
      ShowMessage('请输入域名!');
      Edit_Host.SetFocus;
      Exit;
    end;

    if Trim(Edit_DNSServer.Text) = '' then
    begin
      ShowMessage('请输入DNS服务器IP!');
      Edit_DNSServer.SetFocus;
      Exit;
    end;

    if Trim(Edit_DNSPort.Text) = '' then
    begin
      ShowMessage('请输入DNS服务器端口!');
      Edit_DNSPort.SetFocus;
      Exit;
    end;

    strHostName := AnsiString(Trim(Edit_Host.Text));

    nDNSPort := StrToIntDef(Trim(Edit_DNSPort.Text), 53);

    AUDPSocket := socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if AUDPSocket = INVALID_SOCKET then Exit;
    SockAddrIn.sin_family := AF_INET;
    SockAddrIn.sin_port := htons(nDNSPort);
    SockAddrIn.sin_addr.s_addr := inet_addr(PAnsiChar(AnsiString(Trim(Edit_DNSServer.Text))));


    Randomize;
    QueryID := Random(65535);
    Buffer.Count := 0;
    Buffer.ID := Swap(QueryID); //查询的ID
    Move(Header_Option[1], Buffer.Option, 10);
    StringToLabel(strHostName, SubLabel);
    Move(SubLabel[1], Buffer.Data[0], Length(SubLabel));
    Buffer.Count := Length(SubLabel);
    QType := #0#1; //A记录
    Move(QType[1], Buffer.Data[Buffer.Count], 2);
    Buffer.Count := Buffer.Count + 2;
    Move(QClass_Internet, Buffer.Data[Buffer.Count], 2);
    Buffer.Count := Buffer.Count + 14; // 2 + 12

    if sendto(AUDPSocket, PAnsiChar(@Buffer.ID)^, Buffer.Count, 0, SockAddrIn, sizeof(SockAddrIn)) = SOCKET_ERROR then
    begin
      CloseSocket(AUDPSocket);
      ShowMessage('数据发送失败!');
      Exit;
    end;

    nLen := sizeof(SockAddrIn);
    nRet := recvfrom(AUDPSocket, PAnsiChar(@Buffer.ID)^, sizeof(Buffer), 0, SockAddrIn, nLen);
    if nRet = SOCKET_ERROR then
    begin
      CloseSocket(AUDPSocket);
      ShowMessage('数据接收失败!');
      Exit;
    end;
    CloseSocket(AUDPSocket);
    //==========================================================================
    if Buffer.ID = Swap(QueryID) then
    begin
      i := 0;
      while Buffer.Data[i] <> #0 do Inc(i);
      i := i + 5;
      while (Buffer.Data[i] <> #32) and (i < nRet) do
      begin
        LabelToString(@Buffer.Data, i, RRDomain);
        Inc(i);
        RRDataType := Ord(Buffer.Data[i]);
        i := i + 9;
        case RRDataType of
          QType_CNAME: Memo1.Lines.Add(GetCNAME(Buffer, i));
          QType_NS: Memo1.Lines.Add(GetNS(Buffer, i));
          QType_A: Memo1.Lines.Add(GetA(Buffer, i));
          QType_SOA: Memo1.Lines.Add(GetSOA(Buffer, i));
          QType_PTR: Memo1.Lines.Add(GetPTR(Buffer, i));
          QType_MX: Memo1.Lines.Add(GetMX(Buffer, i));
        else
          begin
            uShort.B[1] := Buffer.nData[i - 2];
            uShort.B[0] := Buffer.nData[i - 1];
            i := i + uShort.Value;
          end;
        end;//end for "case"
      end;//end for "while"
    end;//end for "if"
  finally
    (Sender as TButton).Enabled := True;
  end;
end;

        程序运行界面如下:

xwinpcap9

        对应的,我们再来实现一个DNS服务器,代码如下:

const
    HOSTIP='127.0.0.1';//需要返回的IP地址

procedure TForm1.ReadData(var Message: TMessage);
var
  client_addr: TSockAddrIn;
  szBuffer: array[0..4095] of AnsiChar;
  len: integer;
  flen: integer;
  Event: word;
var
  strRecvData: AnsiString;

  ID, Code, QDCount: Word;
  BitCode: TDNSBitCode;
  i: integer;
  APos: Integer;
  DomainName: AnsiString;
  QueryClass: Word;
  QueryType: Word;

  strSend: AnsiString;
begin
  flen := sizeof(client_addr);
  Event := WSAGetSelectEvent(Message.LParam);
  if Event = FD_READ then
  begin
    len := recvfrom(m_UDPSocket, szBuffer, sizeof(szBuffer), 0, client_addr, flen);
    if len <= 0 then Exit;
    if len > 512 then Exit;

    SetLength(strRecvData, len);
    Move(szBuffer[0], strRecvData[1], len);

    if Length(strRecvData) < sizeof(TDNSHeader) then Exit;
    ID := TwoCharToWord(strRecvData[1], strRecvData[2]);
    Code := TwoCharToWord(strRecvData[3], strRecvData[4]);
    BitCode := GetDNSBitCode(Code);
    if BitCode.QR <> 0 then Exit; //0表示查询,1表示反馈
    QDCount := TwoCharToWord(strRecvData[5], strRecvData[6]);
    if QDCount <= 0 then Exit;

    APos := 13; // DNS头恒为12字节,我们从下一字节开始分析
    for i := 1 to QDCount do
    begin
      DomainName := DNSStrToDomain(strRecvData, APos);
      if DomainName = '' then Exit;
      QueryType := TwoCharToWord(strRecvData[APos], strRecvData[APos + 1]);
      Inc(APos, 2);
      QueryClass := TwoCharToWord(strRecvData[APos], strRecvData[APos + 1]);
      Inc(APos, 2);
      Memo1.Lines.Add(DomainName);
      (*
      m_strInfo := 'DNS查询包' + #$D#$A;
      m_strInfo := m_strInfo + SourceIP + ':' + IntToStr(SourcePort) + '--->' + DestIP + ':' + IntToStr(DestPort) + #$D#$A;
      m_strInfo := m_strInfo + 'DNSID:' + IntToStr(ID) + #$D#$A;
      m_strInfo := m_strInfo + 'Domain:' + DomainName + #$D#$A;
      m_strInfo := m_strInfo + 'QueryType:' + GetDNSTypeStr(QueryType) + #$D#$A;
      m_strInfo := m_strInfo + 'QueryClass:' + GetDNSClassStr(QueryClass) + #$D#$A;
      m_strInfo := m_strInfo + '===================================================';
      Synchronize(ShowInfo);
      *)
    end;
    strSend := BuilderDNSResponse(szBuffer, len, HOSTIP);//构造DNS应答包
    sendto(m_UDPSocket, strSend[1], Length(strSend), 0, client_addr, flen);
  end;
end;

        程序写的很简单,就是根据查询包,分析出客户端需要查询的域名,在Memo1显示出来,然后构造一个应答包返回。这里出于演示目的,我们简单的全部返回127.0.0.1,对于真正的DNS服务器,这里会使用gethostbyname之类往上一级查询,或直接在数据库和缓存里面查找,根据不同的域名返回对应的IP。我们先运行程序,然后把另外一台机器的DNS服务器指向这个IP:

xwinpcap10

        然后随便Ping 一下www.qq.com:

xwinpcap11

        可以看到,返回的IP地址是127.0.0.1。同时我们的DNS服务器的界面如下:

xwinpcap12

        最后,我们把DNS服务端的代码移植到我们的Demo代码里面,就实现了DNS欺骗。需要注意的地方是,基于IP的数据包都有一个校验和的,所以如果你修改了数据包,一定要重新计算一次校验和,具体过程见CheckSum函数。程序运行效果如下:

xwinpcap13

        DNS欺骗利用的原理是:DNS客户端发送请求包后,只要返回包里面的ID跟发送的一致,它就认为合法。如果有多个返回包,则它使用最前面的,这个是跟ARP刚好相反的(ARP会使用最新的覆盖原来的)。几年前,360云查杀的时候,我做过一个试验,就是在应用层开启RAWSOCKET来捕获数据,发现是请求360云服务器的话,则直接创建一个UDP SOCKET然后往请求地址发送一个回复,360就无法连接真正的云服务器了。实际上,不只是DNS协议如此,对于TCP协议,也是一样的。只要你捕获到它的数据包,就可以实现网页插代码、下载内容替换等等。这里再回头前面的话题:不用ARP欺骗就修改数据。方法之一就是在路由的镜像口接一台电脑,比如说,想欺骗TCP,只要你回复的数据包的SEQ和ACK按照顺序,客户端就会老老实实的被欺骗。因为你的程序工作在路由旁边,比真正的服务器回复的速度是更快的,甚至你可以再伪造回复的同时,顺便发送一个RST包给真正的服务器,让它断开连接。如果你无法接触路由,又不想使用ARP怎么办?那就用方法二或方法三了。

附件下载:
本节代码

]]>
http://www.138soft.com/?feed=rss2&p=420 6
WinPcap权威指南(三):ARP协议 http://www.138soft.com/?p=409 http://www.138soft.com/?p=409#comments Mon, 04 Aug 2014 09:15:23 +0000 http://www.138soft.com/?p=409         ARP协议在局域网内使用的非常广泛,它的数据包类型分为请求包和答复包。Windows系统内部有一个缓冲区,保存了最近的ARP信息,可以在cmd下使用命令arp -a来显示目前的缓存,或者使用命令arp -d来清除该缓存(Win7下需要以管理员权限运行cmd)。

        在局域网内,两台机器之间通信,实际上靠的是网卡的物理地址。比如说,本机的IP是192.168.1.80,现在想往另外一台IP为192.168.1.138的机器发送一个ICMP包,或者发起一个TCP连接,操作系统第一步会先获取目标机器的网卡物理地址,获取的步骤是先在ARP缓存里面查找,如果没有找到,则发送ARP请求包(一般是广播方式,询问这个IP的网卡物理地址是多少,目标机器收到请求包,发现请求的目标IP是自己,则反馈一个ARP回复包)。如果目标IP的网卡物理地址获取失败,则无法通信—因为不管TCP、UDP还是ICMP,这些基于以太网的数据包,都有一个以太网帧头。如果没有目标的网卡物理地址,底层就无法填充目的地结构,也不知道发送给哪个网卡了。

        下面我们结合程序来做个实验。先说说如何调用WinPcap发送数据包。WinPcap的发包函数是PacketSendPacket,这里我们封装一个函数来调用它:

function SendPackets(pBuffer: PAnsiChar; dwSize: Cardinal; bFree: Boolean): Boolean;
var
  pSendPacket: LPPACKET;
begin
  Result := False;

  pSendPacket := PacketAllocatePacket;
  if pSendPacket = nil then Exit;

  //初始化一个_PACKET结构。
  PacketInitPacket(pSendPacket, pBuffer, dwSize);

  //发送一个或多个数据报的副本。
  if (PacketSendPacket(g_pAdapter, pSendPacket, 1) = 0) then
  begin
    PacketFreePacket(pSendPacket);
    Exit;
  end;

  if bFree then FreeMem(pBuffer);

  PacketFreePacket(pSendPacket);

  Result := True;
end;

        我们先把程序里面TAnalysePacketsThread的ProcessUDPPacket(LPUDPPacket(pBuffer));注释掉,只留下ProcessARPPacket函数,用意其实是我们只处理ARP包,暂时不理会其它类型的。然后打开cmd,执行命令arp -d清除缓冲区。然后输入命令”ping 192.168.1.138″,这时候,可以看到我们的程序捕获到两个ARP包:一个是本机发送的ARP请求包,这是一个目标物理地址为FF:FF:FF:FF:FF的广播包,类型是请求包,内容是询问192.168.1.138的物理地址;另外一个就是对方的回复包了。我们再arp -a显示,发现这个IP对应的物理地址已经在缓冲区里面的(不过ICMP包被对方的防火墙拦截了,所以ping没返回)。

winpcap3
winpcap4

然后我们再连接这个IP,这时候,机器不再发送请求包了(因为缓冲区内还有这个记录)。 另外,我们在目标机器用arp -a命令,可以看到该机器也有发送端的物理地址记录了:
winpcap5

        回到我们程序的主界面,细心的朋友可能发现网关物理地址是空的。实际上,我们的电脑如果和外网通信,比如说连接微软的站点,操作系统发送数据包的时候,是如何获取微软主机的网卡物理地址呢?答案是获取不到,也无须获取。操作系统组包的时候,目标物理地址会填写网关的物理地址,然后将数据包发送出去。网关路由器收到数据包后,发现目的IP地址不是自己,就将源物理地址改成自己的,目标物理地址改成下一级的路由的物理地址,然后发送出去。下一级路由则同样重复这个过程,直到数据到底最终目的地。

        我们先来解决这个界面问题—获取网关的物理地址。方法1是从本机的ARP缓存获取(一般地说,只要连接过外网,缓存肯定有对应的记录),类似cmd下的arp -a命令:

function GetMacAddrInCache(DestIP: DWORD; pMAC: PULONG): Boolean;
var
  dwSize: Cardinal;
  dwRet: Cardinal;
  pIpNetTable: PMIB_IPNETTABLE;
  i: Cardinal;
begin
  Result := False;

  //从本机ARP表中来查询得到
  dwSize := 0;
  dwRet := GetIpNetTable(nil, dwSize, False);
  if (dwRet <> ERROR_INSUFFICIENT_BUFFER) then Exit;

  GetMem(pIpNetTable, dwSize);
  if pIpNetTable = nil then Exit;
  dwRet := GetIpNetTable(pIpNetTable, dwSize, False);
  if (dwRet <> ERROR_SUCCESS) then
  begin
    FreeMem(pIpNetTable);
    Exit;
  end;

  for i := 0 to pIpNetTable^.dwNumEntries - 1 do
  begin
    //pIpNetTable^.table[i].dwIndex;      //适配器索引
    //pIpNetTable^.table[i].dwPhysAddrLen;//物理接口长度
    //pIpNetTable^.table[i].bPhysAddr;    //物理地址
    //pIpNetTable^.table[i].dwAddr;       //IP地址
    //pIpNetTable^.table[i].dwType;       //ARP条目类型
    //MIB_IPNET_TYPE_STATIC;
    if (DestIP = pIpNetTable^.table[i].dwAddr) then //列表中找到该IP信息
    begin
      CopyMemory(pMAC, @pIpNetTable^.table[i].bPhysAddr, 6);
      FreeMem(pIpNetTable);
      Result := True;
      Exit;
    end;
  end;

  if pIpNetTable <> nil then
  begin
    FreeMem(pIpNetTable);
    //pIpNetTable := nil;
  end;
end;

        方法2就是发送请求包,调用了iphlpapi.dll里面的SendARP函数:

function GetMACAddrByARP(DestIP: DWORD; pMAC: PULONG): Boolean;
var
  dwSize: Cardinal;
  dwRet: Cardinal;
begin
  Result := False;
  dwSize := 6;
  dwRet := SendARP(DestIP, 0, pMAC, dwSize);
  if (dwRet = NO_ERROR) and (dwSize <> 0) then
  begin
    Result := True;
    Exit;
  end;
end;

        界面的处理过程:

procedure TFrmMain.ComboBox_GatewayIPsChange(Sender: TObject);
var
  IP: Cardinal;
  Mac: TMacAddr;
begin
  Edit_GatewayMac.Clear;
  if ComboBox_GatewayIPs.Text = '' then Exit;
  IP := inet_addr(PAnsiChar(ComboBox_GatewayIPs.Text));
  if not GetMacAddrInCache(IP, @Mac) then
    if not GetMACAddrByARP(IP, @Mac) then Exit;

  Edit_GatewayMac.Text := Mac2Str(Mac);
end;

        我们前面已经介绍了WinPcap的发包函数,那么我们还可以使用第三种方法,自己构造arp请求包,函数如下:

function SendARPPacket(const DestMac, SourceMac: TMacAddr; const strDestIP, strSourceIP: AnsiString; bReQuest: Boolean): Boolean;
var
  ARPPacket: TARPPacket;
begin
  //1、填充TEthernetHeader
  CopyMemory(@ARPPacket.EthernetHeader.DestMac, @DestMac, sizeof(TMacAddr));
  CopyMemory(@ARPPacket.EthernetHeader.SourceMac, @SourceMac, sizeof(TMacAddr));
  ARPPacket.EthernetHeader.EthernetType := htons(ETHERTYPE_ARP);

  //2、填充TARPHeader
  ARPPacket.ARPHeader.HardwareType := htons(ARP_HARDWARE);
  ARPPacket.ARPHeader.ProtocolType := htons(ETHERTYPE_IP);
  ARPPacket.ARPHeader.HrdAddrlen := 6;
  ARPPacket.ARPHeader.ProAddrLen := 4;
  if bReQuest then ARPPacket.ARPHeader.Operation := htons(ARP_REQUEST)
  else ARPPacket.ARPHeader.Operation := htons(ARP_REPLY);

  CopyMemory(@ARPPacket.ARPHeader.SenderMAC, @SourceMac, sizeof(TMacAddr));
  ARPPacket.ARPHeader.SenderIP := inet_addr(PAnsiChar(strSourceIP));
  CopyMemory(@ARPPacket.ARPHeader.TargetMAC, @DestMac, sizeof(TMacAddr));
  ARPPacket.ARPHeader.TargetIP := inet_addr(PAnsiChar(strDestIP));

  Result := SendPackets(@ARPPacket, sizeof(ARPPacket), False);
end;

        例如,我们需要获取192.168.1.138的物理地址,则这样调用:

procedure TFrmMain.Button1Click(Sender: TObject);
var
  DestMac, SourceMac: TMacAddr;
  strDestIP, strSourceIP: AnsiString;
begin
  FillChar(DestMac, sizeof(DestMac), $FF); //广播包
  SourceMac := Str2Mac(Trim(Edit_OwnMac.Text));
  strSourceIP := Trim(ComboBox_OwnIPs.Text);
  strDestIP := '192.168.1.138';
  SendARPPacket(DestMac, SourceMac, strDestIP, strSourceIP, True);
end;

        然后从程序主界面,或本机的arp -a命令,或目标机器的arp -a命令都可以验证这个过程。现在我们来看看电脑里面这个ARP缓存表是什么时候更新的。实际上,除了上面说的主动请求获取外,只要机器收到ARP包,不管是请求包还是反馈包,它都会添加或更新自己的缓存。修改一下上面的代码,我们广播一条ARP请求包,但是将源IP地址改成一个不存在的IP“192.168.1.111”:

procedure TFrmMain.Button1Click(Sender: TObject);
var
  DestMac, SourceMac: TMacAddr;
  strDestIP, strSourceIP: AnsiString;
begin
  FillChar(DestMac, sizeof(DestMac), $FF); //广播包
  SourceMac := Str2Mac(Trim(Edit_OwnMac.Text));
  strSourceIP := '192.168.1.111';//Trim(ComboBox_OwnIPs.Text);
  strDestIP := '192.168.1.138';
  SendARPPacket(DestMac, SourceMac, strDestIP, strSourceIP, True);
end;

        在目标机器输入arp -a,发现的确出现了一条对应的记录。也就是说,如果现在在目标机器连接“192.168.1.111”,那么实际上会连接我们的机器。

winpcap6

        如果我们把源IP改成跟目标IP一样呢?

procedure TFrmMain.Button1Click(Sender: TObject);
var
  DestMac, SourceMac: TMacAddr;
  strDestIP, strSourceIP: AnsiString;
begin
  FillChar(DestMac, sizeof(DestMac), $FF); //广播包
  SourceMac := Str2Mac(Trim(Edit_OwnMac.Text));
  strSourceIP := '192.168.1.138';//Trim(ComboBox_OwnIPs.Text);
  strDestIP := '192.168.1.138';
  SendARPPacket(DestMac, SourceMac, strDestIP, strSourceIP, True);
end;

        目标机器会出现如下提示框:

winpcap7
        
如果开一个线程重复这个过程,那台机器就无法上网了。

        最后我们讲述一下ARP欺骗和路由器。因为局域网内通信是依靠物理地址,而只要机器收到请求或反馈包,都会更新自己的缓存,所以可以利用这个特性,进行ARP欺骗。具体做法就是:欺骗目标电脑,告诉它网关的IP对应的物理地址是自己;欺骗网关,告诉它目标IP的物理地址是自己。也就是开一个线程不断的发送ARP请求或反馈包给这两个苦主,然后目标电脑发往外网的数据,都会发送到你的电脑,你的电脑收到数据包后,检查如果目标IP不是自己,说明应该转发,你就把该数据包的源物理地址改成自己的,目标物理地址改成网关的,然后发送出去(注意:源IP地址不要改变);外网返回数据后,将数据发给你的电脑,你的电脑收到数据包后,检查如果目标IP(这时候,明白为什么前面说源IP地址不要改变了吧?)不是自己,说明应该转发,你就把该数据包的源物理地址改成自己的,目标物理地址改成目标电脑的,然后发送出去。

        实际上,这个就是路由器的实现,只不过它是把这个代码写进了芯片里面。另外它不用ARP欺骗,而是你自己将网络连接的网关IP设置成它的。你也可以按照上面的流程,去掉ARP欺骗的过程,仅保留数据转发,然后将局域网另外一台机器的网关设置为你的机器,那么它就需要通过你来上网了。因为现在ARP欺骗不像2003年,到处都是代码,这里就不给出具体的实现,留作大家的课后作业吧。

        下一节我们将讲述UDP协议部分,进入初步的数据修改阶段。我们先实现一个应用层的DNS客户端和服务端,再来在驱动层玩玩。

附件下载:
本节代码

]]>
http://www.138soft.com/?feed=rss2&p=409 2
WinPcap权威指南(二) http://www.138soft.com/?p=403 http://www.138soft.com/?p=403#respond Sun, 03 Aug 2014 11:01:47 +0000 http://www.138soft.com/?p=403         上一节我们简单介绍了WinPcap的一些基础知识,同时也枚举到所有网卡的设备名称,现在我们就可以操作网卡了。WinPcap有一个结构叫TADAPTER,你可以把它想象为一个句柄,我们平时操作文件,一般是先打开/创建一个文件,如果成功,则返回一个句柄,然后读文件就可以使用ReadFile,写文件可以使用WriteFile,操作完毕后,CloseHandle关闭这个句柄,WinPcap下的操作也是类似的:

1、打开一个网卡,返回句柄:function PacketOpenAdapter(AdapterName: PAnsiChar): LPADAPTER;

        其中AdapterName就是上一节获取的网卡设备名称,打开成功则返回该网卡的句柄,失败返回nil。一般地说,打开句柄后,还可以做一些初始化工作。例如:调用PacketSetHwFilter将网卡设置为混杂模式(只有网卡处于混杂模式,才可以获取不是发送给本机的数据);调用PacketSetBuff设置缓冲区大小;调用PacketSetReadTimeout设置接收超时。具体请参考演示代码。

2、从网卡获取数据:function PacketReceivePacket(AdapterObject: LPADAPTER; pPacket: LPPACKET; Sync: Byte): Byte;

        AdapterObject: 就是网卡的句柄,pPacket则对应一个TPACKET结构的指针。一般是先通过函数PacketAllocatePacket来分配一个pPacket,再调用PacketInitPacket将自己的接收缓冲区和它关联,使用完毕后调用PacketFreePacket释放掉。对应的WinPcap内部实现如下:


function PacketAllocatePacket(): LPPACKET;
var
  pPacket: LPPACKET;
begin
  pPacket := GlobalAllocPtr(GMEM_MOVEABLE or GMEM_ZEROINIT, sizeof(TPACKET));
  if (pPacket = nil) then
  begin
    //TRACE_PRINT("PacketAllocatePacket: GlobalAlloc Failed");
  end;

  Result := pPacket;
end;

procedure PacketInitPacket(var pPacket: LPPACKET; Buffer: Pointer; Length: UINT);
begin
  //TRACE_ENTER("PacketInitPacket");

  pPacket^.Buffer := Buffer;//关联用户缓冲区
  pPacket^.Length := Length;//用户缓冲区大小
  pPacket^.ulBytesReceived := 0;
  pPacket^.bIoComplete := 0;

  //TRACE_EXIT("PacketInitPacket");
end;

procedure PacketFreePacket(var pPacket: LPPACKET);
begin
  //TRACE_ENTER("PacketFreePacket");
  GlobalFreePtr(pPacket);
  //TRACE_EXIT("PacketFreePacket");
end;

3、通过网卡发送数据:function PacketSendPacket(AdapterObject: LPADAPTER; pPacket: LPPACKET; Sync: Byte): Byte;

4、关闭网卡句柄:procedure PacketCloseAdapter(lpAdapter: LPADAPTER);

        我们的程序使用了两个线程来处理数据,其中TRecvPackThread用于从网卡获取数据,然后添加到全局的g_List;而TAnalysePacketsThread则从g_List里面取出数据来处理。为了界面更友好,再添加一个TTimer来获取WinPcap的内部状态。

        现在我们获取了数据,但这个数据就是一个指针,里面是什么东西呢?其实跟我们的《张曼玉与指针》里面说的,数据就是一切,你说它是什么,它就是什么。实际上,我们捕获的都是以太网帧数据,帧头结构如下:

type
  _ETHERNET_HDR = packed record
    DestMac: array[0..5] of Byte; //目的MAC地址
    SourceMac: array[0..5] of Byte; //源MAC地址
    EthernetType: Word; //帧类型
  end;
  TEthernetHeader = _ETHERNET_HDR;
  LPEthernetHeader = ^_ETHERNET_HDR;

        实际上,不管上层是什么协议,到了底层,都会封装为以太帧,然后发送出去。例如,TCP协议,是基于IP协议的,那么到了底层,实质上会封装为:以太帧头+IP头+TCP头+实际数据(如果存在)。所以我们在这里根据EthernetType判断是什么类型的帧,再作进一步解释:

    pEthernetHeader := LPEthernetHeader(pBuffer);
    case ntohs(pEthernetHeader^.EthernetType) of //判断以太帧类型
      ETHERTYPE_ARP: ProcessARPPacket(LPARPPacket(pBuffer));
      ETHERTYPE_IP: //IP帧
        begin
          pIPHeader := LPIPHeader(pBuffer + sizeof(TEthernetHeader)); //判断是TCP、UDP还是ICMP等
          case pIPHeader^.Protocol of
            IPPROTO_TCP: ProcessTCPPacket(LPTCPPacket(pBuffer));
            IPPROTO_UDP: ProcessUDPPacket(LPUDPPacket(pBuffer));
          end;
        end;
    end;

winpcap2

附件下载:
本节代码

        本节仅简单的解释了ARP和UDP协议,下一节再结合发包深入网络的连接过程。例如:ARP的欺骗、路由器的实质、数据的修改等。

]]>
http://www.138soft.com/?feed=rss2&p=403 0
WinPcap权威指南(一) http://www.138soft.com/?p=391 http://www.138soft.com/?p=391#respond Sat, 02 Aug 2014 16:15:47 +0000 http://www.138soft.com/?p=391         WinPcap是一个开源的网络抓包模块,顾名思义,它只能工作在Windows下,但本文介绍的知识并不局限于任何操作系统和开发语言,因为网络协议本身是没有这些区别的。阅读本指南之前,请先下载WinPcap安装到自己的电脑上,目前WinPcap的最新版本是4.1.3,支持基于NT核心的所有操作系统(从NT4一直到Win8),读者可以从官方网站http://www.winpcap.org/install/default.htm下载。

        网络其实是分层设计的,一个应用层的网络程序对外通信的时候,大概的流程如下:应用程序(调用WinSock)<--->SPI层<--->TDI层<--->NDIS层<--->MiniPort层<-->物理网卡,实际上,应用程序可以直接调用上面的某一层,例如:直接调用TDI驱动收发网络包;又例如,一般软件防火墙工作于NDIS层,如果你直接使用MiniPort HOOK进行收发数据,那么防火墙是完全不知情的,也不会有任何提示。应用层抓包的话,方法有很多:例如本机的可以使用API HOOK,截取WinSock的调用;或者直接安装一个SPI的HOOK;又或者直接使用RawSocket接口。WinPcap的驱动实际上位于NDIS层,属于NDIS过滤驱动,也就是说,它只是抓包,但不像软件防火墙的passthru模型还可以拦截。另外,它还可以发送数据包。注意:笔者曾经将它翻译为Delphi,发现在以太网环境下(例如局域网)它是通过自己的驱动发布,而对于拨号上网的PPPOE连接,它调用的是微软的NetMon接口,也就是说,如果你的电脑没有安装NetMon,那么在拨号上网的环境下WinpCap可能是无法预期工作的。

        调用WinPcap有两种方式,一种是通过packet.dll调用它的驱动npf.sys,另外一种是通过wpcap.dll再间接调用packet.dll来调用它的驱动npf.sys,可能会有人问,既然可以直接调用packet.dll,为什么还要有调用wpcap.dll这种方式?原因很简单:在Unix下(或者Linux)下有一个抓包接口叫Pcap—这也是WinPcap的名称来由,wpcap.dll对外提供了相同的接口函数,便于程序员在不同的平台下移植。本文介绍的是packet.dll调用方式,对wpcap.dll接口感兴趣的朋友可以参考韩国程序员写的一个控件包Snoop2,我将它的下载放在本文的末尾(说句题外话,WinPcap的安装程序,实际上是释放sys和dll到系统,然后安装sys为服务,具体可以参考这个Snoop2(Jingtao修改版),里面直接集成了sys和dll,所以无需先安装WinPcap了,但是由于当时这个版本时间很早,所以还支持Win98系统,读者可以将里面的sys和dll文件替换成现在的最新版本再编译)。

        要调用WinPcap,第一步当然是打开需要操作的网卡,因为有可能一台电脑上有好几张网卡。WinPcap提供了一个函数叫PacketGetAdapterNames,用于获取系统已经安装的所有网卡,函数声明如下:

  TPacketGetAdapterNames = function(pStr: PAnsiChar; BufferSize: PULONG): Byte; cdecl;

        其中pStr是一个用于接收网卡名称的缓冲区,BufferSize是该缓冲区的大小,如果函数成功,则返回非0值。需要注意的是,网络的名称有多种形式,一种是设备名称,一种是友好名称,这个函数返回的格式是:设备名称+两个0+网卡名称,在WinPcap里面打开某块网卡,需要提供的是设备名称,packet.dll的源代码里面大概的实现如下:

function PacketGetAdaptersNPF(): Byte;
var
  LinkageKey, AdapKey, OneAdapKey: HKEY;
  RegKeySize: DWORD;
  Status: Longint;
  i: Integer;
  dim: DWORD;
  RegType: DWORD;
  TName: array[0..255] of AnsiChar;
  TAName: array[0..255] of AnsiChar;
  AdapName: array[0..255] of AnsiChar;
  TcpBindingsMultiString: PAnsiChar;
  FireWireFlag: UINT;
  //
  //	Old registry based WinPcap names
  //
  //	CHAR		npfCompleteDriverPrefix[MAX_WINPCAP_KEY_CHARS];
  //	UINT		RegQueryLen;

  npfCompleteDriverPrefix: array[0..MAX_WINPCAP_KEY_CHARS - 1] of AnsiChar; // = NPF_DRIVER_COMPLETE_DEVICE_PREFIX;
  DeviceGuidName: array[0..255] of AnsiChar;
label tcpip_linkage;
begin
  RegKeySize := 0;
  FillChar(npfCompleteDriverPrefix, sizeof(npfCompleteDriverPrefix), #0);
  StrCopy(npfCompleteDriverPrefix, NPF_DRIVER_COMPLETE_DEVICE_PREFIX);
  //TRACE_ENTER("PacketGetAdaptersNPF");

 //
 //	Old registry based WinPcap names
 //
 //	Get device prefixes from the registry

  Status := RegOpenKeyEx(HKEY_LOCAL_MACHINE,
    'SYSTEMCurrentControlSetControlClass{4D36E972-E325-11CE-BFC1-08002BE10318}',
    0,
    KEY_READ,
    AdapKey);

  if (Status <> ERROR_SUCCESS) then
  begin
    //TRACE_PRINT("PacketGetAdaptersNPF: RegOpenKeyEx ( Class\{networkclassguid} ) Failed");
    goto tcpip_linkage;
  end;

  i := 0;

  //TRACE_PRINT("PacketGetAdaptersNPF: RegOpenKeyEx ( Class\{networkclassguid} ) was successful");
  //TRACE_PRINT("PacketGetAdaptersNPF: Cycling through the adapters in the registry:");

  //
  // Cycle through the entries inside the {4D36E972-E325-11CE-BFC1-08002BE10318} key
  // To get the names of the adapters
  //
  //while((Result = RegEnumKey(AdapKey, i, AdapName, sizeof(AdapName)/2)) == ERROR_SUCCESS)
  while ((RegEnumKey(AdapKey, i, AdapName, sizeof(AdapName) div 2)) = ERROR_SUCCESS) do
  begin
    Inc(i);
    FireWireFlag := 0;
    //
    // Get the adapter name from the registry key
    //
    Status := RegOpenKeyEx(AdapKey, AdapName, 0, KEY_READ, OneAdapKey);
    if (Status <> ERROR_SUCCESS) then
    begin
      //TRACE_PRINT1("%d) RegOpenKey( OneAdapKey ) Failed, skipping the adapter.",i);
      continue;
    end;

    //
    //
    // Check if this is a FireWire adapter, looking for "1394" in its ComponentId string.
    // We prevent listing FireWire adapters because winpcap can open them, but their interface
    // with the OS is broken and they can cause blue screens.
    //
    dim := sizeof(TName);
    Status := RegQueryValueEx(OneAdapKey,
      'ComponentId',
      nil,
      nil,
      PBYTE(@TName[0]),
      @dim);

    if (Status = ERROR_SUCCESS) then
    begin
      if (IsFireWire(TName) <> 0) then
      begin
        FireWireFlag := INFO_FLAG_DONT_EXPORT;
      end;
    end;

    Status := RegOpenKeyEx(OneAdapKey, 'Linkage', 0, KEY_READ, LinkageKey);
    if (Status <> ERROR_SUCCESS) then
    begin
      RegCloseKey(OneAdapKey);
      //TRACE_PRINT1("%d) RegOpenKeyEx ( LinkageKey ) Failed, skipping the adapter",i);
      continue;
    end;

    dim := sizeof(DeviceGuidName);
    Status := RegQueryValueExA(LinkageKey,
      'Export',
      nil,
      nil,
      PBYTE(@DeviceGuidName[0]),
      @dim);

    if (Status <> ERROR_SUCCESS) then
    begin
      RegCloseKey(OneAdapKey);
      RegCloseKey(LinkageKey);
      //TRACE_PRINT1("%d) Name = SKIPPED (error reading the key)", i);
      continue;
    end;

    if (strlen(DeviceGuidName) >= strlen('Device')) then
    begin
      // Put the DeviceNPF_ string at the beginning of the name
      StrPCopy(TAName, Format('%s%s', [npfCompleteDriverPrefix,
        DeviceGuidName + strlen('Device')]));
    end
    else
      continue;

    //terminate the string, just in case
    TAName[sizeof(TAName) - 1] := #0;
    //TRACE_PRINT2("%d) Successfully retrieved info for adapter %s, trying to add it to the global list...", i, TAName);
    // If the adapter is valid, add it to the list.


    PacketAddAdapterNPF(TAName, FireWireFlag);

    RegCloseKey(OneAdapKey);
    RegCloseKey(LinkageKey);

  end; // while enum reg keys

  RegCloseKey(AdapKey);

  tcpip_linkage:
  //
  // no adapters were found under {4D36E972-E325-11CE-BFC1-08002BE10318}. This means with great probability
  // that we are under Windows NT 4, so we try to look under the tcpip bindings.
  //

  //TRACE_PRINT("Adapters not found under SYSTEM\CurrentControlSet\Control\Class. Using the TCP/IP bindings.");

  Status := RegOpenKeyEx(HKEY_LOCAL_MACHINE,
    'SYSTEMCurrentControlSetServicesTcpipLinkage',
    0,
    KEY_READ,
    LinkageKey);

  if (Status = ERROR_SUCCESS) then
  begin
    // Retrieve the length of th binde key
    // This key contains the name of the devices as devicefoo
    //in ASCII, separated by a single '\0'. The list is terminated
    //by another '\0'
    Status := RegQueryValueExA(LinkageKey,
      'bind',
      nil,
      @RegType,
      nil,
      @RegKeySize);

    // Allocate the buffer
    TcpBindingsMultiString := GlobalAllocPtr(GMEM_MOVEABLE or GMEM_ZEROINIT, RegKeySize + 2);

    if (TcpBindingsMultiString = nil) then
    begin
      //TRACE_PRINT("GlobalAlloc failed allocating memory for the registry key, returning.");
      //TRACE_EXIT("PacketGetAdaptersNPF");
      Result := 0;
      Exit;
    end;

    // Query the key again to get its content
    Status := RegQueryValueExA(LinkageKey,
      'bind',
      nil,
      @RegType,
      PBYTE(@TcpBindingsMultiString[0]),
      @RegKeySize);

    RegCloseKey(LinkageKey);

    // Scan the buffer with the device names
    i := 0;
    while True do
    begin
      if (TcpBindingsMultiString[i] = #0) then
        break;

      StrPCopy(TAName, Format('%s%s', [npfCompleteDriverPrefix, TcpBindingsMultiString + i + strlen('Device')]));
      //
      // TODO GV: this cast to avoid a compilation warning is
      //			actually stupid. We shouls check not to go over the buffer boundary!
      //
      Inc(i, strlen(PAnsiChar(TcpBindingsMultiString + i)) + 1);

      // If the adapter is valid, add it to the list.
      PacketAddAdapterNPF(TAName, 0);
    end;

    GlobalFreePtr(TcpBindingsMultiString);
  end

  else
  begin

  end;
  Result := 1;
end;

        另外,IpHlp函数也提供了获取网卡信息的接口,而且网卡名称跟WinPcap的一样。由于IpHlp可以获取网卡的更多信息,例如:IP地址、物理地址、网关IP等信息,所以我们可以结合它来实现更加友好的选择界面。下面是本讲实现的最终效果图:

winpcap1

附件下载:
Snoop2(Jingtao修改版)
本讲演示代码

        后面的系列将讲解ARP欺骗、如何实现一个路由器、如何实现一个TCP协议等内容,敬请关注。

]]>
http://www.138soft.com/?feed=rss2&p=391 0
Windows完成端口与猪肉佬 http://www.138soft.com/?p=338 http://www.138soft.com/?p=338#comments Fri, 27 Dec 2013 14:50:42 +0000 http://www.138soft.com/?p=338         首先应该说明的是,我也是第一次使用完成端口。虽然以前偶尔在网上看到完成端口的文章和代码,但真正自己动手写还是第一次,不过我这个人有个特点就是大胆,例如没有写那个界面编程系列前,其实我甚至不知道原来一个矩形的宽度Width原来就是Rect.Right-Rect.left。但现在网络信息那么发达,学习一个新东西,看看MSDN,再Google一下,还是可以冒充老手的。另外,本文仅仅讲完成端口在网络方面的应用。
        一、为什么要使用完成端口:
        在Windows下做过网络开发的朋友都知道,网络模型大概有这几种:
        1、消息模型。大概流程是使用WSAAsyncSelect函数将Socket句柄跟窗口句柄关联,有事件发生的时候将在窗口消息过程触发对应的消息(例如:新的连接--FD_ACCEPT、有数据到达--FD_READ)。如果连接数和收发数据大,那么这种模型很快就无法支撑,这种模型一般用于长连接而且是小包数据的环境,例如,基于反向连接的远程管理程序,被控制端连接上来后,一般仅等待控制端发送指令,那么可以使用这个。Delphi以前的TServerSocket、TClientSocket控件就是基于这种模型的。
        2、阻塞模式。这种模型一般是一个连接对应一个线程,例如Delphi的Indy控件库就是基于阻塞的。阻塞的好处是处理数据的业务代码逻辑可以很独立,缺点就是需要线程了,即使使用线程池+Select,效果改变也不大。因为每个进程可创建的线程数是有限的,例如,读者可以自己写一个测试程序,点一下按钮就创建多个线程,线程里面什么都不干,只是简单的Sleep(1),在32位的XP下,在作者的电脑里面,程序创建到2010个线程就开始出错了:

IOCP1

        3、事件模型。因为事件等待的限制(一个线程只能同时等待64个),所以实际上仍然无法满足海量连接。另外线程的切换其实也是需要开销的。

        二、什么是完成端口:
        简单点说,完成端口就是个黑盒子,它有一个进端口,一个出端口。你把要求(例如需要接收数据)从进端口送进去,它内部完成后,出列从出端口给你最终的结果。你甚至可以这样想象:你是个厨师,做外卖饮食的。你需要猪肉(网络数据)作为原料炒菜,传统的模型是需要你自己切割猪肉(接收数据)。菜炒出来后,需要按照客人的地址外送到他手上(发送数据)。使用完成端口的话,你拿一个碗(内存),上面贴好标签(WSARecv或WSASend)表明是需要将猪肉放到这个碗里(接收),还是把这个碗里的内容送到客人手上(发送),然后从进端口送进这个小屋子(盒子)里面。盒子完成后,从出端口把碗还给你,标签上面还会标明结果。如下图所示:
IOCP2
        三、完成端口的使用流程:主要是创建完成端口,然后往入口送请求,从出口取结果。
        1、创建一个完成端口,就是创建上图的完成盒子和进出端口:m_CompletionPort := CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
        2、将Socket和上面的完成端口关联起来。你可以看作是去那个完成盒子登记一下。
        3、创建几个线程(微软推荐一般是CPU的个数*2),作用是不断的从上面的出端口取结果:GetQueuedCompletionStatus(m_CompletionPort,xxx,xxx,xxx,INFINITE)。INFINITE表示没有等待无限时间,直到有一个请求完成了。
        4、需要收数据的时候,分配一块内存(也可以不分配,后面再说)然后通过WSARecv函数送到那个入端口,函数的返回值会有三种可能:
        (1)返回值不等于SOCKET_ERROR,说明投递成功。
        (2)返回值等于SOCKET_ERROR,但WSAGetLastError等于ERROR_IO_PENDING,说明投递成功,但处于排队状态,因为入口那里人很多。
        (3)返回值等于SOCKET_ERROR,但WSAGetLastError不等于ERROR_IO_PENDING,说明投递失败了,例如可能网络出现故障。
        下面是一个典型的WSARecv伪代码:

    dwBytesRecv := 0;
    dwFlags := 0;

    GetMem(pBuffer,1024*4);//分配一块4KB的内存,一般是从内存池取。
    pOverlappedData^.DataBuf.buf:=pBuffer;//将该内存和TWsaBuf关联起来。
    pOverlappedData^.DataBuf.len:=1024*4;
    ret := WSARecv(ClientSocket.m_Socket, @pOverlappedData^.DataBuf, 1, dwBytesSend, dwFlags, LPWSAOVERLAPPED(pOverlappedData), nil);

    if ret = SOCKET_ERROR then
    begin
      lLastError := WSAGetLastError();
      if lLastError <> ERROR_IO_PENDING then
      begin
       //发生错误,应该释放内存或回收到池......
       Exit; 
      end;
    end;

        一般地说,连接上来后,都需要投递一个WSARecv便于接收数据。
        5、需要发送数据的时候,跟上面接收数据是一样的。另外,因为完成端口是基于异步的,所以这些操作都会马上返回。当然,即使返回成功,也不表示数据发送出去对方已经成功接收,这个跟其它模型是一样的。注意:只要投递成功了,那么最后一定会从出口那里出列(就是GetQueuedCompletionStatus返回)。

        四、使用完成端口需要注意的地方:
        折腾了半天,终于到了正题了。使用完成端口,你将要抛弃以前那些小打小闹的连接观念,完成端口的实质,是利用内存换线程,所以也有人说,玩完成端口,其实就是玩内存。一般地说,如果完成端口程序出了问题,99.999999999999999999999999999999999999999999999999999999999999999999999%是因为内存使用不当导致的,而且一旦出问题,都是莫名其妙的错误。比如说,空指针、空变量,诸如此类,其实错误根本不在这里。记住:完成端口是做服务器用的,不要再站在只有几十个连接的角度考虑问题。
        1、投递接收的内存缓冲区大小。上面已经说了,连接上来后,第一件事情就是投递一个WSARecv,这个函数会绑定一块内存,如果接收成功,或者这个Socket发生网络错误(也可以是用户自己关闭了Socket,例如需要退出程序),这个请求(和所绑定的内存)才会从出端口返回,如果接收到数据,数据将保存在这块内存里面。问题在于,投递成功后,如果没有出列返回前(例如,对方没有数据发送过来,这个Socket也没有发生错误),这块内存你是无法使用的,系统已经将它锁定,也不能释放。另外,这块内存的大小,一般是页面大小(系统页面内存一般是4KB,可以通过GetSystemInfo函数得到)的倍数,例如页面大小是4KB,即使你发送1个字节,系统仍然锁定4KB。假设一下,现在有4万个连接上来,你投递了4万个WSARecv,系统锁定了这些内存。如果这4万个连接都不发送数据,那么当有新的连接上来,或者进行其它需要有内存的操作(例如发送数据),就可能会发送WSANoBuff错误了。这种情况下,新的连接无法成功,数据也无法发送,意思是你的程序基本OVER了。
        使用前面猪肉的比喻,你的碗的数量(内存)是一定的,你的碗送进小屋里面后,没有回来前,你的碗是越来越少,甚至不够用的。
        解决的办法一般有两个:
        (1)0字节投递。意思是投递的时候,pOverlappedData^.DataBuf.len设置为0,这样一来就不会有任何内存被锁定。就好比你需要猪肉,但不给碗,只给标签。具体做法还可以分为两种,一种是投递的时候,len设置为0,最后一个参数设置为MSG_PARTIAL,当请求出列返回,说明真的有数据了,再循环调用Recv直到返回WSAEWOULDBLOCK。另外一种len设置为0,当请求出列返回后,再投递一个真正带接收内存的WSARecv,因为本身有数据了,这个请求也会很快返回。
        但这种做法的缺点在于牺牲了吞吐量。想象一下,春运你去买票,第一次,你去排队,经过前面那3万人(连接),终于轮到你了,但是你到窗口只是问有没有到广州的火车票,答复说有,你返回,拿钱包,然后再排队一次。
        (2)程序启动后,根据预先设定(比如说,你的服务器只允许1万个连接),计算出程序极限需要多少内存,然后一次性分配,放到内存池。需要内存的时候,就从这个池里面取,用完后,丢回这个池里面。如果连接数超过1万,当有新的连接上来,那么就拒绝对方连接,直接closesocket。这种做法还可以防止内存碎片,因为一开始就分配了一整块。缺点就见仁见智了。
        2、内存的释放时机。
        因为投递后(不管是发送还是接收),内存都会锁定,所以如果它没有出列,就千万不要释放它。有些人写代码的逻辑是这样的:
        (1)每分配一块内存,加到List。释放的时候就从List删除。
        (2)程序退出的时候,他就循环这个List,逐一释放。
        这种逻辑实际上是不对的。运气好的时候,释放过程中程序就出错了,你还知道自己怎么死的;运气不好的时候,特别是连接很多,你释放完,但不要关闭程序,有时候过了15分钟程序才报错。
        实际上,完成端口顺利退出(而且没有内存泄漏)是第一步。如果你写完成端口,那么我建议你第一步就是写退出,以随时随地退出,快速退出而没有内存泄露为标准。真正的做法应该是先关闭所有socket,从而导致这个socket投递的所有内存从盒子里面出列,从而解锁,然后再释放。一般稳妥的做法是对每个连接都使用一个计数器,每投递一个请求就加1,每返回一个就减1。只有计数器为0,说明没有内存被锁定了,才能释放这个SOCKET对象。
        3、使用心跳。有些文章说,可以使用WinSock2的keepLive选项,但笔者更加建议使用自定义心跳,只要一个连接在一定时间内没有数据收发,就要断开它,而keepLive对于占着茅坑不拉屎的恶意连接也是容忍不断开的。一般做法是使用时间轮:

IOCP3

        简单点说,新加入的对象(包括更新)总是位于指针的前一个槽,指针比如说每秒钟移动一格,指向哪个槽就把该槽所有的SOCKET给关闭。这个是O(1)操作。如果使用LIST,那么复杂度将是O(n)。
        4、将所有阻塞的同步操作改为异步。例如读写文件,如果你在IO线程阻塞写文件,那么表现是CPU占用不高,但操作系统非常卡。
        5、多投递。到底一个SOCKET同时投递多少个WSARecv最合适,这又是个见仁见智的问题。我觉得这也是外面的完成端口库无法通用的原因,因为它们要么全部是0投递,要么全部是多投递,要么永远只有一个投递。其实我觉得投递多少这个应该和业务逻辑结合。对于我的程序来说,做法是将一个Socket分成了几类,对外提供了一个ChangeType函数。例如,对于长连接,但偶尔有数据的Socket,我是0投递或永远仅有一个WSARecv投递。对于文件传输这种吞吐量很大的连接,我马上ChangeType,令其连续投递多个WSARecv。注意:对一个Socket连续投递多个WSARecv是会可能造成乱序的。比如说,你投递了WSARecv1,WSARecv2,WSARecv3,每个给它4K内存,然后对方发送了16K内容过来,那么,WSARecv1,WSARecv2,WSARecv3里面的内容是顺序的,这个是完成端口决定的,绝对不会乱。但是出列的时候(比如说你前面开了4个线程GetQueuedCompletionStatus,因为线程调度有先后),有可能是WSARecv2会先返回,所以要自己做处理。例如,WSARecv的时候,加个序列号。也有人只用一个IO线程,从而不使用序列号。
        五、其它一些需要说明的:
        没有什么好说的了。这东西其实就是内存换线程而已。没什么神秘的。几个线程和一点点CPU就可以轻松的将网络带宽用到极限,例如下面的程序仅用了一个网络线程:

网络吞吐量:

IOCP4

CPU占用:

IOCP5

        实际上,完成端口还可以用于文件读写之类。那个完成盒子里面有着一个高速的队列。合理的利用完成端口,可以减轻应用程序很多工作,完成端口,完成端口,它全部帮你完成,让你的程序无事可做。

]]>
http://www.138soft.com/?feed=rss2&p=338 6
TCP程序设计系列(一) http://www.138soft.com/?p=153 http://www.138soft.com/?p=153#comments Sun, 11 Aug 2013 17:31:35 +0000 http://www.138soft.com/?p=153 很多Delphi的使用者依赖控件,比如说,Indy系列。也有很多使用Winsock API的,但是或多或少都有着一些小问题,这是因为对Winsock不了解造成的。

笔者几年前曾经设计过一个完整的TCP协议栈,其中协议栈是纯粹用Delphi写的,驱动部分则是用C挂接到网卡驱动前,鉴于最近“Carberp 木马源码”已经公开,所以打算提提这方面的东西。

我们的讲座将以WinpCap作为底层收发数据部分,应用层则使用Delphi。例如,我们熟悉的两个WinSock函数:

function htons(hostshort: Word): Word;
begin
Result := ((hostshort and $FF00) shr 8) or ((hostshort and $00FF) shl 8);
end;

function htonl(hostlong: Cardinal): Cardinal;
begin
Result := ((hostlong and $FF000000) shr 24) or
((hostlong and $00FF0000) shr 8) or
((hostlong and $0000FF00) shl 8) or
((hostlong and $000000FF) shl 24);
end;

其实,TCP协议因为是基于非可靠传输的IP层协议之上的,所以可靠传输是其核心。而可靠传输的核心则是阻塞控制,简单一点说,阻塞控制的意思就是什么时候应该发送多大的数据包。

假如说,一条马路允许30台汽车同时并排通过,现在上面已经有12台了,那么你再放18台是刚好合适的。少于18台,浪费了可用的空间;多于18台,则会造成堵车,最后交通瘫痪。读者可以自己试验一下:创建一个UDP,然后往网络狂发数据,一会儿本机的QQ会掉线,IE之类无法打开,(不过UDP因为跟TCP 不同,TCP是坚持友好协议的,如果阻塞了,会暂停一段时间再启动,也称为慢启动。所以如果一个UDP程序跟TCP程序争夺带宽,TCP一会儿就败下阵来,QQ传输文件比MSN慢也是有这个原因存在)。

当然,带宽是不断变化的,如何准确判断当前的可用带宽,算法有很多,笔者阅读过Lwip、usIP和Windows XP的协议栈,发现基本上可以分为两种。这个后面再说。

先准备一台电脑,一个叫iris的抓包工具用于调试,然后开始我们的TCP之旅吧。

]]>
http://www.138soft.com/?feed=rss2&p=153 13