在C#中监听ICMP数据包

8
我有一个SIP应用程序需要发送UDP数据包以建立SIP呼叫。SIP具有超时机制来处理传递失败。我想要做的另一件事是检测UDP套接字是否关闭,以便无需等待SIP使用的32秒重传间隔。

我所指的情况是当尝试发送到UDP套接字时,远程主机生成ICMP目标不可达数据包。如果我尝试向一个正在运行但端口未监听的主机发送UDP数据包,我可以看到ICMP消息返回并带有数据包跟踪器,但问题是如何从我的C#代码中访问它?

我正在玩弄原始套接字,但迄今为止尚未能够使ICMP数据包被我的程序接收。即使ICMP消息在我的PC上到达,下面的示例也永远不会接收到数据包。

Socket icmpListener = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp);
icmpListener.Bind(new IPEndPoint(IPAddress.Any, 0));

byte[] buffer = new byte[4096];
EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
int bytesRead = icmpListener.ReceiveFrom(buffer, ref remoteEndPoint);
logger.Debug("ICMPListener received " + bytesRead + " from " + remoteEndPoint.ToString());

以下是一个Wireshark跟踪,显示了来自尝试从10.0.0.100(我的电脑)发送UDP数据包到10.0.0.138(我的路由器)上我知道它没有在监听的端口的ICMP响应。我的问题是如何利用这些ICMP数据包来实现UDP发送失败,而不仅仅是等待应用程序在任意时间后超时?
7个回答

21
近3年后,我偶然发现了http://www.codeproject.com/Articles/17031/A-Network-Sniffer-in-C,它为我提供了足够的提示,帮助我找到在Windows 7上接收ICMP数据包的解决方案(不确定Vista是否适用,原问题是关于Vista的,但我认为这个解决方案会起作用)。
两个关键点是:套接字必须绑定到一个特定的IP地址而不是IPAddress.Any,并且使用IOControl调用来设置SIO_RCVALL标志。
Socket icmpListener = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp);
icmpListener.Bind(new IPEndPoint(IPAddress.Parse("10.1.1.2"), 0));
icmpListener.IOControl(IOControlCode.ReceiveAll, new byte[] { 1, 0, 0, 0 }, new byte[] { 1, 0, 0, 0 });

byte[] buffer = new byte[4096];
EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
int bytesRead = icmpListener.ReceiveFrom(buffer, ref remoteEndPoint);
Console.WriteLine("ICMPListener received " + bytesRead + " from " + remoteEndPoint);
Console.ReadLine();

我还需要设置防火墙规则,允许接收ICMP端口不可达数据包。

netsh advfirewall firewall add rule name="All ICMP v4" dir=in action=allow protocol=icmpv4:any,any

2
我不知道这是否有帮助,但是我一直使用Socket.BeginReceiveFrom,而有时它会失败,出现SocketException的SocketError.ConnectionReset(10054)。在UDP协议下,这表示套接字收到了ICMP端口不可达消息。 - SilverX
1
无论你如何从套接字接收,都会抛出相同的异常。我一直遇到的问题是异常没有告诉你ICMP响应来自哪个远程主机。这就是ICMP监听器可以做的事情。 - sipsorcery
10
在三年之后仍然关心并提供解决方案,晚到了一个加1! - Mathieu Guindon
尽管我努力尝试,但我仍然无法在Win10上使其正常工作(多年后)。ICMP数据包从未返回到套接字,并且很可能被过滤掉了。经过整整一天的尝试,我还没有找到可行的解决方案。 - Michael Brown
尽管我尝试了很多次,但我仍然无法在Win10上使其正常工作(多年后)。ICMP数据包从未返回到套接字,很可能被过滤掉了。经过整整一天的尝试,我仍然没有找到可行的解决方案。 - undefined

4

更新:我觉得我快疯了……你发的那段代码也可以在我的电脑(XP SP3)上运行……

以下这段代码对我来说没问题(XP SP3):

using System;
using System.Net;
using System.Net.Sockets;

