65.9K
CodeProject 正在变化。 阅读更多。
Home

在某台机器上获取活动的 TCP/UDP 连接

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (32投票s)

2003年6月10日

7分钟阅读

viewsIcon

339651

downloadIcon

11563

本文介绍 IP 辅助 API 的主要 TCP/UDP 函数的实现,该 API 用于获取活动连接的信息,包括连接所关联的进程。

Sample Image - netstat.jpg

引言

该库的主要目的是监视 PC 上活动的 UDP/TCP 连接(类似于 netstat 的功能);它主要是 IpHelperApi.dll 的一个包装器,实现了四个通用函数,用于获取 TCP 和 UDP 连接的统计信息(GetUdpStats()/ GetTcpStats()),以及 UDP 和 TCP 连接的表(GetUdpConnexions() / GetTcpConnexions())。

它还实现了 IpHelperApi.dll 的两个未记录函数,这些函数与 GetUdpConnexions/GetTcpConnexions 类似,只是它们会获取与连接关联的 processID;在 Win 32 API 中,它们名为:AllocateAndGetTcpExTableFromStackAllocateAndGetUdpExTableFromStack

包装 IpHlpAPI.dll

该库名为 IPHelper,它只是 .NET 框架 P/Invoke 机制对 IpHelperAPI.dll 的一个包装器。在 IPHlpAPI32.cs 文件中包含了 IPHlpApi.dll 中所有函数和结构的声明;它使用了 System.Runtime.InteropServices 命名空间中的标准属性。

[DllImport("iphlpapi.dll",SetLastError=true)]
public extern static int GetUdpStatistics (ref MIB_UDPSTATS pStats );

SetLastError=true 标志允许我们获取 P/Invoke 机制中引发的任何错误的有关信息,通常错误代码由 API 函数返回,但此代码并不非常直观,因此我包含了一个函数,使用 kernell32.dllFormatMessage 函数来获取错误消息描述。

public static string GetAPIErrorMessageDescription(int ApiErrNumber )
{     
System.Text.StringBuilder sError = new System.Text.StringBuilder(512);
int lErrorMessageLength; 
lErrorMessageLength = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
  (IntPtr)0, ApiErrNumber, 0, sError, sError.Capacity, (IntPtr)0) ;
if(lErrorMessageLength > 0) {
string strgError = sError.ToString(); 
strgError=strgError.Substring(0,strgError.Length-2); 
return strgError+" ("+ApiErrNumber.ToString()+")"; 
} 
return "none"; 
}

此函数仅从系统中获取消息描述,如果找不到描述,则返回“none”。

在 MSDN 文档中,所有 iphlpapi 函数都通过传递的参数填充自己的结构;对于没有数组、指针或嵌套结构的简单结构,很容易正确填充结构,但对于复杂结构,这可能会更困难。

让我们看看如何做到这一点。

从 API 调用中获取结果

简单结构包装

首先,让我们看看如何获取一个简单函数 GetTcpStats() 的结果;此函数用于获取 TCP 连接的各种信息,例如活动连接的数量。

在原始的 C++ 库中,它的声明如下:

typedef struct _MIB_TCPSTATS { //The structure holding the results 
 DWORD dwRtoAlgorithm;
  //.. Other DWORD fields
 DWORD dwNumConns;

}MIB_TCPSTATS, *PMIB_TCPSTATS    
DWORD GetTcpStatistics(PMIB_TCPSTATS pStats);        

此结构非常简单,所有 DWORD 字段在 c# 中都可以替换为 integer。

        
[StructLayout(LayoutKind.Sequential)] 
public struct MIB_TCPSTATS
{ 
 public int dwRtoAlgorithm;
 //.. Other int fields
 public int dwNumConns ; 
}
    
[DllImport("iphlpapi.dll",SetLastError=true)]
public extern static int GetTcpStatistics (ref MIB_TCPSTATS pStats );    

