////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2001-2023 Alexey Kuryakin daqgroup@mail.ru under MIT license //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// This file is part of the CRW-DAQ project by DaqGroup - component CRWLIB.   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Purpose:                                                                   //
// Routines for network interface adapters.                                   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// History:                                                                   //
// 20231005 - Created by A.K.                                                 //
////////////////////////////////////////////////////////////////////////////////

unit _crw_netif; //  Network interface adapters functions.

{$I _crw_sysdef}

{$IFDEF FPC}{$mode Delphi}{$ENDIF}{$H+}

interface

uses
 _crw_sharemem, // NB: THIS UNIT MUST BE FIRST !!!
 {$IFDEF UNIX} cthreads, dl, {$ENDIF}
 {$IFDEF WINDOWS} windows, {$ENDIF}
 {$IFDEF WINDOWS} JwaIpExport, JwaIpRtrMib, JwaIpTypes, JwaWinType, JwaWinBase, JwaWinSock, JwaIpHlpApi, {$ENDIF}
 sysutils, classes, math, ctypes, sockets,
 _crw_alloc, _crw_ef, _crw_utf8, _crw_str;

{$IFDEF UNIX}
{$PACKRECORDS C}
type
 PPifaddrs = ^Pifaddrs;
 Pifaddrs = ^ifaddrs;
 ifaddrs = record
  ifa_next: Pifaddrs;
  ifa_name: PAnsiChar;
  ifa_flags: cuint;
  ifa_addr: Psockaddr;
  ifa_netmask: Psockaddr;
  ifa_dstaddr: Psockaddr;
  ifa_data: Pointer;
 end platform;

function getifaddrs(ifap:PPifaddrs):Integer; cdecl; external 'c' name 'getifaddrs';
procedure freeifaddrs(ifap:pifaddrs); cdecl; external 'c' name 'freeifaddrs';

const
 IF_NAMESIZE   = 16;        // ifaddrs.ifa_name name size

const                         // ifaddrs.ifa_flags:
 IFF_UP          = $00000001; // Interface is running.
 IFF_BROADCAST   = $00000002; // Valid broadcast address set.
 IFF_DEBUG       = $00000004; // Internal debugging flag.
 IFF_LOOPBACK    = $00000008; // Interface is a loopback interface.
 IFF_POINTOPOINT = $00000010; // Interface is a point-to-point link.
 IFF_NOTRAILERS  = $00000020; // Avoid use of trailers.
 IFF_RUNNING     = $00000040; // Resources allocated.
 IFF_NOARP       = $00000080; // No arp protocol, L2 destination address not set.
 IFF_PROMISC     = $00000100; // Interface is in promiscuous mode.
 IFF_ALLMULTI    = $00000200; // Receive all multicast packets.
 IFF_MASTER      = $00000400; // Master of a load balancing bundle.
 IFF_SLAVE       = $00000800; // Slave of a load balancing bundle.
 IFF_MULTICAST   = $00001000; // Supports multicast
 IFF_PORTSEL     = $00002000; // Is able to select media type via ifmap.
 IFF_AUTOMEDIA   = $00004000; // Auto media selection active.
 IFF_DYNAMIC     = $00008000; // The addresses are lost when the interface goes down.
 IFF_LOWER_UP	 = $00010000; // Driver signals L1 up (since Linux 2.6.17)
 IFF_DORMANT     = $00020000; // Driver signals dormant (since Linux 2.6.17)
 IFF_ECHO        = $00040000; // Echo sent packets (since Linux 2.6.25)
const // List of known interface flags
 IFF_LIST_KNOWN  = 'UP,BROADCAST,DEBUG,LOOPBACK,P2P,NOTRAILERS,RUNNING,NOARP,'
                  +'PROMISC,ALLMULTI,MASTER,SLAVE,MULTICAST,PORTSEL,AUTOMEDIA,'
                  +'DYNAMIC,LOWERUP,DORMANT,ECHO';