namespace icmp_capture
{
    class Program
    {
        static void Main(string[] args)
        {            
            IPEndPoint ipMyEndPoint = new IPEndPoint(IPAddress.Any, 0);
            EndPoint myEndPoint = (ipMyEndPoint);
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp);            
            socket.Bind(myEndPoint);
            while (true)
            {

                /*                
                //SEND SOME BS (you will get a nice infinite loop if you uncomment this)
                var udpClient = new UdpClient("192.168.2.199", 666);   //**host must exist if it's in the same subnet (if not routed)**              
                Byte[] messagebyte = Encoding.Default.GetBytes("hi".ToCharArray());                
                int s = udpClient.Send(messagebyte, messagebyte.Length);
                */

                Byte[] ReceiveBuffer = new Byte[256];
                var nBytes = socket.ReceiveFrom(ReceiveBuffer, 256, 0, ref myEndPoint);
                if (ReceiveBuffer[20] == 3)// ICMP type = Delivery failed
                {
                    Console.WriteLine("Delivery failed");
                    Console.WriteLine("Returned by: " + myEndPoint.ToString());
                    Console.WriteLine("Destination: " + ReceiveBuffer[44] + "." + ReceiveBuffer[45] + "." + ReceiveBuffer[46] + "." + ReceiveBuffer[47]);
                    Console.WriteLine("---------------");
                }
                else {
                    Console.WriteLine("Some (not delivery failed) ICMP packet ignored");
                }
            }

        }
    }
}

当你说工作时,你是否意味着如果你发送ping或其他东西,你会收到ICMP数据包?我相当确定当我在对不可达端口进行UDP发送并且无法接收ICMP时,我正在使用这个。我需要再次检查。 - sipsorcery
我只测试了不可到达的主机和TCP连接... 我会尝试端口。 - Tarnay Kálmán
是的...使用 UDP 和现有主机非常正常...我添加了那个被注释掉的部分进行测试。 - Tarnay Kálmán
我也可以确认这点。我使用的代码和Kalmi的样例在XP和2k3上可用,但在Vista上不行。我的应用程序是为2k3准备的,所以这是个好消息! - sipsorcery

3
Icmp正在使用一个标识符,每个icmp“会话”(对于每个icmp套接字)似乎都不同。因此,对于未由相同套接字发送的icmp数据包的回复将被有用地过滤掉。这就是为什么那段代码不起作用的原因。(我不确定这一点。这只是在查看一些ICMP流量后的假设。) 您可以简单地ping主机并查看是否可以到达它,然后尝试您的SIP事情。但是,如果其他主机过滤icmp,则无法工作。 一个丑陋(但有效)的解决方案是使用winpcap。(将其作为唯一的工作解决方案似乎太糟糕了。) 我的意思是使用winpcap,您可以捕获ICMP流量,然后查看捕获的数据包是否与您的UDP数据包无法传递有关。 这里有一个捕获tcp数据包的示例: http://www.tamirgal.com/home/SourceView.aspx?Item=SharpPcap&File=Example6.DumpTCP.cs (使用ICMP应该不难做到同样的事情。)

1
我同意。如果没有其他选择,那是个好办法。我只是觉得还有另外一种方式。实际上,当接收到 ICMP 消息时,UDP 发送应该会抛出异常。 - sipsorcery
UDP是无状态的,所以我认为它不应该。 - Tarnay Kálmán
点赞以确保正确的人获得悬赏,而不是这篇帖子值得它。 - Joshua

3

只需使用连接的UDP套接字,操作系统将匹配ICMP不可达消息并在UDP套接字中返回错误。

搜索连接的UDP套接字。


UDP客户端会因为ICMP端口不可达而抛出套接字异常,但不会因为ICMP主机不可达或任何其他ICMP“不可达”消息而抛出异常。 - trampster

