0

I'd like to get a MAC address from an IP of a host in the same local network. I'd prefer to get this information from the local cache instead of sending a new ARP ARP request. I found that ResolveIpNetEntry2 should be what I need.

Unfortunately I didn't find any code sample for Delphi with that function. Even worse I didn't even find any Delphi headers for that function and its data types. So I tried converting them myself. Well, it compiles, but I get ERROR_INVALID_PARAMETER (87), so apparently I converted something wrong.

Could someone please tell me how to correct it?

const
  IF_MAX_PHYS_ADDRESS_LENGTH = 32;
type
  NET_LUID = record
    case Word of
     1: (Value: Int64;);
     2: (Reserved: Int64;);
     3: (NetLuidIndex: Int64;);
     4: (IfType: Int64;);
  end;

  NL_NEIGHBOR_STATE = (
    NlnsUnreachable=0,
    NlnsIncomplete,
    NlnsProbe,
    NlnsDelay,
    NlnsStale,
    NlnsReachable,
    NlnsPermanent,
    NlnsMaximum);
  PMIB_IPNET_ROW2 = ^MIB_IPNET_ROW2;
  MIB_IPNET_ROW2 = record
    Address: LPSOCKADDR; //SOCKADDR_INET
    InterfaceIndex: ULONG; //NET_IFINDEX
    InterfaceLuid: NET_LUID;
    PhysicalAddress: array [0..IF_MAX_PHYS_ADDRESS_LENGTH - 1] of  UCHAR;
    PhysicalAddressLength: ULONG;
    State: NL_NEIGHBOR_STATE;
    Union: record
      case Integer of
        0: (IsRouter: Boolean;
            IsUnreachable: Boolean);
        1: (Flags: UCHAR);
      end;
    ReachabilityTime: record
      case Integer of
        0: (LastReachable: ULONG);
        1: (LastUnreachable: ULONG);
    end;
  end;

function ResolveIp(const AIp: String; AIfIndex: ULONG): String;
type
  TResolveIpNetEntry2Func = function (Row: PMIB_IPNET_ROW2; const SourceAddress: LPSOCKADDR): DWORD; stdcall; //NETIOAPI_API
const
  IphlpApiDll = 'iphlpapi.dll';
var
  hIphlpApiDll: THandle;
  ResolveIpNetEntry2: TResolveIpNetEntry2Func;
  dw: DWORD;
  Row: PMIB_IPNET_ROW2;
  SourceAddress: LPSOCKADDR;
  IpAddress: LPSOCKADDR;
begin
  hIphlpApiDll := LoadLibrary(IphlpApiDll);
  if hIphlpApiDll = 0 then
    Exit;
  ResolveIpNetEntry2 := GetProcAddress(hIphlpApiDll, 'ResolveIpNetEntry2');
  if (@ResolveIpNetEntry2 = nil) then
    Exit;

  IpAddress := AllocMem(SizeOf(IpAddress));
  IpAddress.sa_family := AF_INET;
  IpAddress.sa_data := PAnsiChar(AIp);
  Row := AllocMem(SizeOf(Row));
  Row.Address := IpAddress;
  Row.InterfaceIndex := AIfIndex;
  SourceAddress := 0;
  dw := ResolveIpNetEntry2(Row, SourceAddress);
  //...
end;
CodeX
  • 717
  • 7
  • 23
  • Why don't you just get the ARP -A IPNUMBER output and extract the MAC address from it ?. – Marc Guillot Nov 14 '16 at 19:06
  • Here you can see an example of how to catch the output of the ARP command http://stackoverflow.com/questions/9119999/getting-output-from-a-shell-dos-app-into-a-delphi-app/9120103#9120103 – Marc Guillot Nov 14 '16 at 19:14
  • I know how to send ARP requests via command line or even better via API (SendArp()). For many reasons in this case I prefer to get the cached information instead of sending new requests. – CodeX Nov 14 '16 at 23:01
  • 1
    The suggested ARP -A command returns the content of the cache. https://technet.microsoft.com/en-us/library/cc940107.aspx – Marc Guillot Nov 14 '16 at 23:06
  • You're right: arp.exe does a cached lookup instead of sending a new one. But in the end it just performs some API calls internally, most likely ResolveIpNetEntry2. I'd always prefer to use the API directly instead of parsing a command line output. – CodeX Nov 15 '16 at 00:29

