使用 C# UPnP 进行 NAT 穿透






4.89/5 (42投票s)
使用 C# UPnP 进行 NAT 穿透,无需任何库。
引言
在编写网络代码时,NAT 可能是一个真正的问题。对于大多数应用程序来说,要求用户向路由器添加端口转发规则不是一个好主意,甚至不可接受,因为用户通常不允许更改路由器的设置,或者不知道如何更改。
幸运的是,有一些方法可以自动化添加端口转发规则的过程。UPnP 是其中一种方法,据我所知,这是最常用的方法。
网络程序的另一个主要问题是,它们似乎没有可靠的方法来查找运行它的计算机的外部 IP 地址。UPnP 部分解决了这个问题,但当然,它并不完全可靠(并非所有路由器都支持它,而支持它的路由器可能出于安全原因关闭了 UPnP)。
如果您曾经尝试使用现有的 UPnP 库,您可能会发现它们大多不起作用,或者没有涵盖 UPnP 的 NAT 方面。这促使我编写了自己的、非常小的 UPnP 库,它只涵盖 NAT 方面,甚至不完全涵盖——只包括 UPnP 设备发现、基本端口转发和检索外部 IP 地址。然而,对于许多网络程序来说,这就足够了。
使用代码
在源代码中,您会找到一个名为 NAT
的公共类,其中包含以下公共方法:
static bool Discover()
static IPAddress GetExternalIP()
static void ForwardPort(int port, ProtocolType protocol, string description)
static void DeleteForwardingRule(int port, ProtocolType protocol)
请注意,这些方法只进行有限的错误检查,如果不存在 UPnP 设备,或者第一个 UPnP 设备对命令语法过于挑剔,则会失败。在 GetExternalIP
或 ForwardPort
工作之前,必须调用 Discover
。Discover
默认最多需要 3 秒(之后会超时——您可以更改超时值),如果设置的超时值很高,可能需要几分钟,因此对于任何 GUI 应用程序,建议将其放在单独的线程上调用。
要最简单地转发端口,代码如下即可:
UPnP.NAT.Discover();
UPnP.NAT.ForwardPort(12345, ProtocolType.Tcp, "MyApp (TCP)");
实际上,最好检查异常——以及 Discover
返回的值。
UPnP + NAT
对于那些想自己动手的人,或者对此感兴趣的人,以下是我对 UPnP 与 NAT 路由器结合使用的发现。
第一步是找到您感兴趣的 UPnP 设备。显然,最好的方法是使用 SSDP。
使用 SSDP 查找 UPnP 设备归结为通过 UDP 广播一个数据包并检查回复。数据包看起来像这样(或者至少,当它看起来像这样时,它就会起作用)
"M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST:upnp:rootdevice\r\n" +
"MAN:\"ssdp:discover\"\r\n" +
"MX:3\r\n\r\n";
以 ASCII 格式。虽然 Host 是 239.255.255.250,但数据包仍然必须广播到标准的 IPAddress.Broadcast
地址。
检查回复相当直接。您应该只收到一个,但如果您收到了更多,那么正确的回复是可以识别的。来自 NAT 设备的标准回复如下所示:
HTTP/1.1 200 OK
ST:upnp:rootdevice
USN:uuid:00-0C-F6-12-95-D3-FE7BA8C00::upnp:rootdevice
Location:http://192.168.123.254:80/desc.xml
Cache-Control:max-age=1800
Server:IGD-HTTP/1.1 UPnP/1.0 UPnP-Device-Host/1.0
Ext:
ST 应该是 "upnp:rootdevice";如果不是,则不是正确的设备,它本不应该回复,但我们永远不知道网络中会发生什么。
无论如何,我们对 location 感兴趣,因为在该位置会有一个 XML 文件,其中包含有关设备的信息——我们需要这个来找出如何向设备发送命令。
通过一种称为 "service" 的方式,可以向 UPnP 设备发送命令。描述设备的 XML 文件很长,因此只需知道我们感兴趣的服务的 XPath 是
"//tns:service[tns:serviceType=\"urn:schemas-upnp-
org:service:WANIPConnection:1\"]/tns:controlURL/text()"
由于它使用了命名空间,您必须使用 XmlNamespaceManager
,但这又是另一个话题了。
此查询的结果应大致为 "/serv3.xml",再次是一个 XML 文件。请注意,这是一个相对位置,因此您需要将其附加到基本 URL,在本例中为 "http://192.168.123.254:80"。
此文件具有双重目的:首先,其内容告诉您它接受哪些类型的命令以及它将如何回复它们,其次,您将向其发送 SOAP POST 请求。是的,又是 XML。
在此之前,我从未对 XML 文件进行过 HTTP POST 请求,这让我非常惊讶,但显然,它的工作方式应该是这样的。
发送命令就是以正确的方式使用 SOAP。然而,这并不直接——如果您正在使用 WebRequest
(就像我一样),那么您不仅要将 Method 设置为 "POST",还要添加一个 header,并更改 content-type
Headers.Add("SOAPACTION",
"\"urn:schemas-upnp-org:service:WANIPConnection:1#" +
function + "\"");
ContentType = "text/xml; charset=\"utf-8\"";
这里的 function
变量必须与您在内容中发送的函数相同。
这就是全部。有了这些信息,您应该能够实现自己的 UPnP NAT 穿透方法。
关注点
在编写这个小型库的代码时,我完全被该主题缺乏清晰信息所震惊。大多数网站只处理 UPnP 的媒体等方面,或者解释 UPnP NAT 穿透缺乏安全性,而不深入探讨如何实际执行它。当有此类信息时,通常是以我至少理解一半的方式编写的。
最终,我使用 WireShark 检查了 µTorrent 的 UPnP 流量,并逆向工程了该过程。我希望本文能为一些人改变现状,使他们不必进行任何逆向工程即可实现 UPnP NAT 穿透。
历史
- 2008 年 7 月 18 日:编写了 UPnP NAT 库的第一个版本。
- 2008 年 7 月 22 日:编写了本文的第一个版本。
- 2008 年 7 月 22 日(当天晚些时候):更新了源代码和文章,使其能与一些更严格的路由器以及端口不是 80 的情况一起工作(感谢 ajiau)。
- 2008 年 7 月 23 日:更新了源代码,使其能与一些额外的路由器一起工作(感谢 Chris Harper)
- 2009 年 1 月 14 日:更新了源代码和文章,以(希望)能与更多额外的路由器一起工作,感谢过去半年中所有发布反馈的人。