2
所以您想以编程方式获取不可达的返回icmp数据包吗?这很困难。我认为在接近它之前,网络堆栈会吞噬它。
我不认为纯C#方法在这里有效。您需要使用驱动程序级拦截来获得钩子。看看这个应用程序,它使用Windows的ipfiltdrv.sys来捕获数据包(icmp、tcp、udp等),并使用托管代码(c#)读取/处理它们。 http://www.codeproject.com/KB/IP/firewall_sniffer.aspx?display=Print - Oisin

2
在网上有很多帖子提到Vista操作系统中ICMP端口不可达数据包无法访问的问题。 堆栈应该在接收到ICMP时返回异常。但至少在Vista上它没有这样做。因此,您正在尝试解决问题的方法是绕过它。
我不喜欢那些说这是不可能的答案,但似乎确实如此。所以我建议您回到原始问题,即SIP中的长时间超时。
- 您可以让用户配置超时时间(因此基本上符合规范)。 - 在超时结束之前,您可以开始执行其他操作(例如检查其他代理)。 - 您可以缓存已知的错误目标(但这需要对缓存进行良好的管理)。 - 如果icmp和udp没有给出正确的错误消息,请尝试tcp或另一种协议。只是为了引出所需的信息。
(任何事情都有可能,只是可能需要大量资源。)

1

我写这篇独立的答案,因为细节与我之前写的完全不同。

所以基于Kalmi的评论关于会话ID,让我想到了为什么我可以在同一台机器上打开两个ping程序,而响应不会交叉。它们都是ICMP,因此都使用无端口原始套接字。这意味着IP堆栈中的某些内容必须知道这些响应的套接字是用于什么目的的。对于ping来说,事实证明在ICMP包的数据中使用了一个ID作为ECHO REQUEST和ECHO REPLY的一部分。

然后我在维基百科上看到了这条评论ICMP

尽管ICMP消息包含在标准IP数据报中,但ICMP消息通常被处理为特殊情况,与正常IP处理区别开来,而不是作为IP的正常子协议进行处理。在许多情况下,有必要检查ICMP消息的内容并将适当的错误消息传递给生成原始IP数据包的应用程序,即促使发送ICMP消息的那个应用程序。

这是在这里间接阐述的:

互联网头部加上原始数据报的前64位数据。主机使用此数据将消息与适当的进程匹配。如果更高级别的协议使用端口号,则假定它们位于原始数据报的前64个数据位中。

由于您正在使用使用端口的UDP,因此网络堆栈可能会将ICMP消息路由回原始套接字。这就是为什么您的新套接字从未接收到这些消息的原因。我想UDP会吞掉ICMP消息。

如果我正确,解决此问题的一种方法是打开一个原始套接字并手动创建您的UDP数据包,监听任何返回的内容,并根据需要处理UDP和ICMP消息。我不确定代码会是什么样子,但我认为这不会太困难,并且可能被认为比winpcap解决方案更“优雅”。

此外,这个链接 http://www.networksorcery.com/enp/default1003.htm 似乎是一个很好的低层网络协议资源。
希望这能有所帮助。

你提到的 ICMP 会话 ID 的观点对我来说非常有道理。这实际上是问题的关键,为什么 ICMP 消息没有指示无法传递的 UDP 数据包被传递到我的应用程序?由于某种原因,Windows 似乎无法将它们与该应用程序匹配,因为它们是针对 UDP 数据包的。 - sipsorcery
1
我曾考虑过尝试在同一个原始套接字上多路复用UDP和ICMP,但除了需要进行所有UDP处理之外,当您使用Windows创建原始套接字时,您必须选择它是IP还是ICMP,不能两者兼备。 - sipsorcery
是的,我认为你需要将它变成一个原始IP套接字,并处理所有ICMP消息。这可能比它值得的要复杂,但根据我所能阅读到的有关IP和ICMP的信息,这可能是唯一的非WinPcap风格的答案。 - grieve
澄清一下:我认为您的应用程序正在接收 ICMP 消息,但 UDP 栈正在抑制它。当然,这只是猜测,因为我没有看到您如何创建和使用原始 UDP 消息的套接字。 - grieve

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接