const // List of known interface address families
 AF_LIST_KNOWN   = 'UNIX,INET,AX25,IPX,APPLETALK,NETROM,BRIDGE,ATMPVC,X25,'
                  +'INET6,ROSE,DECnet,NETBEUI,SECURITY,KEY,NETLINK,PACKET,'
                  +'ASH,ECONET,ATMSVC,RDS,SNA,IRDA,PPPOX,WANPIPE,LLC,'
                  +'IB,MPLS,CAN,TIPC,BLUETOOTH,IUCV,RXRPC,ISDN,PHONET,'
                  +'IEEE802154,CAIF,ALG,NFC,VSOCK,KCM,QIPCRTR,SMC,XDP';

 // Check if aIPv4Addr IP address belongs to localhost.
function IPv4BelongsToLocalHostFast(aIPv4Addr:LongString):Boolean;
{$ENDIF ~UNIX}

 // Get list of local network interfaces like:
 //  INTERFACE MAC_ADDRESS        IP_ADDRESS    NET_MASK       FLAGS
 //  eth0      28:d2:44:3b:9c:3b  192.168.0.11  255.255.255.0  69699
 //  wlan0     00:c2:c6:13:d0:30  192.168.0.12  255.255.255.0  69699
 //  vboxnet0  0a:00:27:00:00:00  192.168.56.1  255.255.255.0  69699
function GetListOfNetworkInterfaces(Mode:Integer=-1):LongString;

const                      // Network Interface Mode:
 nim_up       = $00000001; // Show only UP interfaces
 nim_skiploop = $00000002; // Skip loopback interfaces
 nim_skipipv4 = $00000004; // Skip IPv4 interfaces
 nim_skipipv6 = $00000008; // Skip IPv6 interfaces
 nim_nice     = $00000010; // Nice table format
 nim_sort     = $00000020; // Sort table
 nim_others   = $00000040; // List others (non IP)
 nim_default  : Integer = nim_up+nim_skiploop+nim_skipipv6+nim_nice;

 // Check if aIPv4Addr IP address belongs to localhost.
function IPv4BelongsToLocalHost(aIPv4Addr:LongString):Boolean;

implementation

{$IFDEF UNIX}
function ValidateName(Name:PChar):LongString;
begin
 Result:=Trim(StrPas(Name));
 if HasChars(Result,JustBlanks) then begin
  Result:=StringReplace(Result,ASCII_HT,' ',[rfReplaceAll]);
  Result:=StringReplace(Result,' ','_',[rfReplaceAll]);
 end;
end;

function EncodeFamilyFlags(Family,Flags:Cardinal):LongString;
var i:Integer;
begin
 Result:=ExtractWord(Family,AF_LIST_KNOWN,ScanSpaces);
 if IsEmptyStr(Result) then Result:='UNKNOWN';
 for i:=0 to WordCount(IFF_LIST_KNOWN,ScanSpaces)-1 do
 if IsBit(Flags,i) then begin
  if (Result<>'') then Result:=Result+'+';
  Result:=Result+ExtractWord(i+1,IFF_LIST_KNOWN,ScanSpaces);
 end;
end;

function ReadMacAddr(id:LongString):LongString;
begin
 Result:=Trim(ReadTextLinesFromFile('/sys/class/net/'+Trim(id)+'/address'));
 if IsEmptyStr(Result) then Result:='00:00:00:00:00:00';
end;

function PNetAddrToStr(addr:Psockaddr):LongString;
const zero_addr:in_addr=(s_addr:0);
begin
 if Assigned(addr)
 then Result:=NetAddrToStr(addr.sin_addr)
 else Result:=NetAddrToStr(zero_addr);
end;

function PNetAddrToStr6(addr:Psockaddr):LongString;
const zero_addr:in6_addr=(s6_addr32:(0,0,0,0));
begin
 if Assigned(addr)
 then Result:=NetAddrToStr6(psockaddr_in6(addr).sin6_addr)
 else Result:=NetAddrToStr6(zero_addr);
end;

