From 713d91ac585de727a1682a88fe21370df818353c Mon Sep 17 00:00:00 2001 From: lidgren Date: Wed, 27 Apr 2011 20:52:29 +0000 Subject: [PATCH] UPnP support added: get external ip, add forwarding rule and delete forwarding rule --- Lidgren.Network/Lidgren.Network.csproj | 1 + Lidgren.Network/NetPeer.Internal.cs | 233 ++++++++++--------- Lidgren.Network/NetPeer.LatencySimulation.cs | 2 + Lidgren.Network/NetPeer.cs | 23 +- Lidgren.Network/NetPeerConfiguration.cs | 15 ++ Lidgren.Network/NetUPnP.cs | 186 +++++++++++++++ UnitTests/Program.cs | 5 +- 7 files changed, 353 insertions(+), 112 deletions(-) create mode 100644 Lidgren.Network/NetUPnP.cs diff --git a/Lidgren.Network/Lidgren.Network.csproj b/Lidgren.Network/Lidgren.Network.csproj index d310e1f..86029e9 100644 --- a/Lidgren.Network/Lidgren.Network.csproj +++ b/Lidgren.Network/Lidgren.Network.csproj @@ -118,6 +118,7 @@ + diff --git a/Lidgren.Network/NetPeer.Internal.cs b/Lidgren.Network/NetPeer.Internal.cs index aff472d..b6ca9b3 100644 --- a/Lidgren.Network/NetPeer.Internal.cs +++ b/Lidgren.Network/NetPeer.Internal.cs @@ -22,6 +22,7 @@ namespace Lidgren.Network private object m_initializeLock = new object(); private uint m_frameCounter; private double m_lastHeartbeat; + private NetUPnP m_upnp; internal readonly NetPeerConfiguration m_configuration; private readonly NetQueue m_releasedIncomingMessages; @@ -63,6 +64,9 @@ namespace Lidgren.Network if (m_status == NetPeerStatus.Running) return; + if (m_configuration.m_enableUPnP) + m_upnp = new NetUPnP(this); + InitializePools(); m_releasedIncomingMessages.Clear(); @@ -288,126 +292,147 @@ namespace Lidgren.Network //if (m_socket == null || m_socket.Available < 1) // return; - int bytesReceived = 0; - try + do { - bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); - } - catch (SocketException sx) - { - if (sx.SocketErrorCode == SocketError.ConnectionReset) - { - // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" - // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! - // So, what to do? - return; - } - - LogWarning(sx.ToString()); - return; - } - - if (bytesReceived < NetConstants.HeaderByteSize) - return; - - //LogVerbose("Received " + bytesReceived + " bytes"); - - IPEndPoint ipsender = (IPEndPoint)m_senderRemote; - - NetConnection sender = null; - m_connectionLookup.TryGetValue(ipsender, out sender); - - double receiveTime = NetTime.Now; - // - // parse packet into messages - // - int numMessages = 0; - int ptr = 0; - while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) - { - // decode header - // 8 bits - NetMessageType - // 1 bit - Fragment? - // 15 bits - Sequence number - // 16 bits - Payload length in bits - - numMessages++; - - NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; - - byte low = m_receiveBuffer[ptr++]; - byte high = m_receiveBuffer[ptr++]; - - bool isFragment = ((low & 1) == 1); - ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); - - ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); - int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); - - if (bytesReceived - ptr < payloadByteLength) - { - LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); - return; - } - + int bytesReceived = 0; try { - NetException.Assert(tp < NetMessageType.Unused1 || tp > NetMessageType.Unused29); - - if (tp >= NetMessageType.LibraryError) + bytesReceived = m_socket.ReceiveFrom(m_receiveBuffer, 0, m_receiveBuffer.Length, SocketFlags.None, ref m_senderRemote); + } + catch (SocketException sx) + { + if (sx.SocketErrorCode == SocketError.ConnectionReset) { - if (sender != null) - sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); - else - ReceivedUnconnectedLibraryMessage(receiveTime, ipsender, tp, ptr, payloadByteLength); + // connection reset by peer, aka connection forcibly closed aka "ICMP port unreachable" + // we should shut down the connection; but m_senderRemote seemingly cannot be trusted, so which connection should we shut down?! + // So, what to do? + return; } - else - { - if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) - return; // dropping unconnected message since it's not enabled - NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); - msg.m_isFragment = isFragment; - msg.m_receiveTime = receiveTime; - msg.m_sequenceNumber = sequenceNumber; - msg.m_receivedMessageType = tp; - msg.m_senderConnection = sender; - msg.m_senderEndpoint = ipsender; - msg.m_bitLength = payloadBitLength; - Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); - if (sender != null) + LogWarning(sx.ToString()); + return; + } + + if (bytesReceived < NetConstants.HeaderByteSize) + return; + + //LogVerbose("Received " + bytesReceived + " bytes"); + + IPEndPoint ipsender = (IPEndPoint)m_senderRemote; + + if (ipsender.Port == 1900) + { + // UPnP response + try + { + string resp = System.Text.Encoding.ASCII.GetString(m_receiveBuffer, 0, bytesReceived); + if (resp.Contains("upnp:rootdevice")) { - if (tp == NetMessageType.Unconnected) + resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9); + resp = resp.Substring(0, resp.IndexOf("\r")).Trim(); + m_upnp.ExtractServiceUrl(resp); + return; + } + } + catch { } + } + + NetConnection sender = null; + m_connectionLookup.TryGetValue(ipsender, out sender); + + double receiveTime = NetTime.Now; + // + // parse packet into messages + // + int numMessages = 0; + int ptr = 0; + while ((bytesReceived - ptr) >= NetConstants.HeaderByteSize) + { + // decode header + // 8 bits - NetMessageType + // 1 bit - Fragment? + // 15 bits - Sequence number + // 16 bits - Payload length in bits + + numMessages++; + + NetMessageType tp = (NetMessageType)m_receiveBuffer[ptr++]; + + byte low = m_receiveBuffer[ptr++]; + byte high = m_receiveBuffer[ptr++]; + + bool isFragment = ((low & 1) == 1); + ushort sequenceNumber = (ushort)((low >> 1) | (((int)high) << 7)); + + ushort payloadBitLength = (ushort)(m_receiveBuffer[ptr++] | (m_receiveBuffer[ptr++] << 8)); + int payloadByteLength = NetUtility.BytesToHoldBits(payloadBitLength); + + if (bytesReceived - ptr < payloadByteLength) + { + LogWarning("Malformed packet; stated payload length " + payloadByteLength + ", remaining bytes " + (bytesReceived - ptr)); + return; + } + + try + { + NetException.Assert(tp < NetMessageType.Unused1 || tp > NetMessageType.Unused29); + + if (tp >= NetMessageType.LibraryError) + { + if (sender != null) + sender.ReceivedLibraryMessage(tp, ptr, payloadByteLength); + else + ReceivedUnconnectedLibraryMessage(receiveTime, ipsender, tp, ptr, payloadByteLength); + } + else + { + if (sender == null && !m_configuration.IsMessageTypeEnabled(NetIncomingMessageType.UnconnectedData)) + return; // dropping unconnected message since it's not enabled + + NetIncomingMessage msg = CreateIncomingMessage(NetIncomingMessageType.Data, payloadByteLength); + msg.m_isFragment = isFragment; + msg.m_receiveTime = receiveTime; + msg.m_sequenceNumber = sequenceNumber; + msg.m_receivedMessageType = tp; + msg.m_senderConnection = sender; + msg.m_senderEndpoint = ipsender; + msg.m_bitLength = payloadBitLength; + Buffer.BlockCopy(m_receiveBuffer, ptr, msg.m_data, 0, payloadByteLength); + if (sender != null) { - // We're connected; but we can still send unconnected messages to this peer - msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; - ReleaseMessage(msg); + if (tp == NetMessageType.Unconnected) + { + // We're connected; but we can still send unconnected messages to this peer + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); + } + else + { + // connected application (non-library) message + sender.ReceivedMessage(msg); + } } else { - // connected application (non-library) message - sender.ReceivedMessage(msg); + // at this point we know the message type is enabled + // unconnected application (non-library) message + msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; + ReleaseMessage(msg); } } - else - { - // at this point we know the message type is enabled - // unconnected application (non-library) message - msg.m_incomingMessageType = NetIncomingMessageType.UnconnectedData; - ReleaseMessage(msg); - } } + catch (Exception ex) + { + LogError("Packet parsing error: " + ex.Message + " from " + ipsender); + } + ptr += payloadByteLength; } - catch (Exception ex) - { - LogError("Packet parsing error: " + ex.Message + " from " + ipsender); - } - ptr += payloadByteLength; - } - m_statistics.PacketReceived(bytesReceived, numMessages); - if (sender != null) - sender.m_statistics.PacketReceived(bytesReceived, numMessages); + m_statistics.PacketReceived(bytesReceived, numMessages); + if (sender != null) + sender.m_statistics.PacketReceived(bytesReceived, numMessages); + + } while (m_socket.Available > 0); } private void ReceivedUnconnectedLibraryMessage(double now, IPEndPoint senderEndpoint, NetMessageType tp, int ptr, int payloadByteLength) diff --git a/Lidgren.Network/NetPeer.LatencySimulation.cs b/Lidgren.Network/NetPeer.LatencySimulation.cs index 7d7e520..ae15197 100644 --- a/Lidgren.Network/NetPeer.LatencySimulation.cs +++ b/Lidgren.Network/NetPeer.LatencySimulation.cs @@ -115,6 +115,7 @@ namespace Lidgren.Network connectionReset = false; try { + // TODO: refactor this check outta here if (target.Address == IPAddress.Broadcast) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); @@ -230,6 +231,7 @@ namespace Lidgren.Network connectionReset = false; try { + // TODO: refactor this check outta here if (target.Address == IPAddress.Broadcast) m_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); diff --git a/Lidgren.Network/NetPeer.cs b/Lidgren.Network/NetPeer.cs index e1ef915..210ce74 100644 --- a/Lidgren.Network/NetPeer.cs +++ b/Lidgren.Network/NetPeer.cs @@ -43,6 +43,11 @@ namespace Lidgren.Network /// public int Port { get { return m_listenPort; } } + /// + /// Returns an UPnP object if enabled in the NetPeerConfiguration + /// + public NetUPnP UPnP { get { return m_upnp; } } + /// /// Gets or sets the application defined object containing data about the peer /// @@ -99,7 +104,7 @@ namespace Lidgren.Network m_handshakes = new Dictionary(); m_senderRemote = (EndPoint)new IPEndPoint(IPAddress.Any, 0); m_status = NetPeerStatus.NotRunning; - m_receivedFragmentGroups = new Dictionary>(); + m_receivedFragmentGroups = new Dictionary>(); } /// @@ -131,8 +136,12 @@ namespace Lidgren.Network m_networkThread.IsBackground = true; m_networkThread.Start(); - // allow some time for network thread to start up in case they call Connect() immediately - Thread.Sleep(10); + // send upnp discovery + if (m_upnp != null) + m_upnp.Discover(this); + + // allow some time for network thread to start up in case they call Connect() or UPnP calls immediately + Thread.Sleep(50); } /// @@ -258,18 +267,20 @@ namespace Lidgren.Network } } -#if DEBUG /// /// Send raw bytes; only used for debugging /// +#if DEBUG public void RawSend(byte[] arr, int offset, int length, IPEndPoint destination) - { +#else + internal void RawSend(byte[] arr, int offset, int length, IPEndPoint destination) +#endif + { // wrong thread - this miiiight crash with network thread... but what's a boy to do. Array.Copy(arr, offset, m_sendBuffer, 0, length); bool unused; SendPacket(length, destination, 1, out unused); } -#endif /// /// Disconnects all active connections and closes the socket diff --git a/Lidgren.Network/NetPeerConfiguration.cs b/Lidgren.Network/NetPeerConfiguration.cs index b90bbab..524948c 100644 --- a/Lidgren.Network/NetPeerConfiguration.cs +++ b/Lidgren.Network/NetPeerConfiguration.cs @@ -39,6 +39,7 @@ namespace Lidgren.Network internal float m_pingInterval; internal bool m_useMessageRecycling; internal float m_connectionTimeout; + internal bool m_enableUPnP; internal NetIncomingMessageType m_disabledTypes; internal int m_port; @@ -243,6 +244,20 @@ namespace Lidgren.Network } } + /// + /// Enables UPnP support; enabling port forwarding and getting external ip + /// + public bool EnableUPnP + { + get { return m_enableUPnP; } + set + { + if (m_isLocked) + throw new NetException(c_isLockedMessage); + m_enableUPnP = value; + } + } + /// /// Gets or sets the local ip address to bind to. Defaults to IPAddress.Any. Cannot be changed once NetPeer is initialized. /// diff --git a/Lidgren.Network/NetUPnP.cs b/Lidgren.Network/NetUPnP.cs new file mode 100644 index 0000000..94467eb --- /dev/null +++ b/Lidgren.Network/NetUPnP.cs @@ -0,0 +1,186 @@ +using System; +using System.IO; +using System.Xml; +using System.Net; +using System.Net.Sockets; + +namespace Lidgren.Network +{ + /// + /// UPnP support class + /// + public class NetUPnP + { + private string m_serviceUrl; + private NetPeer m_peer; + + public NetUPnP(NetPeer peer) + { + m_peer = peer; + } + + internal void Discover(NetPeer peer) + { + string str = +"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"; + + byte[] arr = System.Text.Encoding.ASCII.GetBytes(str); + + peer.Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + peer.RawSend(arr, 0, arr.Length, new IPEndPoint(IPAddress.Broadcast, 1900)); + peer.Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, false); + + // allow some extra time for router to respond + System.Threading.Thread.Sleep(50); + } + + internal void ExtractServiceUrl(string resp) + { +#if !DEBUG + try + { +#endif + XmlDocument desc = new XmlDocument(); + desc.Load(WebRequest.Create(resp).GetResponse().GetResponseStream()); + XmlNamespaceManager nsMgr = new XmlNamespaceManager(desc.NameTable); + nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0"); + XmlNode typen = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr); + if (!typen.Value.Contains("InternetGatewayDevice")) + return; + XmlNode node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:WANIPConnection:1\"]/tns:controlURL/text()", nsMgr); + if (node == null) + return; + m_serviceUrl = CombineUrls(resp, node.Value); + m_peer.LogDebug("UPnP service ready"); + System.Threading.Thread.Sleep(50); +#if !DEBUG + } + catch { return; } +#endif + } + + private static string CombineUrls(string gatewayURL, string subURL) + { + // Is Control URL an absolute URL? + if ((subURL.Contains("http:")) || (subURL.Contains("."))) + return subURL; + + gatewayURL = gatewayURL.Replace("http://", ""); // strip any protocol + int n = gatewayURL.IndexOf("/"); + if (n != -1) + gatewayURL = gatewayURL.Substring(0, n); // Use first portion of URL + return "http://" + gatewayURL + subURL; + } + + /// + /// Add a forwarding rule to the router using UPnP + /// + public bool ForwardPort(int port, string description) + { + if (m_serviceUrl == null) + return false; + + IPAddress mask; + var client = NetUtility.GetMyAddress(out mask); + + try + { + XmlDocument xdoc = SOAPRequest(m_serviceUrl, + "" + + "" + port.ToString() + "" + + "" + ProtocolType.Udp.ToString().ToUpper() + "" + + "" + port.ToString() + "" + + "" + client.ToString() + "" + + "1" + + "" + description + "" + + "0" + + "", + "AddPortMapping"); + + m_peer.LogDebug("Sent UPnP port forward request"); + System.Threading.Thread.Sleep(50); + } + catch (Exception ex) + { + m_peer.LogWarning("UPnP port forward failed: " + ex.Message); + return false; + } + return true; + } + + /// + /// Delete a forwarding rule from the router using UPnP + /// + public bool DeleteForwardingRule(int port) + { + if (m_serviceUrl == null) + return false; + try + { + XmlDocument xdoc = SOAPRequest(m_serviceUrl, + "" + + "" + + "" + + "" + port + "" + + "" + ProtocolType.Udp.ToString().ToUpper() + "" + + "", "DeletePortMapping"); + return true; + } + catch (Exception ex) + { + m_peer.LogWarning("UPnP delete forwarding rule failed: " + ex.Message); + return false; + } + } + + /// + /// Retrieve the extern ip using UPnP + /// + public IPAddress GetExternalIP() + { + if (m_serviceUrl == null) + return null; + + try + { + XmlDocument xdoc = SOAPRequest(m_serviceUrl, "" + + "", "GetExternalIPAddress"); + XmlNamespaceManager nsMgr = new XmlNamespaceManager(xdoc.NameTable); + nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0"); + string IP = xdoc.SelectSingleNode("//NewExternalIPAddress/text()", nsMgr).Value; + return IPAddress.Parse(IP); + } + catch (Exception ex) + { + m_peer.LogWarning("Failed to get external IP: " + ex.Message); + return null; + } + } + + private XmlDocument SOAPRequest(string url, string soap, string function) + { + string req = "" + + "" + + "" + + soap + + "" + + ""; + WebRequest r = HttpWebRequest.Create(url); + r.Method = "POST"; + byte[] b = System.Text.Encoding.UTF8.GetBytes(req); + r.Headers.Add("SOAPACTION", "\"urn:schemas-upnp-org:service:WANIPConnection:1#" + function + "\""); + r.ContentType = "text/xml; charset=\"utf-8\""; + r.ContentLength = b.Length; + r.GetRequestStream().Write(b, 0, b.Length); + XmlDocument resp = new XmlDocument(); + WebResponse wres = r.GetResponse(); + Stream ress = wres.GetResponseStream(); + resp.Load(ress); + return resp; + } + } +} \ No newline at end of file diff --git a/UnitTests/Program.cs b/UnitTests/Program.cs index 93621ee..5070f88 100644 --- a/UnitTests/Program.cs +++ b/UnitTests/Program.cs @@ -1,6 +1,8 @@ using System; using System.Reflection; using Lidgren.Network; +using System.Net; +using System.Net.Sockets; namespace UnitTests { @@ -9,11 +11,10 @@ namespace UnitTests static void Main(string[] args) { NetPeerConfiguration config = new NetPeerConfiguration("unittests"); + config.EnableUPnP = true; NetPeer peer = new NetPeer(config); peer.Start(); // needed for initialization - System.Threading.Thread.Sleep(50); - Console.WriteLine("Unique identifier is " + NetUtility.ToHexString(peer.UniqueIdentifier)); ReadWriteTests.Run(peer);