{==============================================================================|
| Project : Ararat Synapse                                       | 004.000.000 |
|==============================================================================|
| Content: PING sender                                                         |
|==============================================================================|
| Copyright (c)1999-2007, Lukas Gebauer                                        |
| All rights reserved.                                                         |
|                                                                              |
| Redistribution and use in source and binary forms, with or without           |
| modification, are permitted provided that the following conditions are met:  |
|                                                                              |
| Redistributions of source code must retain the above copyright notice, this  |
| list of conditions and the following disclaimer.                             |
|                                                                              |
| Redistributions in binary form must reproduce the above copyright notice,    |
| this list of conditions and the following disclaimer in the documentation    |
| and/or other materials provided with the distribution.                       |
|                                                                              |
| Neither the name of Lukas Gebauer nor the names of its contributors may      |
| be used to endorse or promote products derived from this software without    |
| specific prior written permission.                                           |
|                                                                              |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"  |
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE    |
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE   |
| ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR  |
| ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL       |
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR   |
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER   |
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT           |
| LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY    |
| OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH  |
| DAMAGE.                                                                      |
|==============================================================================|
| The Initial Developer of the Original Code is Lukas Gebauer (Czech Republic).|
| Portions created by Lukas Gebauer are Copyright (c)2000-2007.                |
| All Rights Reserved.                                                         |
|==============================================================================|
| Contributor(s):                                                              |
|==============================================================================|
| History: see HISTORY.HTM from distribution package                           |
|          (Found at URL: http://www.ararat.cz/synapse/)                       |
|==============================================================================}

{:@abstract(ICMP PING implementation.)
Allows create PING and TRACEROUTE. Or you can diagnose your network.

This unit using IpHlpApi (on WinXP or higher) if available. Otherwise it trying
 to use RAW sockets.

Warning: For use of RAW sockets you must have some special rights on some
 systems. So, it working allways when you have administator/root rights.
 Otherwise you can have problems!

Note: This unit is NOT portable to .NET!
  Use native .NET classes for Ping instead.
}

{$IFDEF FPC}
  {$MODE DELPHI}
{$ENDIF}
{$Q-}
{$R-}
{$H+}

{$IFDEF CIL}
  Sorry, this unit is not for .NET!
{$ENDIF}

unit pingsend;

interface

uses
  SysUtils,
  synsock, blcksock, synautil, synafpc, synaip
{$IFDEF WIN32}
  , windows
{$ENDIF}
  ;

const
  ICMP_ECHO = 8;
  ICMP_ECHOREPLY = 0;
  ICMP_UNREACH = 3;
  ICMP_TIME_EXCEEDED = 11;
//rfc-2292
  ICMP6_ECHO = 128;
  ICMP6_ECHOREPLY = 129;
  ICMP6_UNREACH = 1;
  ICMP6_TIME_EXCEEDED = 3;

type
  {:List of possible ICMP reply packet types.}
  TICMPError = (
    IE_NoError,
    IE_Other,
    IE_TTLExceed,
    IE_UnreachOther,
    IE_UnreachRoute,
    IE_UnreachAdmin,
    IE_UnreachAddr,
    IE_UnreachPort
    );

  {:@abstract(Implementation of ICMP PING and ICMPv6 PING.)}
  TPINGSend = class(TSynaClient)
  private
    FSock: TICMPBlockSocket;
    FBuffer: string;
    FSeq: Integer;
    FId: Integer;
    FPacketSize: Integer;
    FPingTime: Integer;
    FIcmpEcho: Byte;
    FIcmpEchoReply: Byte;
    FIcmpUnreach: Byte;
    FReplyFrom: string;
    FReplyType: byte;
    FReplyCode: byte;
    FReplyError: TICMPError;
    FReplyErrorDesc: string;
    FTTL: Byte;
    Fsin: TVarSin;
    function Checksum(Value: string): Word;
    function Checksum6(Value: string): Word;
    function ReadPacket: Boolean;
    procedure TranslateError;
    procedure TranslateErrorIpHlp(value: integer);
    function InternalPing(const Host: string): Boolean;
    function InternalPingIpHlp(const Host: string): Boolean;
    function IsHostIP6(const Host: string): Boolean;
    procedure GenErrorDesc;
  public
    {:Send ICMP ping to host and count @link(pingtime). If ping OK, result is
     @true.}
    function Ping(const Host: string): Boolean;
    constructor Create;
    destructor Destroy; override;
  published
    {:Size of PING packet. Default size is 32 bytes.}
    property PacketSize: Integer read FPacketSize Write FPacketSize;

    {:Time between request and reply.}
    property PingTime: Integer read FPingTime;

    {:From this address is sended reply for your PING request. It maybe not your
     requested destination, when some error occured!}
    property ReplyFrom: string read FReplyFrom;

    {:ICMP type of PING reply. Each protocol using another values! For IPv4 and
     IPv6 are used different values!}
    property ReplyType: byte read FReplyType;

    {:ICMP code of PING reply. Each protocol using another values! For IPv4 and
     IPv6 are used different values! For protocol independent value look to
     @link(ReplyError)}
    property ReplyCode: byte read FReplyCode;

    {:Return type of returned ICMP message. This value is independent on used
     protocol!}
    property ReplyError: TICMPError read FReplyError;

    {:Return human readable description of returned packet type.}
    property ReplyErrorDesc: string read FReplyErrorDesc;

    {:Socket object used for TCP/IP operation. Good for seting OnStatus hook, etc.}
    property Sock: TICMPBlockSocket read FSock;

    {:TTL value for ICMP query}
    property TTL: byte read FTTL write FTTL;
  end;

{:A very useful function and example of its use would be found in the TPINGSend
 object. Use it to ping to any host. If successful, returns the ping time in
 milliseconds.  Returns -1 if an error occurred.}
function PingHost(const Host: string): Integer;

{:A very useful function and example of its use would be found in the TPINGSend
 object. Use it to TraceRoute to any host.}
function TraceRouteHost(const Host: string): string;

implementation

type
  {:Record for ICMP ECHO packet header.}
  TIcmpEchoHeader = record
    i_type: Byte;
    i_code: Byte;
    i_checkSum: Word;
    i_Id: Word;
    i_seq: Word;
    TimeStamp: integer;
  end;

  {:record used internally by TPingSend for compute checksum of ICMPv6 packet
   pseudoheader.}
  TICMP6Packet = record
    in_source: TInAddr6;
    in_dest: TInAddr6;
    Length: integer;
    free0: Byte;
    free1: Byte;
    free2: Byte;
    proto: Byte;
  end;

{$IFDEF WIN32}
const
  DLLIcmpName = 'iphlpapi.dll';
type
  TIP_OPTION_INFORMATION = packed record
    TTL: Byte;
    TOS: Byte;
    Flags: Byte;
    OptionsSize: Byte;
    OptionsData: PChar;
  end;
  PIP_OPTION_INFORMATION = ^TIP_OPTION_INFORMATION;

  TICMP_ECHO_REPLY = packed record
    Address: TInAddr;
    Status: integer;
    RoundTripTime: integer;
    DataSize: Word;
    Reserved: Word;
    Data: pointer;
    Options: TIP_OPTION_INFORMATION;
  end;
  PICMP_ECHO_REPLY = ^TICMP_ECHO_REPLY;

  TICMPV6_ECHO_REPLY = packed record
    Address: TSockAddrIn6;
    Status: integer;
    RoundTripTime: integer;
  end;
  PICMPV6_ECHO_REPLY = ^TICMPV6_ECHO_REPLY;

  TIcmpCreateFile = function: integer; stdcall;
  TIcmpCloseHandle = function(handle: integer): boolean; stdcall;
  TIcmpSendEcho2 = function(handle: integer; Event: pointer; ApcRoutine: pointer;
    ApcContext: pointer; DestinationAddress: TInAddr; RequestData: pointer;
    RequestSize: integer; RequestOptions: PIP_OPTION_INFORMATION;
    ReplyBuffer: pointer; ReplySize: integer; Timeout: Integer): integer; stdcall;
  TIcmp6CreateFile = function: integer; stdcall;
  TIcmp6SendEcho2 = function(handle: integer; Event: pointer; ApcRoutine: pointer;
    ApcContext: pointer; SourceAddress: PSockAddrIn6; DestinationAddress: PSockAddrIn6;
    RequestData: pointer; RequestSize: integer; RequestOptions: PIP_OPTION_INFORMATION;
    ReplyBuffer: pointer; ReplySize: integer; Timeout: Integer): integer; stdcall;

var
  IcmpDllHandle: TLibHandle = 0;
  IcmpHelper4: boolean = false;
  IcmpHelper6: boolean = false;
  IcmpCreateFile: TIcmpCreateFile = nil;
  IcmpCloseHandle: TIcmpCloseHandle = nil;
  IcmpSendEcho2: TIcmpSendEcho2 = nil;
  Icmp6CreateFile: TIcmp6CreateFile = nil;
  Icmp6SendEcho2: TIcmp6SendEcho2 = nil;
{$ENDIF}
{==============================================================================}

constructor TPINGSend.Create;
begin
  inherited Create;
  FSock := TICMPBlockSocket.Create;
  FTimeout := 5000;
  FPacketSize := 32;
  FSeq := 0;
  Randomize;
  FTTL := 128;
end;

destructor TPINGSend.Destroy;
begin
  FSock.Free;
  inherited Destroy;
end;

function TPINGSend.ReadPacket: Boolean;
begin
  FBuffer := FSock.RecvPacket(Ftimeout);
  Result := FSock.LastError = 0;
end;

procedure TPINGSend.GenErrorDesc;
begin
  case FReplyError of
    IE_NoError:
      FReplyErrorDesc := '';
    IE_Other:
      FReplyErrorDesc := 'Unknown error';
    IE_TTLExceed:
      FReplyErrorDesc := 'TTL Exceeded';
    IE_UnreachOther:
      FReplyErrorDesc := 'Unknown unreachable';
    IE_UnreachRoute:
      FReplyErrorDesc := 'No route to destination';
    IE_UnreachAdmin:
      FReplyErrorDesc := 'Administratively prohibited';
    IE_UnreachAddr:
      FReplyErrorDesc := 'Address unreachable';
    IE_UnreachPort:
      FReplyErrorDesc := 'Port unreachable';
  end;
end;

function TPINGSend.IsHostIP6(const Host: string): Boolean;
var
  f: integer;
begin
  f := AF_UNSPEC;
  if IsIp(Host) then
    f := AF_INET
  else
    if IsIp6(Host) then
      f := AF_INET6;
  synsock.SetVarSin(Fsin, host, '0', f,
    IPPROTO_UDP, SOCK_DGRAM, Fsock.PreferIP4);
  result := Fsin.sin_family = AF_INET6;
end;

function TPINGSend.Ping(const Host: string): Boolean;
var
  b: boolean;
begin
  FPingTime := -1;
  FReplyFrom := '';
  FReplyType := 0;
  FReplyCode := 0;
  FReplyError := IE_Other;
  GenErrorDesc;
  FBuffer := StringOfChar(#55, SizeOf(TICMPEchoHeader) + FPacketSize);
{$IFDEF WIN32}
  b := IsHostIP6(host);
  if not(b) and IcmpHelper4 then
    result := InternalPingIpHlp(host)
  else
    if b and IcmpHelper6 then
      result := InternalPingIpHlp(host)
    else
      result := InternalPing(host);
{$ELSE}
   result := InternalPing(host);
{$ENDIF}
end;

function TPINGSend.InternalPing(const Host: string): Boolean;
var
  IPHeadPtr: ^TIPHeader;
  IpHdrLen: Integer;
  IcmpEchoHeaderPtr: ^TICMPEchoHeader;
  t: Boolean;
  x: cardinal;
  IcmpReqHead: string;
begin
  Result := False;
  FSock.TTL := FTTL;
  FSock.Bind(FIPInterface, cAnyPort);
  FSock.Connect(Host, '0');
  if FSock.LastError <> 0 then
    Exit;
  FSock.SizeRecvBuffer := 60 * 1024;
  if FSock.IP6used then
  begin
    FIcmpEcho := ICMP6_ECHO;
    FIcmpEchoReply := ICMP6_ECHOREPLY;
    FIcmpUnreach := ICMP6_UNREACH;
  end
  else
  begin
    FIcmpEcho := ICMP_ECHO;
    FIcmpEchoReply := ICMP_ECHOREPLY;
    FIcmpUnreach := ICMP_UNREACH;
  end;
  IcmpEchoHeaderPtr := Pointer(FBuffer);
  with IcmpEchoHeaderPtr^ do
  begin
    i_type := FIcmpEcho;
    i_code := 0;
    i_CheckSum := 0;
    FId := System.Random(32767);
    i_Id := FId;
    TimeStamp := GetTick;
    Inc(FSeq);
    i_Seq := FSeq;
    if fSock.IP6used then
      i_CheckSum := CheckSum6(FBuffer)
    else
      i_CheckSum := CheckSum(FBuffer);
  end;
  FSock.SendString(FBuffer);
  // remember first 8 bytes of ICMP packet
  IcmpReqHead := Copy(FBuffer, 1, 8);
  x := GetTick;
  repeat
    t := ReadPacket;
    if not t then
      break;
    if fSock.IP6used then
    begin
{$IFNDEF WIN32}
      IcmpEchoHeaderPtr := Pointer(FBuffer);
{$ELSE}
//WinXP SP1 with networking update doing this think by another way ;-O
//      FBuffer := StringOfChar(#0, 4) + FBuffer;
      IcmpEchoHeaderPtr := Pointer(FBuffer);
//      IcmpEchoHeaderPtr^.i_type := FIcmpEchoReply;
{$ENDIF}
    end
    else
    begin
      IPHeadPtr := Pointer(FBuffer);
      IpHdrLen := (IPHeadPtr^.VerLen and $0F) * 4;
      IcmpEchoHeaderPtr := @FBuffer[IpHdrLen + 1];
    end;
  //check for timeout
    if TickDelta(x, GetTick) > FTimeout then
    begin
      t := false;
      Break;
    end;
  //it discard sometimes possible 'echoes' of previosly sended packet
  //or other unwanted ICMP packets...
  until (IcmpEchoHeaderPtr^.i_type <> FIcmpEcho)
    and ((IcmpEchoHeaderPtr^.i_id = FId)
    or (Pos(IcmpReqHead, FBuffer) > 0));
  if t then
    begin
      FPingTime := TickDelta(x, GetTick);
      FReplyFrom := FSock.GetRemoteSinIP;
      FReplyType := IcmpEchoHeaderPtr^.i_type;
      FReplyCode := IcmpEchoHeaderPtr^.i_code;
      TranslateError;
      Result := True;
    end;
end;

function TPINGSend.Checksum(Value: string): Word;
var
  CkSum: integer;
  Num, Remain: Integer;
  n, i: Integer;
begin
  Num := Length(Value) div 2;
  Remain := Length(Value) mod 2;
  CkSum := 0;
  i := 1;
  for n := 0 to Num - 1 do
  begin
    CkSum := CkSum + Synsock.HtoNs(DecodeInt(Value, i));
    inc(i, 2);
  end;
  if Remain <> 0 then
    CkSum := CkSum + Ord(Value[Length(Value)]);
  CkSum := (CkSum shr 16) + (CkSum and $FFFF);
  CkSum := CkSum + (CkSum shr 16);
  Result := Word(not CkSum);
end;

function TPINGSend.Checksum6(Value: string): Word;
const
  IOC_OUT = $40000000;
  IOC_IN = $80000000;
  IOC_INOUT = (IOC_IN or IOC_OUT);
  IOC_WS2 = $08000000;
  SIO_ROUTING_INTERFACE_QUERY = 20 or IOC_WS2 or IOC_INOUT;
var
  ICMP6Ptr: ^TICMP6Packet;
  s: string;
  b: integer;
  ip6: TSockAddrIn6;
  x: integer;
begin
  Result := 0;
{$IFDEF WIN32}
  s := StringOfChar(#0, SizeOf(TICMP6Packet)) + Value;
  ICMP6Ptr := Pointer(s);
  x := synsock.WSAIoctl(FSock.Socket, SIO_ROUTING_INTERFACE_QUERY,
    @FSock.RemoteSin, SizeOf(FSock.RemoteSin),
    @ip6, SizeOf(ip6), @b, nil, nil);
  if x <> -1 then
    ICMP6Ptr^.in_dest := ip6.sin6_addr
  else
    ICMP6Ptr^.in_dest := FSock.LocalSin.sin6_addr;
  ICMP6Ptr^.in_source := FSock.RemoteSin.sin6_addr;
  ICMP6Ptr^.Length := synsock.htonl(Length(Value));
  ICMP6Ptr^.proto := IPPROTO_ICMPV6;
  Result := Checksum(s);
{$ENDIF}
end;

procedure TPINGSend.TranslateError;
begin
  if fSock.IP6used then
  begin
    case FReplyType of
      ICMP6_ECHOREPLY:
        FReplyError := IE_NoError;
      ICMP6_TIME_EXCEEDED:
        FReplyError := IE_TTLExceed;
      ICMP6_UNREACH:
        case FReplyCode of
          0:
            FReplyError := IE_UnreachRoute;
          3:
            FReplyError := IE_UnreachAddr;
          4:
            FReplyError := IE_UnreachPort;
          1:
            FReplyError := IE_UnreachAdmin;
        else
          FReplyError := IE_UnreachOther;
        end;
    else
      FReplyError := IE_Other;
    end;
  end
  else
  begin
    case FReplyType of
      ICMP_ECHOREPLY:
        FReplyError := IE_NoError;
      ICMP_TIME_EXCEEDED:
        FReplyError := IE_TTLExceed;
      ICMP_UNREACH:
        case FReplyCode of
          0:
            FReplyError := IE_UnreachRoute;
          1:
            FReplyError := IE_UnreachAddr;
          3:
            FReplyError := IE_UnreachPort;
          13:
            FReplyError := IE_UnreachAdmin;
        else
          FReplyError := IE_UnreachOther;
        end;
    else
      FReplyError := IE_Other;
    end;
  end;
  GenErrorDesc;
end;

procedure TPINGSend.TranslateErrorIpHlp(value: integer);
begin
  case value of
    11000, 0:
      FReplyError := IE_NoError;
    11013:
      FReplyError := IE_TTLExceed;
    11002:
      FReplyError := IE_UnreachRoute;
    11003:
      FReplyError := IE_UnreachAddr;
    11005:
      FReplyError := IE_UnreachPort;
    11004:
      FReplyError := IE_UnreachAdmin;
  else
    FReplyError := IE_Other;
  end;
  GenErrorDesc;
end;

function TPINGSend.InternalPingIpHlp(const Host: string): Boolean;
{$IFDEF WIN32}
var
  PingIp6: boolean;
  PingHandle: integer;
  r: integer;
  ipo: TIP_OPTION_INFORMATION;
  RBuff: string;
  ip4reply: PICMP_ECHO_REPLY;
  ip6reply: PICMPV6_ECHO_REPLY;
  ip6: TSockAddrIn6;
begin
  Result := False;
  PingIp6 := Fsin.sin_family = AF_INET6;
  if pingIp6 then
    PingHandle := Icmp6CreateFile
  else
    PingHandle := IcmpCreateFile;
  if PingHandle <> -1 then
  begin
    try
      ipo.TTL := FTTL;
      ipo.TOS := 0;
      ipo.Flags := 0;
      ipo.OptionsSize := 0;
      ipo.OptionsData := nil;
      setlength(RBuff, 4096);
      if pingIp6 then
      begin
        FillChar(ip6, sizeof(ip6), 0);
        r := Icmp6SendEcho2(PingHandle, nil, nil, nil, @ip6, @Fsin,
          Pchar(FBuffer), length(FBuffer), @ipo, pchar(RBuff), length(RBuff), FTimeout);
        if r > 0 then
        begin
          RBuff := #0 + #0 + RBuff;
          ip6reply := PICMPV6_ECHO_REPLY(pointer(RBuff));
          FPingTime := ip6reply^.RoundTripTime;
          ip6reply^.Address.sin6_family := AF_INET6;
          FReplyFrom := GetSinIp(TVarSin(ip6reply^.Address));
          TranslateErrorIpHlp(ip6reply^.Status);
          Result := True;
        end;
      end
      else
      begin
        r := IcmpSendEcho2(PingHandle, nil, nil, nil, Fsin.sin_addr,
          Pchar(FBuffer), length(FBuffer), @ipo, pchar(RBuff), length(RBuff), FTimeout);
        if r > 0 then
        begin
          ip4reply := PICMP_ECHO_REPLY(pointer(RBuff));
          FPingTime := ip4reply^.RoundTripTime;
          FReplyFrom := IpToStr(swapbytes(ip4reply^.Address.S_addr));
          TranslateErrorIpHlp(ip4reply^.Status);
          Result := True;
        end;
      end
    finally
      IcmpCloseHandle(PingHandle);
    end;
  end;
end;
{$ELSE}
begin
  result := false;
end;
{$ENDIF}

{==============================================================================}

function PingHost(const Host: string): Integer;
begin
  with TPINGSend.Create do
  try
    Result := -1;
    if Ping(Host) then
      if ReplyError = IE_NoError then
        Result := PingTime;
  finally
    Free;
  end;
end;

function TraceRouteHost(const Host: string): string;
var
  Ping: TPingSend;
  ttl : byte;
begin
  Result := '';
  Ping := TPINGSend.Create;
  try
    ttl := 1;
    repeat
      ping.TTL := ttl;
      inc(ttl);
      if ttl > 30 then
        Break;
      if not ping.Ping(Host) then
      begin
        Result := Result + cAnyHost+ ' Timeout' + CRLF;
        continue;
      end;
      if (ping.ReplyError <> IE_NoError)
        and (ping.ReplyError <> IE_TTLExceed) then
      begin
        Result := Result + Ping.ReplyFrom + ' ' + Ping.ReplyErrorDesc + CRLF;
        break;
      end;
      Result := Result + Ping.ReplyFrom + ' ' + IntToStr(Ping.PingTime) + CRLF;
    until ping.ReplyError = IE_NoError;
  finally
    Ping.Free;
  end;
end;

{$IFDEF WIN32}
initialization
begin
  IcmpHelper4 := false;
  IcmpHelper6 := false;
  IcmpDllHandle := LoadLibrary(DLLIcmpName);
  if IcmpDllHandle <> 0 then
  begin
    IcmpCreateFile := GetProcAddress(IcmpDLLHandle, 'IcmpCreateFile');
    IcmpCloseHandle := GetProcAddress(IcmpDLLHandle, 'IcmpCloseHandle');
    IcmpSendEcho2 := GetProcAddress(IcmpDLLHandle, 'IcmpSendEcho2');
    Icmp6CreateFile := GetProcAddress(IcmpDLLHandle, 'Icmp6CreateFile');
    Icmp6SendEcho2 := GetProcAddress(IcmpDLLHandle, 'Icmp6SendEcho2');
    IcmpHelper4 := assigned(IcmpCreateFile)
      and assigned(IcmpCloseHandle)
      and assigned(IcmpSendEcho2);
    IcmpHelper6 := assigned(Icmp6CreateFile)
      and assigned(Icmp6SendEcho2);
  end;
end;

finalization
begin
  FreeLibrary(IcmpDllHandle);
end;
{$ENDIF}

end.