function GetListOfNetworkInterfaces(Mode:Integer=-1):LongString;
var iadr,curif:Pifaddrs; Lines:TStringList; Line:LongString;
var family:sa_family_t; Match,Up,Loopback:Boolean;
var i,n1,n2,n3,n4:Integer;
begin
 Result:='';
 try
  if (Mode=-1) then Mode:=nim_default;
  if (getifaddrs(@iadr)=NO_ERROR) then
  try
   Lines:=TStringList.Create;
   try
    curif:=iadr;
    while Assigned(curif) do begin
     Line:=''; Match:=true;
     if Assigned(curif.ifa_name) then
     if Assigned(curif.ifa_addr) then begin
      family:=curif.ifa_addr.sa_family;
      Up:=HasFlags(curif.ifa_flags,IFF_UP);
      Loopback:=HasFlags(curif.ifa_flags,IFF_LOOPBACK);
      if HasFlags(Mode,nim_up) then Match:=Match and Up;
      if HasFlags(Mode,nim_skiploop) then Match:=Match and not Loopback;
      if Match then
      case family of
       AF_INET: begin
        if not HasFlags(Mode,nim_skipipv4) then
        Line:=ValidateName(curif.ifa_name)
        +' '+ReadMacAddr(ValidateName(curif.ifa_name))
        +' '+PNetAddrToStr(curif.ifa_addr)
        +' '+PNetAddrToStr(curif.ifa_netmask)
        +' '+EncodeFamilyFlags(family,curif.ifa_flags);
       end;
       AF_INET6: begin
        if not HasFlags(Mode,nim_skipipv6) then
        if (psockaddr_in6(curif.ifa_addr).sin6_scope_id=0) then // don't include scoped IPv6
        Line:=ValidateName(curif.ifa_name)
        +' '+ReadMacAddr(ValidateName(curif.ifa_name))
        +' '+PNetAddrToStr6(curif.ifa_addr)
        +' '+PNetAddrToStr6(curif.ifa_netmask)
        +' '+EncodeFamilyFlags(family,curif.ifa_flags);
       end;
       else begin
        if HasFlags(Mode,nim_others) then
        Line:=ValidateName(curif.ifa_name)
        +' '+ReadMacAddr(ValidateName(curif.ifa_name))
        +' '+PNetAddrToStr(nil)
        +' '+PNetAddrToStr(nil)
        +' '+EncodeFamilyFlags(family,curif.ifa_flags);
       end;
      end;
     end;
     if (Line<>'') then Lines.Add(Line);
     curif:=curif.ifa_next;
    end;
    // Format table nice
    if HasFlags(Mode,nim_nice) then
    if (Lines.Count>0) then begin
     n1:=0; n2:=0; n3:=0; n4:=0;
     for i:=0 to Lines.Count-1 do begin
      Line:=Lines[i];
      n1:=Max(n1,Length(ExtractWord(1,Line,ScanSpaces)));
      n2:=Max(n2,Length(ExtractWord(2,Line,ScanSpaces)));
      n3:=Max(n3,Length(ExtractWord(3,Line,ScanSpaces)));
      n4:=Max(n4,Length(ExtractWord(4,Line,ScanSpaces)));
     end;
     for i:=0 to Lines.Count-1 do begin
      Line:=Lines[i];
      Line:=Pad(ExtractWord(1,Line,ScanSpaces),n1)
      +'  '+Pad(ExtractWord(2,Line,ScanSpaces),n2)
      +'  '+Pad(ExtractWord(3,Line,ScanSpaces),n3)
      +'  '+Pad(ExtractWord(4,Line,ScanSpaces),n4)
      +'  '+Trim(SkipWords(4,Line,ScanSpaces));
      Lines[i]:=Trim(Line);
     end;
    end;
    if HasFlags(Mode,nim_sort) then Lines.Sort;
    Result:=Lines.Text;
   finally
    Lines.Free;
   end;
  finally
   freeifaddrs(iadr);
  end;
 except
  on E:Exception do BugReport(E,nil,'GetListOfNetworkInterfaces');
 end;
end;

function IPv4BelongsToLocalHostFast(aIPv4Addr:LongString):Boolean;
var iadr,curif:Pifaddrs;
begin
 if (getifaddrs(@iadr)=0) then
 try
  curif:=iadr;
  while Assigned(curif) do begin
   if Assigned(curif.ifa_addr) then
   if (curif.ifa_addr.sa_family=AF_INET) then
   if SameText(aIPv4Addr,NetAddrToStr(curif.ifa_addr.sin_addr))
   then Exit(true);
   curif:=curif.ifa_next;
  end;
 finally
  freeifaddrs(iadr);
 end;
 Exit(false);