1 Answers1

4

Per the ResolveIpNetEntry2() documentation:

If the function fails, the return value is one of the following error codes.

...

ERROR_INVALID_PARAMETER
An invalid parameter was passed to the function. This error is returned if a NULL pointer is passed in the Row parameter, the Address member of the MIB_IPNET_ROW2 pointed to by the Row parameter was not set to a valid IPv4 or IPv6 address, or both the InterfaceLuid or InterfaceIndex members of the MIB_IPNET_ROW2 pointed to by the Row parameter were unspecified. This error is also returned if a loopback address was passed in the Address member.

Your translation of the API structures is incorrect in general. You got several fields wrong, which affects field offsets and sizes. Such as the bitfields. But more importantly, your declaration of the MIB_IPNET_ROW2.Address field is completely wrong. It is not a pointer to an external SOCKADDR record at all. It is a SOCKADDR_INET record that exists inside of the MIB_IPNET_ROW2 itself, not externally.

You are also not allocating or initializing your memory blocks correctly, which also plays into the above documentation.

And, even if you had the API translated correctly, you are not converting your AIp string to a SOCKADDR correctly, either. You can't simply type-cast it, you need to actually convert it from string characters to a network address integer, using inet_addr(), InetPton(), or other equivalent function. So that also plays into the above documentation.

You are lucky your code did not crash altogether.

Try something more like this instead:

{$MINENUMSIZE 4}

const
  IF_MAX_PHYS_ADDRESS_LENGTH = 32;

type
  NET_LUID_INFO = record
    Reserved: array [0..2] of UCHAR; // ULONG64:24
    NetLuidIndex: array [0..2] of UCHAR; // ULONG64:24
    IfType: array [0..1] of UCHAR; // ULONG64:16
    //
    // TODO: if you need to access these values, define
    // some property getters/setters to translate them
    // to/from UInt64...
  end;

  NET_LUID = record
    case Integer of
      0: (Value: UInt64);
      1: (Info: NET_LUID_INFO);
  end;

  NL_NEIGHBOR_STATE = (
    NlnsUnreachable = 0,
    NlnsIncomplete,
    NlnsProbe,
    NlnsDelay,
    NlnsStale,
    NlnsReachable,
    NlnsPermanent,
    NlnsMaximum);

  PSOCKADDR_INET = ^SOCKADDR_INET;
  SOCKADDR_INET = record
    case Integer of
      0: (Ipv4: SOCKADDR_IN);
      1: (Ipv6: SOCKADDR_IN6);
      2: (si_family: ADDRESS_FAMILY);
  end;

  NETIO_STATUS = DWORD;
  NET_IFINDEX = ULONG;

  PMIB_IPNET_ROW2 = ^MIB_IPNET_ROW2;
  MIB_IPNET_ROW2 = record
    Address: SOCKADDR_INET;
    InterfaceIndex: NET_IFINDEX;
    InterfaceLuid: NET_LUID;
    PhysicalAddress: array [0..IF_MAX_PHYS_ADDRESS_LENGTH - 1] of UCHAR;
    PhysicalAddressLength: ULONG;
    State: NL_NEIGHBOR_STATE;
    Flags: UCHAR;
    ReachabilityTime: record
      case Integer of
        0: (LastReachable: ULONG);
        1: (LastUnreachable: ULONG);
    end;

    function IsRouter: Boolean;
    function IsUnreachable;
  end;

function MIB_IPNET_ROW2.IsRouter: Boolean;
begin
  Result := (Flags and $01) <> 0;
end;

function MIB_IPNET_ROW2.IsUnreachable;
begin
  Result := (Flags and $02) <> 0;
end;

function ResolveIp(const AIp: String; AIfIndex: ULONG): String;
type
  TResolveIpNetEntry2Func = function (Row: PMIB_IPNET_ROW2; const SourceAddress: PSOCKADDR_INET): NETIO_STATUS; stdcall;
const
  IphlpApiDll = 'iphlpapi.dll';
