Bruce Wayne пре 6 година
родитељ
комит
ddcbb1d68b

+ 9 - 0
NatTypeTester-Console/NatTypeTester_Console.csproj

@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
+    <RootNamespace>NatTypeTester_Console</RootNamespace>
+  </PropertyGroup>
+
+</Project>

+ 155 - 0
NatTypeTester-Console/Net/NetUtils.cs

@@ -0,0 +1,155 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+
+namespace NatTypeTester_Console.Net
+{
+	public static class NetUtils
+	{
+		#region static method CompareArray
+
+		/// <summary>
+		/// Compares if specified array items equals.
+		/// </summary>
+		/// <param name="array1">Array 1.</param>
+		/// <param name="array2">Array 2</param>
+		/// <returns>Returns true if both arrays are equal.</returns>
+		public static bool CompareArray(Array array1, Array array2)
+		{
+			return CompareArray(array1, array2, array2.Length);
+		}
+
+		/// <summary>
+		/// Compares if specified array items equals.
+		/// </summary>
+		/// <param name="array1">Array 1.</param>
+		/// <param name="array2">Array 2</param>
+		/// <param name="array2Count">Number of bytes in array 2 used for compare.</param>
+		/// <returns>Returns true if both arrays are equal.</returns>
+		public static bool CompareArray(Array array1, Array array2, int array2Count)
+		{
+			if (array1 == null && array2 == null)
+			{
+				return true;
+			}
+			if (array1 == null)
+			{
+				return false;
+			}
+			if (array2 == null)
+			{
+				return false;
+			}
+			if (array1.Length != array2Count)
+			{
+				return false;
+			}
+
+			for (var i = 0; i < array1.Length; i++)
+			{
+				if (!array1.GetValue(i).Equals(array2.GetValue(i)))
+				{
+					return false;
+				}
+			}
+
+			return true;
+		}
+
+		#endregion
+
+		#region static method IsPrivateIP
+
+		/// <summary>
+		/// Gets if specified IP address is private LAN IP address. For example 192.168.x.x is private ip.
+		/// </summary>
+		/// <param name="ip">IP address to check.</param>
+		/// <returns>Returns true if IP is private IP.</returns>
+		/// <exception cref="ArgumentNullException">Is raised when <b>ip</b> is null reference.</exception>
+		public static bool IsPrivateIP(IPAddress ip)
+		{
+			if (ip == null)
+			{
+				throw new ArgumentNullException(nameof(ip));
+			}
+
+			if (ip.AddressFamily == AddressFamily.InterNetwork)
+			{
+				var ipBytes = ip.GetAddressBytes();
+
+				/* Private IPs:
+					First Octet = 192 AND Second Octet = 168 (Example: 192.168.X.X) 
+					First Octet = 172 AND (Second Octet >= 16 AND Second Octet <= 31) (Example: 172.16.X.X - 172.31.X.X)
+					First Octet = 10 (Example: 10.X.X.X)
+					First Octet = 169 AND Second Octet = 254 (Example: 169.254.X.X)
+
+				*/
+
+				if (ipBytes[0] == 192 && ipBytes[1] == 168)
+				{
+					return true;
+				}
+				if (ipBytes[0] == 172 && ipBytes[1] >= 16 && ipBytes[1] <= 31)
+				{
+					return true;
+				}
+				if (ipBytes[0] == 10)
+				{
+					return true;
+				}
+				if (ipBytes[0] == 169 && ipBytes[1] == 254)
+				{
+					return true;
+				}
+			}
+
+			return false;
+		}
+
+		#endregion
+
+		#region static method CreateSocket
+
+		/// <summary>
+		/// Creates new socket for the specified end point.
+		/// </summary>
+		/// <param name="localEP">Local end point.</param>
+		/// <param name="protocolType">Protocol type.</param>
+		/// <returns>Return newly created socket.</returns>
+		/// <exception cref="ArgumentNullException">Is raised when <b>localEP</b> is null reference.</exception>
+		public static Socket CreateSocket(IPEndPoint localEP, ProtocolType protocolType)
+		{
+			if (localEP == null)
+			{
+				throw new ArgumentNullException(nameof(localEP));
+			}
+
+			var socketType = SocketType.Stream;
+			if (protocolType == ProtocolType.Udp)
+			{
+				socketType = SocketType.Dgram;
+			}
+
+			if (localEP.AddressFamily == AddressFamily.InterNetwork)
+			{
+				var socket = new Socket(AddressFamily.InterNetwork, socketType, protocolType);
+				socket.Bind(localEP);
+
+				return socket;
+			}
+
+			if (localEP.AddressFamily == AddressFamily.InterNetworkV6)
+			{
+				var socket = new Socket(AddressFamily.InterNetworkV6, socketType, protocolType);
+				socket.Bind(localEP);
+
+				return socket;
+			}
+
+			throw new ArgumentException(@"Invalid IPEndPoint address family.");
+		}
+
+		#endregion
+
+	}
+}

+ 55 - 0
NatTypeTester-Console/Net/STUN/Client/NatType.cs

@@ -0,0 +1,55 @@
+namespace NatTypeTester_Console.Net.STUN.Client
+{
+	/// <summary>
+	/// Specifies UDP network type.
+	/// </summary>
+	public enum NatType
+	{
+		/// <summary>
+		/// UDP is always blocked.
+		/// </summary>
+		UdpBlocked,
+
+		/// <summary>
+		/// No NAT, public IP, no firewall.
+		/// </summary>
+		OpenInternet,
+
+		/// <summary>
+		/// No NAT, public IP, but symmetric UDP firewall.
+		/// </summary>
+		SymmetricUdpFirewall,
+
+		/// <summary>
+		/// A full cone NAT is one where all requests from the same internal IP address and port are 
+		/// mapped to the same external IP address and port. Furthermore, any external host can send 
+		/// a packet to the internal host, by sending a packet to the mapped external address.
+		/// </summary>
+		FullCone,
+
+		/// <summary>
+		/// A restricted cone NAT is one where all requests from the same internal IP address and 
+		/// port are mapped to the same external IP address and port. Unlike a full cone NAT, an external
+		/// host (with IP address X) can send a packet to the internal host only if the internal host 
+		/// had previously sent a packet to IP address X.
+		/// </summary>
+		RestrictedCone,
+
+		/// <summary>
+		/// A port restricted cone NAT is like a restricted cone NAT, but the restriction 
+		/// includes port numbers. Specifically, an external host can send a packet, with source IP
+		/// address X and source port P, to the internal host only if the internal host had previously 
+		/// sent a packet to IP address X and port P.
+		/// </summary>
+		PortRestrictedCone,
+
+		/// <summary>
+		/// A symmetric NAT is one where all requests from the same internal IP address and port, 
+		/// to a specific destination IP address and port, are mapped to the same external IP address and
+		/// port.  If the same host sends a packet with the same source address and port, but to 
+		/// a different destination, a different mapping is used. Furthermore, only the external host that
+		/// receives a packet can send a UDP packet back to the internal host.
+		/// </summary>
+		Symmetric
+	}
+}