end;
{$ENDIF ~UNIX}

{$IFDEF WINDOWS}
function ExtractAdMacAddr(tab:PIP_ADAPTER_ADDRESSES):LongString;
var i,n:Integer;
begin
 Result:='';
 n:=tab.PhysicalAddressLength;
 for i:=0 to n-1 do begin
  if (Result<>'') then Result:=Result+':';
  Result:=Result+Format('%.2x',[tab.PhysicalAddress[i]]);
 end;
 if (Result<>'') then Result:=LowerCase(Result);
 if (n=0) then Result:='00:00:00:00:00:00';
end;

function ExtractAdName(tab:PIP_ADAPTER_ADDRESSES):LongString;
begin
 Result:='';
 Result:=Trim(WideToStr(tab.FriendlyName));
 if IsEmptyStr(Result) then Result:=Trim(StrPas(tab.AdapterName));
 if (Result<>'') and HasChars(Result,JustBlanks) then begin
  Result:=StringReplace(Result,ASCII_HT,' ',[rfReplaceAll]);
  Result:=StringReplace(Result,' ','_',[rfReplaceAll]);
 end;
end;

function EncodeFlags(adtyp,iptyp:LongString):LongString;
const IfTypes='1=OTHER;6=ETHERNET;9=TOKENRING;23=PPP;24=LOOPBACK;37=ATM;71=WIRELESS;131=TUNNEL;144=FIREWIRE;';
var iptype:Integer;
begin
 Result:=CookieScan(IfTypes,adtyp,csm_default+Ord(';'));
 if IsEmptyStr(Result) then Result:='UNKNOWN';
 iptype:=StrToIntDef(iptyp,0);
 if HasFlags(iptype,MIB_IPADDR_PRIMARY) then Result:=Result+'+PRIMARY';
 if HasFlags(iptype,MIB_IPADDR_DYNAMIC) then Result:=Result+'+DYNAMIC';
 if HasFlags(iptype,MIB_IPADDR_DISCONNECTED) then Result:=Result+'+DISCONNECTED';
 if HasFlags(iptype,MIB_IPADDR_DELETED) then Result:=Result+'+DELETED';
 if HasFlags(iptype,MIB_IPADDR_TRANSIENT) then Result:=Result+'+TRANSIENT';
end;

function PadUtf8(S:LongString; n:Integer):LongString;
begin
 if (utf8_length(S)=0)
 then Result:=Pad(S,n)
 else Result:=Pad(S,n+(Length(S)-utf8_length(S)));
end;