var
  hIphlpApiDll: THandle;
  ResolveIpNetEntry2: TResolveIpNetEntry2Func;
  status: NETIO_STATUS;
  Row: PMIB_IPNET_ROW2;
begin
  Result := '';
  hIphlpApiDll := LoadLibrary(IphlpApiDll);
  if hIphlpApiDll = 0 then
    Exit;
  try
    @ResolveIpNetEntry2 := GetProcAddress(hIphlpApiDll, 'ResolveIpNetEntry2');
    if not Assigned(ResolveIpNetEntry2) then
      Exit;

    New(Row);
    try
      ZeroMemory(Row, SizeOf(MIB_IPNET_ROW2));

      if InetPton(AF_INET, PChar(AIp), @(Row.Address.Ipv4.sin_addr)) = 1 then
        Row.Address.Ipv4.sin_family := AF_INET
      else
      if InetPton(AF_INET6, PChar(AIp), @(Row.Address.Ipv6.sin6_addr)) = 1 then
        Row.Address.Ipv6.sin6_family := AF_INET6
      else
        Exit;

      Row.InterfaceIndex := AIfIndex;

      status := ResolveIpNetEntry2(Row, nil);
      //...
    finally
      Dispose(Row);
    end;
  finally
    FreeLibrary(hIphlpApiDll);
  end;
end;

Alternatively, ResolveIp() can be written without dynamically allocating the Row at all:

function ResolveIp(const AIp: String; AIfIndex: ULONG): String;
...
var
  ...
  Row: MIB_IPNET_ROW2;
begin
  ...
  ZeroMemory(@Row, SizeOf(Row));
  ...
  status := ResolveIpNetEntry2(@Row, nil);
  ...
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thank you very much! Are ADDRESS_FAMILY (ULONG?) and InetPton included somewhere? It's really a pitty that Delphi Jedi doesn't include any of the newer headers for Vista and later. – CodeX Nov 15 '16 at 00:26
  • `ADDRESS_FAMILY` is actually a `USHORT`, and it is defined in Delphi's `ShellAPI` unit (off place to put it, though). I don't think any Delphi unit declares `InetPton()`, but you can declare it yourself in your code, it is in `Ws2_32.dll`. – Remy Lebeau Nov 15 '16 at 00:40
  • I can get the Ansi version inet_pton to work, but unfortunately I don't know what to do with the WSAAPI information to get its InetPton Unicode pendant. `function inet_pton(Family: Integer; const AddrString: PChar; PAddrBuf: Pointer): Integer; stdcall; external 'Ws2_32.dll';` – CodeX Nov 15 '16 at 00:54
  • If you [read the documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/cc805844.aspx), you would see that the Ansi version is `inet_pton()` and `InetPtonA()`, and the Unicode version is `InetPtonW()`. – Remy Lebeau Nov 15 '16 at 00:56
  • Apparently I misinterpreted the very first sentence in the documentation as "InetPton is the Unicode version and inet_pton is the Ansi version" and didn't know what to do with the WSAAPI in the declaration. Yes, InetPtonW does the trick. Thank you very much! – CodeX Nov 15 '16 at 01:05
  • `WSAAPI` is just a macro that defines the calling convention used (ie `stdcall`). – Remy Lebeau Nov 15 '16 at 01:08
  • For learning purposes I'd like to understand your "Alternatively..." comment. By using `ZeroMemory(@Row, SizeOf(Row));` Row becomes nil and as such fails in the next line with an access violation. Could you please explain this alternative? – CodeX Nov 15 '16 at 01:18
  • In the "alternative" code, `Row` is an entire `MIB_IPNET_ROW2` record declared locally on the stack, not allocated dynamically on the heap like in the first code. The `ZeroMemory(@Row)` call is zeroing out the content of that stack memory. Since `Row` is not a pointer, it cannot be `nil`. You are thinking of the first code, where `Row` is a pointer, in which case using `ZeroMemory(@Row)` instead of `ZeroMemory(Row)` could indeed set the pointer itself to nil rather than zeroing the memory it is pointing at. You have to pay attention to the memory addresses that you pass around. – Remy Lebeau Nov 15 '16 at 01:21
  • Of course, thank you! You're such an enrichment to the Delphi community! – CodeX Nov 15 '16 at 01:27