aboutsummaryrefslogtreecommitdiff
path: root/Tools/Hazel-Networking/Hazel/UPnP
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/Hazel-Networking/Hazel/UPnP')
-rw-r--r--Tools/Hazel-Networking/Hazel/UPnP/ILogger.cs65
-rw-r--r--Tools/Hazel-Networking/Hazel/UPnP/NetUtility.cs158
-rw-r--r--Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs347
3 files changed, 570 insertions, 0 deletions
diff --git a/Tools/Hazel-Networking/Hazel/UPnP/ILogger.cs b/Tools/Hazel-Networking/Hazel/UPnP/ILogger.cs
new file mode 100644
index 0000000..3c7abcf
--- /dev/null
+++ b/Tools/Hazel-Networking/Hazel/UPnP/ILogger.cs
@@ -0,0 +1,65 @@
+using System;
+
+namespace Hazel
+{
+ public interface ILogger
+ {
+ void WriteVerbose(string msg);
+ void WriteError(string msg);
+ void WriteWarning(string msg);
+ void WriteInfo(string msg);
+ }
+
+ public class NullLogger : ILogger
+ {
+ public static readonly NullLogger Instance = new NullLogger();
+
+ public void WriteVerbose(string msg)
+ {
+ }
+
+ public void WriteError(string msg)
+ {
+ }
+
+ public void WriteWarning(string msg)
+ {
+ }
+
+ public void WriteInfo(string msg)
+ {
+ }
+ }
+
+ public class ConsoleLogger : ILogger
+ {
+ private bool verbose;
+ public ConsoleLogger(bool verbose)
+ {
+ this.verbose = verbose;
+ }
+
+ public void WriteVerbose(string msg)
+ {
+ if (this.verbose)
+ {
+ Console.WriteLine($"{DateTime.Now} [VERBOSE] {msg}");
+ }
+ }
+
+ public void WriteWarning(string msg)
+ {
+ Console.WriteLine($"{DateTime.Now} [WARN] {msg}");
+ }
+
+ public void WriteError(string msg)
+ {
+ Console.WriteLine($"{DateTime.Now} [ERROR] {msg}");
+ }
+
+ public void WriteInfo(string msg)
+ {
+ Console.WriteLine($"{DateTime.Now} [INFO] {msg}");
+ }
+ }
+}
diff --git a/Tools/Hazel-Networking/Hazel/UPnP/NetUtility.cs b/Tools/Hazel-Networking/Hazel/UPnP/NetUtility.cs
new file mode 100644
index 0000000..d856823
--- /dev/null
+++ b/Tools/Hazel-Networking/Hazel/UPnP/NetUtility.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+
+namespace Hazel.UPnP
+{
+ internal class NetUtility
+ {
+ private static IList<NetworkInterface> GetValidNetworkInterfaces()
+ {
+ var nics = NetworkInterface.GetAllNetworkInterfaces();
+ if (nics == null || nics.Length < 1)
+ return new NetworkInterface[0];
+
+ var validInterfaces = new List<NetworkInterface>(nics.Length);
+
+ NetworkInterface best = null;
+ foreach (NetworkInterface adapter in nics)
+ {
+ if (adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback || adapter.NetworkInterfaceType == NetworkInterfaceType.Unknown)
+ continue;
+ if (!adapter.Supports(NetworkInterfaceComponent.IPv4) && !adapter.Supports(NetworkInterfaceComponent.IPv6))
+ continue;
+ if (best == null)
+ best = adapter;
+ if (adapter.OperationalStatus != OperationalStatus.Up)
+ continue;
+
+ // make sure this adapter has any ip addresses
+ IPInterfaceProperties properties = adapter.GetIPProperties();
+ foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses)
+ {
+ if (unicastAddress != null && unicastAddress.Address != null)
+ {
+ // Yes it does, add this network interface.
+ validInterfaces.Add(adapter);
+ break;
+ }
+ }
+ }
+
+ if (validInterfaces.Count == 0 && best != null)
+ validInterfaces.Add(best);
+
+ return validInterfaces;
+ }
+
+ /// <summary>
+ /// Gets the addresses from all active network interfaces, but at most one per interface.
+ /// </summary>
+ /// <param name="addressFamily">The <see cref="AddressFamily"/> of the addresses to return</param>
+ /// <returns>An <see cref="ICollection{T}"/> of <see cref="UnicastIPAddressInformation"/>.</returns>
+ public static ICollection<UnicastIPAddressInformation> GetAddressesFromNetworkInterfaces(AddressFamily addressFamily)
+ {
+ var unicastAddresses = new List<UnicastIPAddressInformation>();
+
+ foreach (NetworkInterface adapter in GetValidNetworkInterfaces())
+ {
+ IPInterfaceProperties properties = adapter.GetIPProperties();
+ foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses)
+ {
+ if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == addressFamily)
+ {
+ unicastAddresses.Add(unicastAddress);
+ break;
+ }
+ }
+ }
+
+ return unicastAddresses;
+ }
+
+ /// <summary>
+ /// Gets my local IPv4 address (not necessarily external) and subnet mask
+ /// </summary>
+ public static IPAddress GetMyAddress(out IPAddress mask)
+ {
+ var networkInterfaces = GetValidNetworkInterfaces();
+ IPInterfaceProperties properties = null;
+
+ if (networkInterfaces.Count > 0)
+ properties = networkInterfaces[0]?.GetIPProperties();
+
+ if (properties != null)
+ {
+ foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses)
+ {
+ if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork)
+ {
+ mask = unicastAddress.IPv4Mask;
+ return unicastAddress.Address;
+ }
+ }
+ }
+
+ mask = null;
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the broadcast address for the first network interface or, if not able to,
+ /// the limited broadcast address.
+ /// </summary>
+ /// <returns>An <see cref="IPAddress"/> for broadcasting.</returns>
+ public static IPAddress GetBroadcastAddress()
+ {
+ var networkInterfaces = GetValidNetworkInterfaces();
+ IPInterfaceProperties properties = null;
+
+ if (networkInterfaces.Count > 0)
+ properties = networkInterfaces[0]?.GetIPProperties();
+
+ if (properties != null)
+ {
+ foreach (UnicastIPAddressInformation unicastAddress in properties.UnicastAddresses)
+ {
+ IPAddress ipAddress = GetBroadcastAddress(unicastAddress);
+ if (ipAddress != null)
+ {
+ return ipAddress;
+ }
+ }
+ }
+
+ return IPAddress.Broadcast;
+ }
+
+ /// <summary>
+ /// Gets the broadcast address for the given <paramref name="unicastAddress"/>.
+ /// </summary>
+ /// <param name="unicastAddress">A <see cref="UnicastIPAddressInformation"/></param>
+ /// <returns>An <see cref="IPAddress"/> for broadcasting, null if the <paramref name="unicastAddress"/>
+ /// is not an IPv4 address.</returns>
+ public static IPAddress GetBroadcastAddress(UnicastIPAddressInformation unicastAddress)
+ {
+ if (unicastAddress != null && unicastAddress.Address != null && unicastAddress.Address.AddressFamily == AddressFamily.InterNetwork)
+ {
+ var mask = unicastAddress.IPv4Mask;
+ byte[] ipAdressBytes = unicastAddress.Address.GetAddressBytes();
+ byte[] subnetMaskBytes = mask.GetAddressBytes();
+
+ if (ipAdressBytes.Length != subnetMaskBytes.Length)
+ throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
+
+ byte[] broadcastAddress = new byte[ipAdressBytes.Length];
+ for (int i = 0; i < broadcastAddress.Length; i++)
+ {
+ broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255));
+ }
+ return new IPAddress(broadcastAddress);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs b/Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs
new file mode 100644
index 0000000..771709e
--- /dev/null
+++ b/Tools/Hazel-Networking/Hazel/UPnP/UPnPHelper.cs
@@ -0,0 +1,347 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Hazel.UPnP
+{
+ /// <summary>
+ /// Status of the UPnP capabilities
+ /// </summary>
+ public enum UPnPStatus
+ {
+ /// <summary>
+ /// Still discovering UPnP capabilities
+ /// </summary>
+ Discovering,
+
+ /// <summary>
+ /// UPnP is not available
+ /// </summary>
+ NotAvailable,
+
+ /// <summary>
+ /// UPnP is available and ready to use
+ /// </summary>
+ Available
+ }
+
+ public class UPnPHelper : IDisposable
+ {
+ private const int DiscoveryTimeOutMs = 1000;
+
+ private string serviceUrl;
+ private string serviceName = "";
+
+ private ManualResetEvent discoveryComplete = new ManualResetEvent(false);
+ private Socket socket;
+
+ private DateTime discoveryResponseDeadline;
+
+ private EndPoint ep;
+ private byte[] buffer;
+
+ private ILogger logger;
+
+ /// <summary>
+ /// Status of the UPnP capabilities of this NetPeer
+ /// </summary>
+ public UPnPStatus Status { get; private set; }
+
+ public UPnPHelper(ILogger logger)
+ {
+ this.logger = logger;
+
+ this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ this.socket.EnableBroadcast = true;
+ this.socket.MulticastLoopback = false;
+
+ this.socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
+ this.socket.Bind(new IPEndPoint(IPAddress.Any, 0));
+
+ this.ep = new IPEndPoint(IPAddress.Any, 1900);
+ this.buffer = new byte[ushort.MaxValue];
+
+ ListenForUPnP();
+
+ this.Discover();
+ }
+
+ private void ListenForUPnP()
+ {
+ try
+ {
+ socket.BeginReceiveFrom(this.buffer, 0, this.buffer.Length, SocketFlags.None, ref ep, HandleMessage, null);
+ }
+ catch(Exception e)
+ {
+ this.logger.WriteInfo("Exception listening for UPnP: " + e.Message);
+ }
+ }
+
+ private void HandleMessage(IAsyncResult ar)
+ {
+ int len;
+ try
+ {
+ len = this.socket.EndReceiveFrom(ar, ref ep);
+ }
+ catch
+ {
+ return;
+ }
+
+ string resp = System.Text.Encoding.UTF8.GetString(buffer, 0, len);
+ if (resp.Contains("upnp:rootdevice") || resp.Contains("UPnP/1.0"))
+ {
+ var locationStart = resp.IndexOf("location:", StringComparison.OrdinalIgnoreCase);
+ if (locationStart >= 0)
+ {
+ locationStart += 10;
+ var locationEnd = resp.IndexOf("\r", locationStart);
+
+ resp = resp.Substring(locationStart, locationEnd - locationStart);
+ if (!ExtractServiceUrl(resp))
+ {
+ ListenForUPnP();
+ }
+ }
+ else
+ {
+ ListenForUPnP();
+ }
+ }
+ else
+ {
+ ListenForUPnP();
+ }
+ }
+
+ internal void Discover()
+ {
+ 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";
+
+ discoveryResponseDeadline = DateTime.UtcNow.AddSeconds(6);
+ Status = UPnPStatus.Discovering;
+
+ byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str);
+
+ this.logger.WriteInfo("Attempting UPnP discovery");
+
+ socket.SendTo(buffer, new IPEndPoint(NetUtility.GetBroadcastAddress(), 1900));
+ }
+
+ internal bool ExtractServiceUrl(string resp)
+ {
+ try
+ {
+ XmlDocument desc = new XmlDocument();
+ using (var response = WebRequest.Create(resp).GetResponse())
+ {
+ desc.Load(response.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 false;
+
+ serviceName = "WANIPConnection";
+ XmlNode node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:" + serviceName + ":1\"]/tns:controlURL/text()", nsMgr);
+ if (node == null)
+ {
+ //try another service name
+ serviceName = "WANPPPConnection";
+ node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:" + serviceName + ":1\"]/tns:controlURL/text()", nsMgr);
+ if (node == null)
+ return false;
+ }
+
+ serviceUrl = CombineUrls(resp, node.Value);
+ this.logger.WriteInfo("UPnP service ready");
+ Status = UPnPStatus.Available;
+ discoveryComplete.Set();
+ return true;
+ }
+ catch (Exception e)
+ {
+ this.logger.WriteError("Exception while parsing UPnP Service URL: " + e.Message);
+ return false;
+ }
+ }
+
+ 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 >= 0)
+ {
+ gatewayURL = gatewayURL.Substring(0, n); // Use first portion of URL
+ }
+
+ return "http://" + gatewayURL + subURL;
+ }
+
+ private bool CheckAvailability()
+ {
+ switch (Status)
+ {
+ case UPnPStatus.NotAvailable:
+ return false;
+ case UPnPStatus.Available:
+ return true;
+ case UPnPStatus.Discovering:
+ while (!discoveryComplete.WaitOne(DiscoveryTimeOutMs))
+ {
+ if (DateTime.UtcNow > discoveryResponseDeadline)
+ {
+ Status = UPnPStatus.NotAvailable;
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Add a forwarding rule to the router using UPnP
+ /// </summary>
+ /// <param name="externalPort">The external, WAN facing, port</param>
+ /// <param name="description">A description for the port forwarding rule</param>
+ /// <param name="internalPort">The port on the client machine to send traffic to</param>
+ /// <param name="durationSeconds">The lease duration on the port forwarding rule, in seconds. 0 for indefinite.</param>
+ public bool ForwardPort(int externalPort, string description, int internalPort = 0, int durationSeconds = 0)
+ {
+ if (!CheckAvailability())
+ return false;
+
+ if (internalPort == 0)
+ internalPort = externalPort;
+
+ try
+ {
+ var client = NetUtility.GetMyAddress(out _);
+ if (client == null)
+ return false;
+
+ SOAPRequest(serviceUrl,
+ $"<u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:{serviceName}:1\">" +
+ "<NewRemoteHost></NewRemoteHost>" +
+ $"<NewExternalPort>{externalPort}</NewExternalPort>" +
+ "<NewProtocol>UDP</NewProtocol>" +
+ $"<NewInternalPort>{internalPort}</NewInternalPort>" +
+ $"<NewInternalClient>{client}</NewInternalClient>" +
+ "<NewEnabled>1</NewEnabled>" +
+ $"<NewPortMappingDescription>{description}</NewPortMappingDescription>" +
+ $"<NewLeaseDuration>{durationSeconds}</NewLeaseDuration>" +
+ "</u:AddPortMapping>",
+ "AddPortMapping");
+
+ this.logger.WriteInfo("Sent UPnP port forward request.");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ this.logger.WriteError("UPnP port forward failed: " + ex.Message);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Delete a forwarding rule from the router using UPnP
+ /// </summary>
+ /// <param name="externalPort">The external, 'internet facing', port</param>
+ public bool DeleteForwardingRule(int externalPort)
+ {
+ if (!CheckAvailability())
+ return false;
+
+ try
+ {
+ SOAPRequest(serviceUrl,
+ $"<u:DeletePortMapping xmlns:u=\"urn:schemas-upnp-org:service:{serviceName}:1\">" +
+ "<NewRemoteHost></NewRemoteHost>" +
+ $"<NewExternalPort>{externalPort}</NewExternalPort>" +
+ $"<NewProtocol>UDP</NewProtocol>" +
+ "</u:DeletePortMapping>", "DeletePortMapping");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ // m_peer.LogWarning("UPnP delete forwarding rule failed: " + ex.Message);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Retrieve the extern ip using UPnP
+ /// </summary>
+ public IPAddress GetExternalIP()
+ {
+ if (!CheckAvailability())
+ return null;
+ try
+ {
+ XmlDocument xdoc = SOAPRequest(serviceUrl, "<u:GetExternalIPAddress xmlns:u=\"urn:schemas-upnp-org:service:" + serviceName + ":1\">" +
+ "</u:GetExternalIPAddress>", "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 =
+"<?xml version=\"1.0\"?>" +
+"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
+$"<s:Body>{soap}</s:Body>" +
+"</s:Envelope>";
+
+ WebRequest r = HttpWebRequest.Create(url);
+ r.Headers.Add("SOAPACTION", $"\"urn:schemas-upnp-org:service:{serviceName}:1#{function}\"");
+ r.ContentType = "text/xml; charset=\"utf-8\"";
+ r.Method = "POST";
+
+ byte[] b = System.Text.Encoding.UTF8.GetBytes(req);
+ r.ContentLength = b.Length;
+ r.GetRequestStream().Write(b, 0, b.Length);
+
+ using (WebResponse wres = r.GetResponse())
+ {
+ XmlDocument resp = new XmlDocument();
+ Stream ress = wres.GetResponseStream();
+ resp.Load(ress);
+ return resp;
+ }
+ }
+
+ public void Dispose()
+ {
+ this.discoveryComplete.Dispose();
+ try { this.socket.Shutdown(SocketShutdown.Both); } catch { }
+ this.socket.Dispose();
+ }
+ }
+} \ No newline at end of file