function GetListOfNetworkInterfaces(Mode:Integer=-1):LongString;
var Lines,IndAd,IndIp:TStringList; flags,ipsize,adsize:DWORD;
var Line,adbuf,adind,adnam,admac,adtyp,ipbuf,ipind,ipadr,ipmsk,iptyp:LongString;
var iptab:PMIB_IPADDRTABLE; adtab:PIP_ADAPTER_ADDRESSES; up,loopback:Boolean;
var n1,n2,n3,n4:Integer; i,n,family,len:Integer;
const IF_TYPE_SOFTWARE_LOOPBACK=24;
begin
 Result:='';
 try
  if (Mode=-1) then Mode:=nim_default;
  IndAd:=TStringList.Create;
  IndIp:=TStringList.Create;
  Lines:=TStringList.Create;
  try
   // Adapters table
   family:=AF_UNSPEC; flags:=0;
   adbuf:=StringBuffer(64*KiloByte);
   adtab:=Pointer(adbuf); adsize:=Length(adbuf);
   if (GetAdaptersAddresses(family,flags,nil,adtab,@adsize)=NO_ERROR) then begin
    while Assigned(adtab) do begin
     adind:=IntToStr(adtab.Union.IfIndex);
     adnam:=ExtractAdName(adtab);
     admac:=ExtractAdMacAddr(adtab);
     adtyp:=IntToStr(adtab.IfType);
     up:=(adtab.OperStatus=IfOperStatusUp);
     loopback:=(adtab.IfType=IF_TYPE_SOFTWARE_LOOPBACK);
     // Include only UP status interfaces?
     if up or not HasFlags(Mode,nim_up) then
     // Skip loopback interfaces?
     if not loopback or not HasFlags(Mode,nim_skiploop) then
     IndAd.Values[adind]:=adnam+' '+admac+' '+adtyp;
     adtab:=adtab.Next;
    end;
   end;
   // IP addresses table
   ipbuf:=StringBuffer(64*KiloByte);
   iptab:=Pointer(ipbuf); ipsize:=Length(ipbuf);
   if (GetIpAddrTable(iptab,ipsize,false)=NO_ERROR) then begin
    n:=iptab.dwNumEntries;
    for i:=0 to n-1 do begin
     ipind:=IntToStr(iptab.table[i].dwIndex);
     ipadr:=NetAddrToStr(in_addr(iptab.table[i].dwAddr));
     ipmsk:=NetAddrToStr(in_addr(iptab.table[i].dwMask));
     iptyp:=IntToStr(iptab.table[i].wType);
     IndIp.Values[ipind]:=ipadr+' '+ipmsk+' '+iptyp;
    end;
   end;
   // Compose result table
   for i:=0 to IndIp.Count-1 do
   if (ExtractNameValuePair(IndIp[i],ipind,ipadr)>0) then begin
    adnam:=IndAd.Values[ipind]; if (adnam='') then continue;
    if SameText(ipind,'0') then continue;
    Line:=ExtractWord(1,adnam,ScanSpaces)
     +' '+ExtractWord(2,adnam,ScanSpaces)
     +' '+ExtractWord(1,ipadr,ScanSpaces)
     +' '+ExtractWord(2,ipadr,ScanSpaces)
     +' '+EncodeFlags(ExtractWord(3,adnam,ScanSpaces),
                      ExtractWord(3,ipadr,ScanSpaces));
    Lines.Add(line);
   end;
   // Format table nice
   if HasFlags(Mode,nim_nice) then
   if (Lines.Count>0) then begin
    n1:=0; n2:=0; n3:=0; n4:=0;
    for i:=0 to Lines.Count-1 do begin
     Line:=Lines[i];
     len:=utf8_length(ExtractWord(1,Line,ScanSpaces));
     if (len<=0) then len:=length(ExtractWord(1,Line,ScanSpaces));
     n1:=Max(n1,len);
     n2:=Max(n2,Length(ExtractWord(2,Line,ScanSpaces)));
     n3:=Max(n3,Length(ExtractWord(3,Line,ScanSpaces)));
     n4:=Max(n4,Length(ExtractWord(4,Line,ScanSpaces)));
    end;
    for i:=0 to Lines.Count-1 do begin
     Line:=Lines[i];
     Line:=PadUtf8(ExtractWord(1,Line,ScanSpaces),n1)
     +'  '+Pad(ExtractWord(2,Line,ScanSpaces),n2)
     +'  '+Pad(ExtractWord(3,Line,ScanSpaces),n3)
     +'  '+Pad(ExtractWord(4,Line,ScanSpaces),n4)
     +'  '+Trim(SkipWords(4,Line,ScanSpaces));
     Lines[i]:=Trim(Line);
    end;
   end;
   if HasFlags(Mode,nim_sort) then Lines.Sort;
   Result:=Lines.Text;
  finally
   Lines.Free;
   IndAd.Free;
   IndIp.Free;
   adbuf:='';
   ipbuf:='';
  end;
 except
  on E:Exception do BugReport(E,nil,'GetListOfNetworkInterfaces');
 end;
end;
{$ENDIF ~WINDOWS}

function IPv4BelongsToLocalHost(aIPv4Addr:LongString):Boolean;
var list,ip:LongString; i:Integer;
begin
 Result:=false;
 list:=GetListOfNetworkInterfaces(nim_skipipv6);
 for i:=1 to WordCount(list,EolnDelims) do begin
  ip:=ExtractWord(3,ExtractWord(i,list,EolnDelims),JustBlanks);
  if SameText(ip,aIPv4Addr) then Exit(true);
 end;
end;

///////////////////////////////////////
// Unit initialization and finalization
///////////////////////////////////////

procedure Init_crw_netif;
begin
end;

procedure Free_crw_netif;
begin
end;

initialization

 Init_crw_netif;

finalization

 Free_crw_netif;

end.

//////////////
// END OF FILE
//////////////