请注意,需要 ref 关键字,因为在 C++ 调用中,函数需要结构的指针作为参数。

因此,在 IPHelper 类中,有一个公共方法可以填充 MIB_TCPSTATS 结构。

public void GetTcpStats()
{
    TcpStats = new MIB_TCPSTATS();
    IPHlpAPI32Wrapper.GetTcpStatistics(ref TcpStats);
}
        

非常直接。

请注意,GetUdpStats() 函数的工作方式完全相同。

更复杂的结构包装

GetTcpTable() 函数获取所有活动 TCP 连接的数组,包括本地端点、远程端点和连接状态;这些结果存储在两个嵌套结构中。

typedef struct _MIB_TCPTABLE { // The top level structure 
//that hold an array of the second structure 
DWORD    dwNumEntries; 
MIB_TCPROW table[ANY_SIZE]; //an array of undefined size
} MIB_TCPTABLE, *PMIB_TCPTABLE;

typedef struct _MIB_TCPROW { //the structure that represent 
//a single row in the tcp table
DWORD dwState;  
DWORD dwLocalAddr;  
DWORD dwLocalPort;  
DWORD dwRemoteAddr;  
DWORD dwRemotePort;
} MIB_TCPROW, *PMIB_TCPROW

DWORD GetTcpTable(PMIB_TCPTABLE pTcpTable,PDWORD pdwSize,BOOL bOrder);

GetTcpTable 函数有三个参数:一个指向将保存结果的结构的指针、pTcpTable 参数指向的缓冲区的大小(请注意,如果缓冲区太小,函数将在输出时将此参数设置为所需缓冲区大小),以及一个 bool 标志,用于指定我们是否希望结果排序。

这里的主要问题是,我们无法直接为这两个结构进行封送处理,因为存在一个大小不确定的数组,但有几种解决方案可以解决这个问题;我选择使用一个简单的通用方法,将第一个参数替换为字节数组。

[DllImport("iphlpapi.dll",SetLastError=true)]
public static extern int GetTcpTable(byte[] pTcpTable, 
   out int pdwSize, bool bOrder);            

然后,我们必须在 IPHelper 类中使用此函数,在 GetTcpConnexion 成员方法中。

首先要做的就是获取所需的缓冲区大小(pTcpTable),以便获取所有结果。这是通过使用一个无效缓冲区调用函数来完成的,该函数将返回所需缓冲区的大小(在 pdwSize 参数中);请注意,这是可能的,因为函数已声明 pdwSize 参数前带有 out 关键字。

public void GetTcpConnections()
{
    int pdwSize = 20000;
    byte[] buffer = new byte[pdwSize]; // Start with 20.000 bytes 
//left for information about tcp table
    int res = IPHlpAPI32Wrapper.GetTcpTable(buffer, out pdwSize, true);
    if (res != NO_ERROR)
    {
        buffer = new byte[pdwSize];
        res = IPHlpAPI32Wrapper.GetTcpTable(buffer, out pdwSize, true);
        if (res != 0)
            return;     // Error. You should handle it
    }
    //.....
}
        

一旦有了正确的缓冲区大小,我们再次调用函数,以正确填充缓冲区,然后我们只需解析字节以获取正确的值。

TcpConnexion = new IpHlpApidotnet.MIB_TCPTABLE();

int nOffset = 0;
// number of entry in the
TcpConnexion.dwNumEntries = Convert.ToInt32(buffer[nOffset]);
nOffset+=4; 
TcpConnexion.table = new MIB_TCPROW[TcpConnexion.dwNumEntries];

for(int i=0; i<TcpConnexion.dwNumEntries;i++)
{
    // state
    int st = Convert.ToInt32(buffer[nOffset]);
    // state in string
    ((MIB_TCPROW)(TcpConnexion.table[i])).StrgState=convert_state(st);
    // state  by ID
    ((MIB_TCPROW)(TcpConnexion.table[i])).iState = st;
    nOffset+=4; 
    // local address
    string LocalAdrr = buffer[nOffset].ToString()+"."+
       buffer[nOffset+1].ToString()+"."+
       buffer[nOffset+2].ToString()+"."+buffer[nOffset+3].ToString();
    nOffset+=4; 
    //local port in decimal
    int LocalPort = (((int)buffer[nOffset])<<8) + 
        (((int)buffer[nOffset+1])) + 
        (((int)buffer[nOffset+2])<<24) + (((int)buffer[nOffset+3])<<16);

    nOffset+=4; 
    // store the remote endpoint
    ((MIB_TCPROW)(TcpConnexion.table[i])).Local = 
        new IPEndPoint(IPAddress.Parse(LocalAdrr),LocalPort);

    // remote address
    string RemoteAdrr = buffer[nOffset].ToString()+"."+
        buffer[nOffset+1].ToString()+"."+
        buffer[nOffset+2].ToString()+"."+buffer[nOffset+3].ToString();
    nOffset+=4; 
    // if the remote address = 0 (0.0.0.0) the remote port is always 0
    // else get the remote port in decimal
    int RemotePort;
    //
    if(RemoteAdrr == "0.0.0.0")
    {
        RemotePort = 0;
    }
    else
    {
        RemotePort = (((int)buffer[nOffset])<<8) + 
            (((int)buffer[nOffset+1])) + 
            (((int)buffer[nOffset+2])<<24) + (((int)buffer[nOffset+3])<<16);
    }
    nOffset+=4; 
    ((MIB_TCPROW)(TcpConnexion.table[i])).Remote = 
            new IPEndPoint(IPAddress.Parse(RemoteAdrr),RemotePort);
}        
        

这样,我们就得到了一个名为 TcpConnexion 的结构,它保存了所有活动连接。

请注意,我们不得不将本地和远程端口从 DWORD (UInt32) 转换为 Int16,这是通过按位运算符完成的。GetUdpConnexion() 的工作方式完全相同。

IpHelperApi 未记录函数

GetTcpConnexion()GetUdpConnexion() 函数很有用,但不能用于控制活动连接,这意味着我们无法关闭任何连接,甚至无法获取有关哪个软件正在使用该连接的更多信息。要能够控制连接,意味着我们必须知道哪个进程与该连接关联,但这两个函数仅在 Windows XP 上可用。

经过数天寻找一种方法来获取与连接关联的 processId,我发现 ipHlpApi 有一些未记录的函数:AllocateAndGetTcpExTableFromStackAllocateAndGetUdpExTableFromStack - 这两个函数的功能与 GetTcpTableGetUdpTable 完全相同,只是它们获取与连接关联的 processID;不要问它们为什么不在 MSDN 上记录,我真的不知道。

以下是使用它们的方法,让我们看看 AllocateAndGetTcpExTableFromStackAllocateAndGetUdpExTableFromStack 工作方式相同)在 C++ 中的声明。

typedef struct _MIB_TCPTABLE_EX
{
DWORD dwNumEntries;
MIB_TCPROW_EX table[ANY_SIZE];
} MIB_TCPTABLE_EX, *PMIB_TCPTABLE_EX;    

typedef struct _MIB_TCPROW_EX{
DWORD dwState; 
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
DWORD dwProcessId;
} MIB_TCPROW_EX, *PMIB_TCPROW_EX;

AllocateAndGetTcpExTableFromStack(PMIB_TCPTABLE_EX*,
   BOOL,HANDLE,DWORD,DWORD);
        

第一个参数是指向保存结果的结构的指针,第二个参数是一个 bool 标志,用于排序结果,第三个参数是指向进程堆的指针,另外两个是我不知道含义的标志。

此函数遵循 GetTcpTable 的模式,结果存储在一个具有大小不确定数组的结构中。我在 C# 中实现此功能使用了与 GetTcpTable 略有不同的解决方案:指针的安全 C# 版本。

不要害怕,这并不可怕 :) 对于不熟悉指针的人来说,让我们(简要地)了解一下它。

指针是一种存储内存地址的数据结构;例如,如果你有一个保存数字“100”的整数,那么指向该整数的指针将存储“100”值的地址,而不是值本身,希望这很清楚。

因此,在 C# 中,在安全上下文中,我们使用 IntPtr 结构。

[DllImport("iphlpapi.dll",SetLastError=true)]
public extern static int AllocateAndGetTcpExTableFromStack(
  ref IntPtr pTable, bool bOrder, IntPtr heap ,int zero,int flags );
        
[DllImport("kernel32" ,SetLastError= true)] // this function is 
// used to get the pointer on the process 
// heap required by AllocateAndGetTcpExTableFromStack
public static extern IntPtr GetProcessHeap(); 
        

为了使用它,我将基本遵循与 GetTcpTable 相同的路径:先调用函数一次以获取连接数(行数)从而获取正确的缓冲区大小,然后再次调用函数以获取正确的结果。

public void GetExTcpConnections()
{
            
    // the size of the MIB_EXTCPROW struct =  6*DWORD
    int rowsize = 24;
    int BufferSize = 100000;
    // allocate a dumb memory space in order to retrieve  nb of connexion
    IntPtr lpTable = Marshal.AllocHGlobal(BufferSize);
    //getting infos
    int res = IPHlpAPI32Wrapper.AllocateAndGetTcpExTableFromStack(
          ref lpTable, true, IPHlpAPI32Wrapper.GetProcessHeap(),0,2);
    if(res!=NO_ERROR)
    {
        Debug.WriteLine(
            "Erreur : "+IPHlpAPI32Wrapper.GetAPIErrorMessageDescription(res)
            +" "+res);
        return; // Error. You should handle it
    }
    int CurrentIndex = 0;
    //get the number of entries in the table
    int NumEntries= (int)Marshal.ReadIntPtr(lpTable);
    lpTable = IntPtr.Zero;
    // free allocated space in memory
    Marshal.FreeHGlobal(lpTable);
    
    //...........
}
        

因此,我们必须将一个 IntPtr 传递给函数。第一件事是定义一个 IntPtr,它指向一个足够大的内存空间来存储所有结果。因此,正如我所说的,我们必须分配一个任意的内存空间来调用函数一次,然后我们可以使用:int NumEntries = (int)Marshal.ReadIntPtr(lpTable); 来获取连接数。这里我们使用 Marshal 类的一个函数来读取指针指向的值;最后,不要忘记释放先前分配的内存以避免内存泄漏。

现在,让我们获取所有数据。

    ///////////////////
    // calculate the real buffer size nb of entrie * 
    // size of the struct for each entrie(24) + the dwNumEntries
    BufferSize = (NumEntries*rowsize)+4;
    // make the struct to hold the resullts
    TcpExConnexions = new IpHlpApidotnet.MIB_EXTCPTABLE();
    // Allocate memory
    lpTable = Marshal.AllocHGlobal(BufferSize);
    res = IPHlpAPI32Wrapper.AllocateAndGetTcpExTableFromStack(
       ref lpTable, true,IPHlpAPI32Wrapper.GetProcessHeap() ,0,2);
    if(res!=NO_ERROR)
    {
        Debug.WriteLine("Erreur : "+
            IPHlpAPI32Wrapper.GetAPIErrorMessageDescription(res)+
            " "+res);
        return; // Error. You should handle it
    }
    // New pointer of iterating throught the data
    IntPtr current = lpTable;
    CurrentIndex = 0;
    // get the (again) the number of entries
    NumEntries = (int)Marshal.ReadIntPtr(current);
    TcpExConnexions.dwNumEntries =     NumEntries;
    // Make the array of entries
    TcpExConnexions.table = new MIB_EXTCPROW[NumEntries];
    // iterate the pointer of 4 (the size of the DWORD dwNumEntries)
    CurrentIndex+=4;
    current = (IntPtr)((int)current+CurrentIndex);
    // for each entries
    for(int i=0; i< NumEntries;i++)
    {
        
        // The state of the connexion (in string)
        TcpExConnexions.table[i].StrgState = 
           this.convert_state((int)Marshal.ReadIntPtr(current));
        // The state of the connexion (in ID)
        TcpExConnexions.table[i].iState = (int)Marshal.ReadIntPtr(current);
        // iterate the pointer of 4
        current = (IntPtr)((int)current+4);
        // get the local address of the connexion
        UInt32 localAddr = (UInt32)Marshal.ReadIntPtr(current);
        // iterate the pointer of 4
        current = (IntPtr)((int)current+4);
        // get the local port of the connexion
        UInt32 localPort = (UInt32)Marshal.ReadIntPtr(current);
        // iterate the pointer of 4
        current = (IntPtr)((int)current+4);
        // Store the local endpoint in the struct and 
        // convert the port in decimal (ie convert_Port())
        TcpExConnexions.table[i].Local = new IPEndPoint(localAddr,
              (int)convert_Port(localPort));
        // get the remote address of the connexion
        UInt32 RemoteAddr = (UInt32)Marshal.ReadIntPtr(current);
        // iterate the pointer of 4
        current = (IntPtr)((int)current+4);
        UInt32 RemotePort=0;
        // if the remote address = 0 (0.0.0.0) the remote port is always 0
        // else get the remote port
        if(RemoteAddr!=0)
        {
            RemotePort = (UInt32)Marshal.ReadIntPtr(current);
            RemotePort=convert_Port(RemotePort);
        }
        current = (IntPtr)((int)current+4);
        // store the remote endpoint in the struct  and 
        // convert the port in decimal (ie convert_Port())
        TcpExConnexions.table[i].Remote = new IPEndPoint(
               RemoteAddr,(int)RemotePort);
        // store the process ID
        TcpExConnexions.table[i].dwProcessId = 
               (int)Marshal.ReadIntPtr(current);
        // Store and get the process name in the struct
        TcpExConnexions.table[i].ProcessName = 
             this.get_process_name(TcpExConnexions.table[i].dwProcessId);
        current = (IntPtr)((int)current+4);

    }
    // free the buffer
    Marshal.FreeHGlobal(lpTable);
    // re init the pointer
    current = IntPtr.Zero;

        

因此,我们再次使用正确的缓冲区大小调用函数,然后我们将使用分配内存的开头作为起始地址在内存中“导航”。

前 4 个字节是条目数,然后我们进入一个循环,对连接表中的每一行进行处理,该行从起始地址 + 4 开始;在第一个循环中,我们将采用相同的机制:获取每个值,其顺序与 MIB_TCPROW_EX 中的相同,对于每个值,将指针移动 4 个字节,执行此操作的次数等于行数。

好了,我们已经获得了所有连接以及连接附带的进程 ID。我添加了一些辅助函数,例如,通过给定进程 ID 来获取进程名称,但它们非常简单且自明。

请记住,这两个函数仅在 WinXP 上可用。

如何使用库

我编写了一个小应用程序来测试此库,它只是在一个列表中显示库中所有函数的结果。

以下是库的方法:

  • GetTcpStats() 填充 MIB_TCPSTATS 结构。
  • GetUdpStats() 填充 MIB_UDPSTATS 结构。
  • GetTcpConnexions() 填充 MIB_TCPTABLE 结构。
  • GetUdpConnexions() 填充 MIB_UDPTABLE 结构。
  • GetExTcpConnexions() 填充 MIB_EXTCPTABLE 结构。
  • GetExUdpConnexions() 填充 MIB_EXUDPTABLE 结构。

就是这样,希望它会有用。

© . All rights reserved.