+ 427 - 0
NatTypeTester-Console/Net/STUN/Client/StunClient.cs

@@ -0,0 +1,427 @@
+using NatTypeTester_Console.Net.STUN.Message;
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+
+namespace NatTypeTester_Console.Net.STUN.Client
+{
+	/// <summary>
+	/// This class implements STUN client. Defined in RFC 3489.
+	/// </summary>
+	/// <example>
+	/// <code>
+	/// // Create new socket for STUN client.
+	/// Socket socket = new Socket(AddressFamily.InterNetwork,SocketType.Dgram,ProtocolType.Udp);
+	/// socket.Bind(new IPEndPoint(IPAddress.Any,0));
+	/// 
+	/// // Query STUN server
+	/// STUN_Result result = STUN_Client.Query("stun.ekiga.net",3478,socket);
+	/// if(result.NetType != STUN_NetType.UdpBlocked){
+	///     // UDP blocked or !!!! bad STUN server
+	/// }
+	/// else{
+	///     IPEndPoint publicEP = result.PublicEndPoint;
+	///     // Do your stuff
+	/// }
+	/// </code>
+	/// </example>
+	public static class StunClient
+	{
+		#region static method Query
+
+		/// <summary>
+		/// Gets NAT info from STUN server.
+		/// </summary>
+		/// <param name="host">STUN server name or IP.</param>
+		/// <param name="port">STUN server port. Default port is 3478.</param>
+		/// <param name="localEP">Local IP end point.</param>
+		/// <returns>Returns UDP netwrok info.</returns>
+		/// <exception cref="Exception">Is raised when <b>host</b> or <b>localEP</b> is null reference.</exception>
+		/// <exception cref="ArgumentNullException">Throws exception if unexpected error happens.</exception>
+		public static StunResult Query(string host, int port, IPEndPoint localEP)
+		{
+			if (host == null)
+			{
+				throw new ArgumentNullException("host");
+			}
+			if (localEP == null)
+			{
+				throw new ArgumentNullException("localEP");
+			}
+
+			using (var s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
+			{
+				s.Bind(localEP);
+
+				return Query(host, port, s);
+			}
+		}
+
+		/// <summary>
+		/// Gets NAT info from STUN server.
+		/// </summary>
+		/// <param name="host">STUN server name or IP.</param>
+		/// <param name="port">STUN server port. Default port is 3478.</param>
+		/// <param name="socket">UDP socket to use.</param>
+		/// <returns>Returns UDP netwrok info.</returns>
+		/// <exception cref="Exception">Throws exception if unexpected error happens.</exception>
+		public static StunResult Query(string host, int port, Socket socket)
+		{
+			if (host == null)
+			{
+				throw new ArgumentNullException("host");
+			}
+			if (socket == null)
+			{
+				throw new ArgumentNullException("socket");
+			}
+			if (port < 1)
+			{
+				throw new ArgumentException("Port value must be >= 1 !");
+			}
+			if (socket.ProtocolType != ProtocolType.Udp)
+			{
+				throw new ArgumentException("Socket must be UDP socket !");
+			}
+
+			var remoteEndPoint = new IPEndPoint(Dns.GetHostAddresses(host)[0], port);
+
+			/*
+                In test I, the client sends a STUN Binding Request to a server, without any flags set in the
+                CHANGE-REQUEST attribute, and without the RESPONSE-ADDRESS attribute. This causes the server 
+                to send the response back to the address and port that the request came from.
+            
+                In test II, the client sends a Binding Request with both the "change IP" and "change port" flags
+                from the CHANGE-REQUEST attribute set.  
+              
+                In test III, the client sends a Binding Request with only the "change port" flag set.
+                          
+                                    +--------+
+                                    |  Test  |
+                                    |   I    |
+                                    +--------+
+                                         |
+                                         |
+                                         V
+                                        /\              /\
+                                     N /  \ Y          /  \ Y             +--------+
+                      UDP     <-------/Resp\--------->/ IP \------------->|  Test  |
+                      Blocked         \ ?  /          \Same/              |   II   |
+                                       \  /            \? /               +--------+
+                                        \/              \/                    |
+                                                         | N                  |
+                                                         |                    V
+                                                         V                    /\
+                                                     +--------+  Sym.      N /  \
+                                                     |  Test  |  UDP    <---/Resp\
+                                                     |   II   |  Firewall   \ ?  /
+                                                     +--------+              \  /
+                                                         |                    \/
+                                                         V                     |Y
+                              /\                         /\                    |
+               Symmetric  N  /  \       +--------+   N  /  \                   V
+                  NAT  <--- / IP \<-----|  Test  |<--- /Resp\               Open
+                            \Same/      |   I    |     \ ?  /               Internet
+                             \? /       +--------+      \  /
+                              \/                         \/
+                              |                           |Y
+                              |                           |
+                              |                           V
+                              |                           Full
+                              |                           Cone
+                              V              /\
+                          +--------+        /  \ Y
+                          |  Test  |------>/Resp\---->Restricted
+                          |   III  |       \ ?  /
+                          +--------+        \  /
+                                             \/
+                                              |N
+                                              |       Port
+                                              +------>Restricted
+
+            */
+
+			try
+			{
+				// Test I
+				var test1 = new StunMessage();
+				test1.Type = StunMessageType.BindingRequest;
+				var test1response = DoTransaction(test1, socket, remoteEndPoint, 1600);
+
+				// UDP blocked.
+				if (test1response == null)
+				{
+					return new StunResult(NatType.UdpBlocked, null);
+				}
+				else
+				{
+					// Test II
+					var test2 = new StunMessage();
+					test2.Type = StunMessageType.BindingRequest;
+					test2.ChangeRequest = new StunChangeRequest(true, true);
+
+					// No NAT.
+					if (socket.LocalEndPoint.Equals(test1response.MappedAddress))
+					{
+						var test2Response = DoTransaction(test2, socket, remoteEndPoint, 1600);
+						// Open Internet.
+						if (test2Response != null)
+						{
+							return new StunResult(NatType.OpenInternet, test1response.MappedAddress);
+						}
+						// Symmetric UDP firewall.
+						else
+						{
+							return new StunResult(NatType.SymmetricUdpFirewall, test1response.MappedAddress);
+						}
+					}
+					// NAT
+					else
+					{
+						var test2Response = DoTransaction(test2, socket, remoteEndPoint, 1600);
+
+						// Full cone NAT.
+						if (test2Response != null)
+						{
+							return new StunResult(NatType.FullCone, test1response.MappedAddress);
+						}
+						else
+						{
+							/*
+                                If no response is received, it performs test I again, but this time, does so to 
+                                the address and port from the CHANGED-ADDRESS attribute from the response to test I.
+                            */
+
+							// Test I(II)
+							var test12 = new StunMessage();
+							test12.Type = StunMessageType.BindingRequest;
+
+							var test12Response = DoTransaction(test12, socket, test1response.ChangedAddress, 1600);
+							if (test12Response == null)
+							{
+								throw new Exception("STUN Test I(II) dind't get resonse !");
+							}
+							else
+							{
+								// Symmetric NAT
+								if (!test12Response.MappedAddress.Equals(test1response.MappedAddress))
+								{
+									return new StunResult(NatType.Symmetric, test1response.MappedAddress);
+								}
+								else
+								{
+									// Test III
+									var test3 = new StunMessage();
+									test3.Type = StunMessageType.BindingRequest;
+									test3.ChangeRequest = new StunChangeRequest(false, true);
+
+									var test3Response = DoTransaction(test3, socket, test1response.ChangedAddress, 1600);
+									// Restricted
+									if (test3Response != null)
+									{
+										return new StunResult(NatType.RestrictedCone, test1response.MappedAddress);
+									}
+									// Port restricted
+									else
+									{
+										return new StunResult(NatType.PortRestrictedCone, test1response.MappedAddress);
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+			finally
+			{
+				// Junk all late responses.
+				var startTime = DateTime.Now;
+				while (startTime.AddMilliseconds(200) > DateTime.Now)
+				{
+					// We got response.
+					if (socket.Poll(1, SelectMode.SelectRead))
+					{
+						var receiveBuffer = new byte[512];
+						socket.Receive(receiveBuffer);
+					}
+				}
+			}
+		}
+
+		#endregion
+
+		#region method GetPublicIP
+
+		/// <summary>
+		/// Resolves local IP to public IP using STUN.
+		/// </summary>
+		/// <param name="stunServer">STUN server.</param>
+		/// <param name="port">STUN server port. Default port is 3478.</param>
+		/// <param name="localIP">Local IP address.</param>
+		/// <returns>Returns public IP address.</returns>
+		/// <exception cref="ArgumentNullException">Is raised when <b>stunServer</b> or <b>localIP</b> is null reference.</exception>
+		/// <exception cref="ArgumentException">Is raised when any of the arguments has invalid value.</exception>
+		/// <exception cref="IOException">Is raised when no connection to STUN server.</exception>
+		public static IPAddress GetPublicIP(string stunServer, int port, IPAddress localIP)
+		{
+			if (stunServer == null)
+			{
+				throw new ArgumentNullException("stunServer");
+			}
+			if (stunServer == "")
+			{
+				throw new ArgumentException("Argument 'stunServer' value must be specified.");
+			}
+			if (port < 1)
+			{
+				throw new ArgumentException("Invalid argument 'port' value.");
+			}
+			if (localIP == null)
+			{
+				throw new ArgumentNullException("localIP");
+			}
+
+			if (!NetUtils.IsPrivateIP(localIP))
+			{
+				return localIP;
+			}
+
+			var result = Query(stunServer, port, NetUtils.CreateSocket(new IPEndPoint(localIP, 0), ProtocolType.Udp));
+			if (result.PublicEndPoint != null)
+			{
+				return result.PublicEndPoint.Address;
+			}
+
+			throw new IOException("Failed to STUN public IP address. STUN server name is invalid or firewall blocks STUN.");
+		}
+
+		#endregion
+
+		#region method GetPublicEP
+
+		/// <summary>
+		/// Resolves socket local end point to public end point.
+		/// </summary>
+		/// <param name="stunServer">STUN server.</param>
+		/// <param name="port">STUN server port. Default port is 3478.</param>
+		/// <param name="socket">UDP socket to use.</param>
+		/// <returns>Returns public IP end point.</returns>
+		/// <exception cref="ArgumentNullException">Is raised when <b>stunServer</b> or <b>socket</b> is null reference.</exception>
+		/// <exception cref="ArgumentException">Is raised when any of the arguments has invalid value.</exception>
+		/// <exception cref="IOException">Is raised when no connection to STUN server.</exception>
+		public static IPEndPoint GetPublicEP(string stunServer, int port, Socket socket)
+		{
+			if (stunServer == null)
+			{
+				throw new ArgumentNullException("stunServer");
+			}
+			if (stunServer == "")
+			{
+				throw new ArgumentException("Argument 'stunServer' value must be specified.");
+			}
+			if (port < 1)
+			{
+				throw new ArgumentException("Invalid argument 'port' value.");
+			}
+			if (socket == null)
+			{
+				throw new ArgumentNullException("socket");
+			}
+			if (socket.ProtocolType != ProtocolType.Udp)
+			{
+				throw new ArgumentException("Socket must be UDP socket !");
+			}
+
+			var remoteEndPoint = new IPEndPoint(Dns.GetHostAddresses(stunServer)[0], port);
+
+			try
+			{
+				// Test I
+				var test1 = new StunMessage();
+				test1.Type = StunMessageType.BindingRequest;
+				var test1response = DoTransaction(test1, socket, remoteEndPoint, 1000);
+
+				// UDP blocked.
+				if (test1response == null)
+				{
+					throw new IOException("Failed to STUN public IP address. STUN server name is invalid or firewall blocks STUN.");
+				}
+
+				return test1response.SourceAddress;
+			}
+			catch
+			{
+				throw new IOException("Failed to STUN public IP address. STUN server name is invalid or firewall blocks STUN.");
+			}
+			finally
+			{
+				// Junk all late responses.
+				var startTime = DateTime.Now;
+				while (startTime.AddMilliseconds(200) > DateTime.Now)
+				{
+					// We got response.
+					if (socket.Poll(1, SelectMode.SelectRead))
+					{
+						var receiveBuffer = new byte[512];
+						socket.Receive(receiveBuffer);
+					}
+				}
+			}
+		}
+
+		#endregion
+
+		#region method DoTransaction
+
+		/// <summary>
+		/// Does STUN transaction. Returns transaction response or null if transaction failed.
+		/// </summary>
+		/// <param name="request">STUN message.</param>
+		/// <param name="socket">Socket to use for send/receive.</param>
+		/// <param name="remoteEndPoint">Remote end point.</param>
+		/// <param name="timeout">Timeout in milli seconds.</param>
+		/// <returns>Returns transaction response or null if transaction failed.</returns>
+		private static StunMessage DoTransaction(StunMessage request, Socket socket, IPEndPoint remoteEndPoint, int timeout)
+		{
+			var requestBytes = request.ToByteData();
+			var startTime = DateTime.Now;
+			// Retransmit with 500 ms.
+			while (startTime.AddMilliseconds(timeout) > DateTime.Now)
+			{
+				try
+				{
+					socket.SendTo(requestBytes, remoteEndPoint);
+
+					// We got response.
+					if (socket.Poll(500 * 1000, SelectMode.SelectRead))
+					{
+						var receiveBuffer = new byte[512];
+						socket.Receive(receiveBuffer);
+
+						// Parse message
+						var response = new StunMessage();
+						response.Parse(receiveBuffer);
+
+						// Check that transaction ID matches or not response what we want.
+						if (NetUtils.CompareArray(request.TransactionId, response.TransactionId))
+						{
+							return response;
+						}
+					}
+				}
+				catch
+				{
+					// ignored
+				}
+			}
+
+			return null;
+		}
+
+		#endregion
+
+		// TODO: Update to RFC 5389
+
+	}
+}

+ 37 - 0
NatTypeTester-Console/Net/STUN/Client/StunResult.cs

@@ -0,0 +1,37 @@
+using System.Net;
+
+namespace NatTypeTester_Console.Net.STUN.Client
+{
+	/// <summary>
+	/// This class holds STUN_Client.Query method return data.
+	/// </summary>
+	public class StunResult
+	{
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		/// <param name="natType">Specifies UDP network type.</param>
+		/// <param name="publicEndPoint">Public IP end point.</param>
+		public StunResult(NatType natType, IPEndPoint publicEndPoint)
+		{
+			NatType = natType;
+			PublicEndPoint = publicEndPoint;
+		}
+
+
+		#region Properties Implementation
+
+		/// <summary>
+		/// Gets UDP network type.
+		/// </summary>
+		public NatType NatType { get; }
+
+		/// <summary>
+		/// Gets public IP end point. This value is null if failed to get network type.
+		/// </summary>
+		public IPEndPoint PublicEndPoint { get; }
+
+		#endregion
+
+	}
+}

+ 55 - 0
NatTypeTester-Console/Net/STUN/Message/StunChangeRequest.cs

@@ -0,0 +1,55 @@
+namespace NatTypeTester_Console.Net.STUN.Message
+{
+	/// <summary>
+	/// This class implements STUN CHANGE-REQUEST attribute. Defined in RFC 3489 11.2.4.
+	/// </summary>
+	public class StunChangeRequest
+	{
+		private bool _mChangeIp = true;
+		private bool _mChangePort = true;
+
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		public StunChangeRequest()
+		{
+		}
+
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		/// <param name="changeIp">Specifies if STUN server must send response to different IP than request was received.</param>
+		/// <param name="changePort">Specifies if STUN server must send response to different port than request was received.</param>
+		public StunChangeRequest(bool changeIp, bool changePort)
+		{
+			_mChangeIp = changeIp;
+			_mChangePort = changePort;
+		}
+
+
+		#region Properties Implementation
+
+		/// <summary>
+		/// Gets or sets if STUN server must send response to different IP than request was received.
+		/// </summary>
+		public bool ChangeIp
+		{
+			get => _mChangeIp;
+
+			set => _mChangeIp = value;
+		}
+
+		/// <summary>
+		/// Gets or sets if STUN server must send response to different port than request was received.
+		/// </summary>
+		public bool ChangePort
+		{
+			get => _mChangePort;
+
+			set => _mChangePort = value;
+		}
+
+		#endregion
+
+	}
+}

+ 35 - 0
NatTypeTester-Console/Net/STUN/Message/StunErrorCode.cs

@@ -0,0 +1,35 @@
+namespace NatTypeTester_Console.Net.STUN.Message
+{
+	/// <summary>
+	/// This class implements STUN ERROR-CODE. Defined in RFC 3489 11.2.9.
+	/// </summary>
+	public class StunErrorCode
+	{
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		/// <param name="code">Error code.</param>
+		/// <param name="reasonText">Reason text.</param>
+		public StunErrorCode(int code, string reasonText)
+		{
+			Code = code;
+			ReasonText = reasonText;
+		}
+
+
+		#region Properties Implementation
+
+		/// <summary>
+		/// Gets or sets error code.
+		/// </summary>
+		public int Code { get; set; }
+
+		/// <summary>
+		/// Gets reason text.
+		/// </summary>
+		public string ReasonText { get; set; }
+
+		#endregion
+
+	}
+}

+ 660 - 0
NatTypeTester-Console/Net/STUN/Message/StunMessage.cs

@@ -0,0 +1,660 @@
+using System;
+using System.Net;
+using System.Text;
+
+namespace NatTypeTester_Console.Net.STUN.Message
+{
+	/// <summary>
+	/// Implements STUN message. Defined in RFC 3489.
+	/// </summary>
+	public class StunMessage
+	{
+		#region enum AttributeType
+
+		/// <summary>
+		/// Specifies STUN attribute type.
+		/// </summary>
+		private enum AttributeType
+		{
+			MappedAddress = 0x0001,
+			ResponseAddress = 0x0002,
+			ChangeRequest = 0x0003,
+			SourceAddress = 0x0004,
+			ChangedAddress = 0x0005,
+			Username = 0x0006,
+			Password = 0x0007,
+			MessageIntegrity = 0x0008,
+			ErrorCode = 0x0009,
+			UnknownAttribute = 0x000A,
+			ReflectedFrom = 0x000B,
+			XorMappedAddress = 0x8020,
+			XorOnly = 0x0021,
+			ServerName = 0x8022
+		}
+
+		#endregion
+
+		#region enum IPFamily
+
+		/// <summary>
+		/// Specifies IP address family.
+		/// </summary>
+		private enum IpFamily
+		{
+			Pv4 = 0x01,
+			Pv6 = 0x02
+		}
+
+		#endregion
+
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		public StunMessage()
+		{
+			TransactionId = new byte[12];
+			new Random().NextBytes(TransactionId);
+		}
+
+		#region method Parse
+
+		/// <summary>
+		/// Parses STUN message from raw data packet.
+		/// </summary>
+		/// <param name="data">Raw STUN message.</param>
+		/// <exception cref="ArgumentNullException">Is raised when <b>data</b> is null reference.</exception>
+		public void Parse(byte[] data)
+		{
+			if (data == null)
+			{
+				throw new ArgumentNullException("data");
+			}
+
+			/* RFC 5389 6.             
+                All STUN messages MUST start with a 20-byte header followed by zero
+                or more Attributes.  The STUN header contains a STUN message type,
+                magic cookie, transaction ID, and message length.
+
+                 0                   1                   2                   3
+                 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+                 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                 |0 0|     STUN Message Type     |         Message Length        |
+                 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                 |                         Magic Cookie                          |
+                 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                 |                                                               |
+                 |                     Transaction ID (96 bits)                  |
+                 |                                                               |
+                 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+              
+               The message length is the count, in bytes, of the size of the
+               message, not including the 20 byte header.
+            */
+
+			if (data.Length < 20)
+			{
+				throw new ArgumentException("Invalid STUN message value !");
+			}
+
+			var offset = 0;
+
+			//--- message header --------------------------------------------------
+
+			// STUN Message Type
+			var messageType = data[offset++] << 8 | data[offset++];
+			if (messageType == (int)StunMessageType.BindingErrorResponse)
+			{
+				Type = StunMessageType.BindingErrorResponse;
+			}
+			else if (messageType == (int)StunMessageType.BindingRequest)
+			{
+				Type = StunMessageType.BindingRequest;
+			}
+			else if (messageType == (int)StunMessageType.BindingResponse)
+			{
+				Type = StunMessageType.BindingResponse;
+			}
+			else if (messageType == (int)StunMessageType.SharedSecretErrorResponse)
+			{
+				Type = StunMessageType.SharedSecretErrorResponse;
+			}
+			else if (messageType == (int)StunMessageType.SharedSecretRequest)
+			{
+				Type = StunMessageType.SharedSecretRequest;
+			}
+			else if (messageType == (int)StunMessageType.SharedSecretResponse)
+			{
+				Type = StunMessageType.SharedSecretResponse;
+			}
+			else
+			{
+				throw new ArgumentException("Invalid STUN message type value !");
+			}
+
+			// Message Length
+			var messageLength = data[offset++] << 8 | data[offset++];
+
+			// Magic Cookie
+			MagicCookie = data[offset++] << 24 | data[offset++] << 16 | data[offset++] << 8 | data[offset++];
+
+			// Transaction ID
+			TransactionId = new byte[12];
+			Array.Copy(data, offset, TransactionId, 0, 12);
+			offset += 12;
+
+			//--- Message attributes ---------------------------------------------
+			while (offset - 20 < messageLength)
+			{
+				ParseAttribute(data, ref offset);
+			}
+		}
+
+		#endregion
+
+		#region method ToByteData
+
+		/// <summary>
+		/// Converts this to raw STUN packet.
+		/// </summary>
+		/// <returns>Returns raw STUN packet.</returns>
+		public byte[] ToByteData()
+		{
+			/* RFC 5389 6.             
+                All STUN messages MUST start with a 20-byte header followed by zero
+                or more Attributes.  The STUN header contains a STUN message type,
+                magic cookie, transaction ID, and message length.
+
+                 0                   1                   2                   3
+                 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+                 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                 |0 0|     STUN Message Type     |         Message Length        |
+                 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                 |                         Magic Cookie                          |
+                 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                 |                                                               |
+                 |                     Transaction ID (96 bits)                  |
+                 |                                                               |
+                 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+              
+               The message length is the count, in bytes, of the size of the
+               message, not including the 20 byte header.
+            */
+
+			// We allocate 512 for header, that should be more than enough.
+			var msg = new byte[512];
+
+			var offset = 0;
+
+			//--- message header -------------------------------------
+
+			// STUN Message Type (2 bytes)
+			msg[offset++] = (byte)(((int)Type >> 8) & 0x3F);
+			msg[offset++] = (byte)((int)Type & 0xFF);
+
+			// Message Length (2 bytes) will be assigned at last.
+			msg[offset++] = 0;
+			msg[offset++] = 0;
+
+			// Magic Cookie           
+			msg[offset++] = (byte)((MagicCookie >> 24) & 0xFF);
+			msg[offset++] = (byte)((MagicCookie >> 16) & 0xFF);
+			msg[offset++] = (byte)((MagicCookie >> 8) & 0xFF);
+			msg[offset++] = (byte)((MagicCookie >> 0) & 0xFF);
+
+			// Transaction ID (16 bytes)
+			Array.Copy(TransactionId, 0, msg, offset, 12);
+			offset += 12;
+
+			//--- Message attributes ------------------------------------
+
+			/* RFC 3489 11.2.
+                After the header are 0 or more attributes.  Each attribute is TLV
+                encoded, with a 16 bit type, 16 bit length, and variable value:
+
+                0                   1                   2                   3
+                0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+               |         Type                  |            Length             |
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+               |                             Value                             ....
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            */
+
+			if (MappedAddress != null)
+			{
+				StoreEndPoint(AttributeType.MappedAddress, MappedAddress, msg, ref offset);
+			}
+			else if (ResponseAddress != null)
+			{
+				StoreEndPoint(AttributeType.ResponseAddress, ResponseAddress, msg, ref offset);
+			}
+			else if (ChangeRequest != null)
+			{
+				/*
+                    The CHANGE-REQUEST attribute is used by the client to request that
+                    the server use a different address and/or port when sending the
+                    response.  The attribute is 32 bits long, although only two bits (A
+                    and B) are used:
+
+                     0                   1                   2                   3
+                     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                    |0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 A B 0|
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+                    The meaning of the flags is:
+
+                    A: This is the "change IP" flag.  If true, it requests the server
+                       to send the Binding Response with a different IP address than the
+                       one the Binding Request was received on.
+
+                    B: This is the "change port" flag.  If true, it requests the
+                       server to send the Binding Response with a different port than the
+                       one the Binding Request was received on.
+                */
+
+				// Attribute header
+				msg[offset++] = (int)AttributeType.ChangeRequest >> 8;
+				msg[offset++] = (int)AttributeType.ChangeRequest & 0xFF;
+				msg[offset++] = 0;
+				msg[offset++] = 4;
+
+				msg[offset++] = 0;
+				msg[offset++] = 0;
+				msg[offset++] = 0;
+				msg[offset++] = (byte)(Convert.ToInt32(ChangeRequest.ChangeIp) << 2 | Convert.ToInt32(ChangeRequest.ChangePort) << 1);
+			}
+			else if (SourceAddress != null)
+			{
+				StoreEndPoint(AttributeType.SourceAddress, SourceAddress, msg, ref offset);
+			}
+			else if (ChangedAddress != null)
+			{
+				StoreEndPoint(AttributeType.ChangedAddress, ChangedAddress, msg, ref offset);
+			}
+			else if (UserName != null)
+			{
+				var userBytes = Encoding.ASCII.GetBytes(UserName);
+
+				// Attribute header
+				msg[offset++] = (int)AttributeType.Username >> 8;
+				msg[offset++] = (int)AttributeType.Username & 0xFF;
+				msg[offset++] = (byte)(userBytes.Length >> 8);
+				msg[offset++] = (byte)(userBytes.Length & 0xFF);
+
+				Array.Copy(userBytes, 0, msg, offset, userBytes.Length);
+				offset += userBytes.Length;
+			}
+			else if (Password != null)
+			{
+				var userBytes = Encoding.ASCII.GetBytes(UserName);
+
+				// Attribute header
+				msg[offset++] = (int)AttributeType.Password >> 8;
+				msg[offset++] = (int)AttributeType.Password & 0xFF;
+				msg[offset++] = (byte)(userBytes.Length >> 8);
+				msg[offset++] = (byte)(userBytes.Length & 0xFF);
+
+				Array.Copy(userBytes, 0, msg, offset, userBytes.Length);
+				offset += userBytes.Length;
+			}
+			else if (ErrorCode != null)
+			{
+				/* 3489 11.2.9.
+                    0                   1                   2                   3
+                    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                    |                   0                     |Class|     Number    |
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                    |      Reason Phrase (variable)                                ..
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                */
+
+				var reasonBytes = Encoding.ASCII.GetBytes(ErrorCode.ReasonText);
+
+				// Header
+				msg[offset++] = 0;
+				msg[offset++] = (int)AttributeType.ErrorCode;
+				msg[offset++] = 0;
+				msg[offset++] = (byte)(4 + reasonBytes.Length);
+
+				// Empty
+				msg[offset++] = 0;
+				msg[offset++] = 0;
+				// Class
+				msg[offset++] = (byte)Math.Floor((double)(ErrorCode.Code / 100));
+				// Number
+				msg[offset++] = (byte)(ErrorCode.Code & 0xFF);
+				// ReasonPhrase
+				Array.Copy(reasonBytes, msg, reasonBytes.Length);
+				offset += reasonBytes.Length;
+			}
+			else if (ReflectedFrom != null)
+			{
+				StoreEndPoint(AttributeType.ReflectedFrom, ReflectedFrom, msg, ref offset);
+			}
+
+			// Update Message Length. NOTE: 20 bytes header not included.
+			msg[2] = (byte)((offset - 20) >> 8);
+			msg[3] = (byte)((offset - 20) & 0xFF);
+
+			// Make reatval with actual size.
+			var retVal = new byte[offset];
+			Array.Copy(msg, retVal, retVal.Length);
+
+			return retVal;
+		}
+
+		#endregion
+
+
+		#region method ParseAttribute
+
+		/// <summary>
+		/// Parses attribute from data.
+		/// </summary>
+		/// <param name="data">SIP message data.</param>
+		/// <param name="offset">Offset in data.</param>
+		private void ParseAttribute(byte[] data, ref int offset)
+		{
+			/* RFC 3489 11.2.
+                Each attribute is TLV encoded, with a 16 bit type, 16 bit length, and variable value:
+
+                0                   1                   2                   3
+                0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+               |         Type                  |            Length             |
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+               |                             Value                             ....
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                            
+            */
+
+			// Type
+			var type = (AttributeType)(data[offset++] << 8 | data[offset++]);
+
+			// Length
+			var length = data[offset++] << 8 | data[offset++];
+
+			// MAPPED-ADDRESS
+			if (type == AttributeType.MappedAddress)
+			{
+				MappedAddress = ParseEndPoint(data, ref offset);
+			}
+			// RESPONSE-ADDRESS
+			else if (type == AttributeType.ResponseAddress)
+			{
+				ResponseAddress = ParseEndPoint(data, ref offset);
+			}
+			// CHANGE-REQUEST
+			else if (type == AttributeType.ChangeRequest)
+			{
+				/*
+                    The CHANGE-REQUEST attribute is used by the client to request that
+                    the server use a different address and/or port when sending the
+                    response.  The attribute is 32 bits long, although only two bits (A
+                    and B) are used:
+
+                     0                   1                   2                   3
+                     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                    |0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 A B 0|
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+                    The meaning of the flags is:
+
+                    A: This is the "change IP" flag.  If true, it requests the server
+                       to send the Binding Response with a different IP address than the
+                       one the Binding Request was received on.
+
+                    B: This is the "change port" flag.  If true, it requests the
+                       server to send the Binding Response with a different port than the
+                       one the Binding Request was received on.
+                */
+
+				// Skip 3 bytes
+				offset += 3;
+
+				ChangeRequest = new StunChangeRequest((data[offset] & 4) != 0, (data[offset] & 2) != 0);
+				offset++;
+			}
+			// SOURCE-ADDRESS
+			else if (type == AttributeType.SourceAddress)
+			{
+				SourceAddress = ParseEndPoint(data, ref offset);
+			}
+			// CHANGED-ADDRESS
+			else if (type == AttributeType.ChangedAddress)
+			{
+				ChangedAddress = ParseEndPoint(data, ref offset);
+			}
+			// USERNAME
+			else if (type == AttributeType.Username)
+			{
+				UserName = Encoding.Default.GetString(data, offset, length);
+				offset += length;
+			}
+			// PASSWORD
+			else if (type == AttributeType.Password)
+			{
+				Password = Encoding.Default.GetString(data, offset, length);
+				offset += length;
+			}
+			// MESSAGE-INTEGRITY
+			else if (type == AttributeType.MessageIntegrity)
+			{
+				offset += length;
+			}
+			// ERROR-CODE
+			else if (type == AttributeType.ErrorCode)
+			{
+				/* 3489 11.2.9.
+                    0                   1                   2                   3
+                    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                    |                   0                     |Class|     Number    |
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                    |      Reason Phrase (variable)                                ..
+                    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                */
+
+				var errorCode = (data[offset + 2] & 0x7) * 100 + (data[offset + 3] & 0xFF);
+
+				ErrorCode = new StunErrorCode(errorCode, Encoding.Default.GetString(data, offset + 4, length - 4));
+				offset += length;
+			}
+			// UNKNOWN-ATTRIBUTES
+			else if (type == AttributeType.UnknownAttribute)
+			{
+				offset += length;
+			}
+			// REFLECTED-FROM
+			else if (type == AttributeType.ReflectedFrom)
+			{
+				ReflectedFrom = ParseEndPoint(data, ref offset);
+			}
+			// XorMappedAddress
+			// XorOnly
+			// ServerName
+			else if (type == AttributeType.ServerName)
+			{
+				ServerName = Encoding.Default.GetString(data, offset, length);
+				offset += length;
+			}
+			// Unknown
+			else
+			{
+				offset += length;
+			}
+		}
+
+		#endregion
+
+		#region method ParseEndPoint
+
+		/// <summary>
+		/// Pasrses IP endpoint attribute.
+		/// </summary>
+		/// <param name="data">STUN message data.</param>
+		/// <param name="offset">Offset in data.</param>
+		/// <returns>Returns parsed IP end point.</returns>
+		private IPEndPoint ParseEndPoint(byte[] data, ref int offset)
+		{
+			/*
+                It consists of an eight bit address family, and a sixteen bit
+                port, followed by a fixed length value representing the IP address.
+
+                0                   1                   2                   3
+                0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                |x x x x x x x x|    Family     |           Port                |
+                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                |                             Address                           |
+                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            */
+
+			// Skip family
+			offset++;
+			offset++;
+
+			// Port
+			var port = data[offset++] << 8 | data[offset++];
+
+			// Address
+			var ip = new byte[4];
+			ip[0] = data[offset++];
+			ip[1] = data[offset++];
+			ip[2] = data[offset++];
+			ip[3] = data[offset++];
+
+			return new IPEndPoint(new IPAddress(ip), port);
+		}
+
+		#endregion
+
+		#region method StoreEndPoint
+
+		/// <summary>
+		/// Stores ip end point attribute to buffer.
+		/// </summary>
+		/// <param name="type">Attribute type.</param>
+		/// <param name="endPoint">IP end point.</param>
+		/// <param name="message">Buffer where to store.</param>
+		/// <param name="offset">Offset in buffer.</param>
+		private void StoreEndPoint(AttributeType type, IPEndPoint endPoint, byte[] message, ref int offset)
+		{
+			/*
+                It consists of an eight bit address family, and a sixteen bit
+                port, followed by a fixed length value representing the IP address.
+
+                0                   1                   2                   3
+                0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                |x x x x x x x x|    Family     |           Port                |
+                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+                |                             Address                           |
+                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+             
+            */
+
+			// Header
+			message[offset++] = (byte)((int)type >> 8);
+			message[offset++] = (byte)((int)type & 0xFF);
+			message[offset++] = 0;
+			message[offset++] = 8;
+
+			// Unused
+			message[offset++] = 0;
+			// Family
+			message[offset++] = (byte)IpFamily.Pv4;
+			// Port
+			message[offset++] = (byte)(endPoint.Port >> 8);
+			message[offset++] = (byte)(endPoint.Port & 0xFF);
+			// Address
+			var ipBytes = endPoint.Address.GetAddressBytes();
+			message[offset++] = ipBytes[0];
+			message[offset++] = ipBytes[1];
+			message[offset++] = ipBytes[2];
+			message[offset++] = ipBytes[3];
+		}
+
+		#endregion
+
+
+		#region Properties Implementation
+
+		/// <summary>
+		/// Gets STUN message type.
+		/// </summary>
+		public StunMessageType Type { get; set; } = StunMessageType.BindingRequest;
+
+		/// <summary>
+		/// Gets magic cookie value. This is always 0x2112A442.
+		/// </summary>
+		public int MagicCookie { get; private set; }
+
+		/// <summary>
+		/// Gets transaction ID.
+		/// </summary>
+		public byte[] TransactionId { get; private set; }
+
+		/// <summary>
+		/// Gets or sets IP end point what was actually connected to STUN server. Returns null if not specified.
+		/// </summary>
+		public IPEndPoint MappedAddress { get; set; }
+
+		/// <summary>
+		/// Gets or sets IP end point where to STUN client likes to receive response.
+		/// Value null means not specified.
+		/// </summary>
+		public IPEndPoint ResponseAddress { get; set; }
+
+		/// <summary>
+		/// Gets or sets how and where STUN server must send response back to STUN client.
+		/// Value null means not specified.
+		/// </summary>
+		public StunChangeRequest ChangeRequest { get; set; }
+
+		/// <summary>
+		/// Gets or sets STUN server IP end point what sent response to STUN client. Value null
+		/// means not specified.
+		/// </summary>
+		public IPEndPoint SourceAddress { get; set; }
+
+		/// <summary>
+		/// Gets or sets IP end point where STUN server will send response back to STUN client 
+		/// if the "change IP" and "change port" flags had been set in the ChangeRequest.
+		/// </summary>
+		public IPEndPoint ChangedAddress { get; set; }
+
+		/// <summary>
+		/// Gets or sets user name. Value null means not specified.
+		/// </summary>          
+		public string UserName { get; set; }
+
+		/// <summary>
+		/// Gets or sets password. Value null means not specified.
+		/// </summary>
+		public string Password { get; set; }
+
+		//public MessageIntegrity
+
+		/// <summary>
+		/// Gets or sets error info. Returns null if not specified.
+		/// </summary>
+		public StunErrorCode ErrorCode { get; set; }
+
+
+		/// <summary>
+		/// Gets or sets IP endpoint from which IP end point STUN server got STUN client request.
+		/// Value null means not specified.
+		/// </summary>
+		public IPEndPoint ReflectedFrom { get; set; }
+
+		/// <summary>
+		/// Gets or sets server name.
+		/// </summary>
+		public string ServerName { get; set; }
+
+		#endregion
+
+	}
+}

+ 38 - 0
NatTypeTester-Console/Net/STUN/Message/StunMessageType.cs

@@ -0,0 +1,38 @@
+namespace NatTypeTester_Console.Net.STUN.Message
+{
+	/// <summary>
+	/// This enum specifies STUN message type.
+	/// </summary>
+	public enum StunMessageType
+	{
+		/// <summary>
+		/// STUN message is binding request.
+		/// </summary>
+		BindingRequest = 0x0001,
+
+		/// <summary>
+		/// STUN message is binding request response.
+		/// </summary>
+		BindingResponse = 0x0101,
+
+		/// <summary>
+		/// STUN message is binding requesr error response.
+		/// </summary>
+		BindingErrorResponse = 0x0111,
+
+		/// <summary>
+		/// STUN message is "shared secret" request.
+		/// </summary>
+		SharedSecretRequest = 0x0002,
+
+		/// <summary>
+		/// STUN message is "shared secret" request response.
+		/// </summary>
+		SharedSecretResponse = 0x0102,
+
+		/// <summary>
+		/// STUN message is "shared secret" request error response.
+		/// </summary>
+		SharedSecretErrorResponse = 0x0112
+	}
+}

+ 15 - 0
NatTypeTester-Console/Program.cs

@@ -0,0 +1,15 @@
+using System;
+
+namespace NatTypeTester_Console
+{
+	internal static class Program
+	{
+		private static void Main(string[] args)
+		{
+			var res = Utils.NatTypeTestCore(Utils.DefaultLocalEnd, @"stun.miwifi.com", 3478);
+			Console.WriteLine(res.Item1);
+			Console.WriteLine(res.Item2);
+			Console.WriteLine(res.Item3);
+		}
+	}
+}

+ 60 - 0
NatTypeTester-Console/Utils.cs

@@ -0,0 +1,60 @@
+using NatTypeTester_Console.Net.STUN.Client;
+using System;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+
+namespace NatTypeTester_Console
+{
+	public static class Utils
+	{
+		public const string DefaultLocalEnd = @"0.0.0.0:0";
+
+		public static IPEndPoint ParseEndpoint(string str)
+		{
+			var ipPort = str.Trim().Split(':');
+			if (ipPort.Length == 2)
+			{
+				if (IPAddress.TryParse(ipPort[0], out var ip))
+				{
+					if (ushort.TryParse(ipPort[1], out var port))
+					{
+						return new IPEndPoint(ip, port);
+					}
+				}
+			}
+
+			return null;
+		}
+
+		public static (string, string, string) NatTypeTestCore(string local, string server, int port)
+		{
+			try
+			{
+				if (string.IsNullOrWhiteSpace(server))
+				{
+					Debug.WriteLine(@"[ERROR]: Please specify STUN server !");
+					return (string.Empty, DefaultLocalEnd, string.Empty);
+				}
+
+				using (var socketV4 = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
+				{
+					var ipe = ParseEndpoint(local) ?? new IPEndPoint(IPAddress.Any, 0);
+					socketV4.Bind(ipe);
+					var result = StunClient.Query(server, port, socketV4);
+
+					return (
+							result.NatType.ToString(),
+							socketV4.LocalEndPoint.ToString(),
+							result.NatType != NatType.UdpBlocked ? result.PublicEndPoint.ToString() : string.Empty
+					);
+				}
+			}
+			catch (Exception ex)
+			{
+				Debug.WriteLine($@"[ERROR]: {ex}");
+				return (string.Empty, DefaultLocalEnd, string.Empty);
+			}
+		}
+	}
+}

+ 7 - 1
NatTypeTester.sln

@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 16
 VisualStudioVersion = 16.0.29102.190
 MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NatTypeTester", "NatTypeTester\NatTypeTester.csproj", "{B5104123-EB01-4079-8865-2A99DD91DC24}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NatTypeTester", "NatTypeTester\NatTypeTester.csproj", "{B5104123-EB01-4079-8865-2A99DD91DC24}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NatTypeTester_Console", "NatTypeTester-Console\NatTypeTester_Console.csproj", "{CC6BE2E2-81CB-4C0B-BA17-4BB48DD403AC}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +17,10 @@ Global
 		{B5104123-EB01-4079-8865-2A99DD91DC24}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{B5104123-EB01-4079-8865-2A99DD91DC24}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{B5104123-EB01-4079-8865-2A99DD91DC24}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CC6BE2E2-81CB-4C0B-BA17-4BB48DD403AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CC6BE2E2-81CB-4C0B-BA17-4BB48DD403AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CC6BE2E2-81CB-4C0B-BA17-4BB48DD403AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{CC6BE2E2-81CB-4C0B-BA17-4BB48DD403AC}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE