Bladeren bron

Refactor: RFC3489 client

Bruce Wayne 4 jaren geleden
bovenliggende
commit
73fed64283

+ 1 - 1
NatTypeTester.ViewModels/RFC3489ViewModel.cs

@@ -55,7 +55,7 @@ namespace NatTypeTester.ViewModels
 					.ObserveOn(RxApp.MainThreadScheduler)
 					.Subscribe(_ => this.RaisePropertyChanged(nameof(Result3489))))
 			{
-				await client.Query3489Async();
+				await client.QueryAsync(token);
 			}
 
 			Result3489.LocalEndPoint = client.LocalEndPoint;

+ 120 - 84
STUN/Client/StunClient3489.cs

@@ -19,93 +19,91 @@ namespace STUN.Client
 	/// </summary>
 	public class StunClient3489 : IDisposable
 	{
-		public IPEndPoint LocalEndPoint => Proxy.LocalEndPoint;
+		public IPEndPoint LocalEndPoint => _proxy.LocalEndPoint;
 
 		public TimeSpan Timeout
 		{
-			get => Proxy.Timeout;
-			set => Proxy.Timeout = value;
+			get => _proxy.Timeout;
+			set => _proxy.Timeout = value;
 		}
 
-		protected readonly IPAddress Server;
-		protected readonly ushort Port;
+		private readonly IPAddress _server;
+		private readonly ushort _port;
 
-		protected IPEndPoint RemoteEndPoint => new(Server, Port);
+		private IPEndPoint RemoteEndPoint => new(_server, _port);
 
-		protected readonly IUdpProxy Proxy;
+		private readonly IUdpProxy _proxy;
 
 		public ClassicStunResult Status { get; } = new();
 
 		public StunClient3489(IPAddress server, ushort port = 3478, IPEndPoint? local = null, IUdpProxy? proxy = null)
 		{
 			Requires.NotNull(server, nameof(server));
-			Requires.Argument(port > 0, nameof(port), @"Port value must be >= 1 !");
+			Requires.Argument(port > 0, nameof(port), @"Port value must be > 0!");
 
-			Proxy = proxy ?? new NoneUdpProxy(local);
+			_proxy = proxy ?? new NoneUdpProxy(local);
 
-			Server = server;
-			Port = port;
+			_server = server;
+			_port = port;
 
-			Timeout = TimeSpan.FromSeconds(1.6);
+			Timeout = TimeSpan.FromSeconds(3);
 			Status.LocalEndPoint = local;
 		}
 
-		private void Init()
+		public virtual async ValueTask ConnectAsync(CancellationToken cancellationToken)
 		{
-			Status.PublicEndPoint = default;
-			Status.LocalEndPoint = default;
-			Status.NatType = NatType.Unknown;
+			Status.Reset();
+
+			using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+			cts.CancelAfter(Timeout);
+
+			await _proxy.ConnectAsync(cts.Token);
+		}
+
+		public virtual async ValueTask DisconnectAsync()
+		{
+			await _proxy.DisconnectAsync();
 		}
 
-		public async Task Query3489Async()
+		public async Task QueryAsync(CancellationToken cancellationToken = default)
 		{
 			try
 			{
-				Init();
-				using var cts = new CancellationTokenSource(Timeout);
-				await Proxy.ConnectAsync(cts.Token);
-				// test I
-				var test1 = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest, MagicCookie = 0 };
+				await ConnectAsync(cancellationToken);
 
-				var (response1, remote1, local1) = await TestAsync(test1, RemoteEndPoint, RemoteEndPoint, cts.Token);
-				if (response1 is null || remote1 is null)
+				// test I
+				var response1 = await Test1Async(cancellationToken);
+				if (response1?.Message is null || response1.Remote is null)
 				{
 					Status.NatType = NatType.UdpBlocked;
 					return;
 				}
 
-				Status.LocalEndPoint = local1 is null ? null : new IPEndPoint(local1, LocalEndPoint.Port);
+				Status.LocalEndPoint = response1.LocalAddress is null ? null : new IPEndPoint(response1.LocalAddress, LocalEndPoint.Port);
 
-				var mappedAddress1 = response1.GetMappedAddressAttribute();
-				var changedAddress1 = response1.GetChangedAddressAttribute();
+				var mappedAddress1 = response1.Message.GetMappedAddressAttribute();
+				var changedAddress = response1.Message.GetChangedAddressAttribute();
+
+				Status.PublicEndPoint = mappedAddress1; // 显示 test I 得到的映射地址
 
 				// 某些单 IP 服务器的迷惑操作
-				if (mappedAddress1 is null
-				|| changedAddress1 is null
-				|| Equals(changedAddress1.Address, remote1.Address)
-				|| changedAddress1.Port == remote1.Port)
+				if (mappedAddress1 is null || changedAddress is null
+					|| Equals(changedAddress.Address, response1.Remote.Address)
+					|| changedAddress.Port == response1.Remote.Port)
 				{
 					Status.NatType = NatType.UnsupportedServer;
 					return;
 				}
 
-				Status.PublicEndPoint = mappedAddress1; // 显示 test I 得到的映射地址
-
-				var test2 = new StunMessage5389
-				{
-					StunMessageType = StunMessageType.BindingRequest,
-					MagicCookie = 0,
-					Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
-				};
-
 				// test II
-				var (response2, remote2, _) = await TestAsync(test2, RemoteEndPoint, changedAddress1, cts.Token);
-				var mappedAddress2 = response2.GetMappedAddressAttribute();
+				var response2 = await Test2Async(changedAddress, cancellationToken);
+				var mappedAddress2 = response2?.Message.GetMappedAddressAttribute();
 
-				if (Equals(mappedAddress1.Address, local1) && mappedAddress1.Port == LocalEndPoint.Port)
+				// is Public IP == link's IP?
+				if (Equals(mappedAddress1.Address, response1.LocalAddress) && mappedAddress1.Port == LocalEndPoint.Port)
 				{
 					// No NAT
-					if (response2 is null)
+					if (response2?.Message is null)
 					{
 						Status.NatType = NatType.SymmetricUdpFirewall;
 						Status.PublicEndPoint = mappedAddress1;
@@ -119,19 +117,18 @@ namespace STUN.Client
 				}
 
 				// NAT
-				if (response2 is not null && remote2 is not null)
+				if (response2?.Message is not null && response2.Remote is not null)
 				{
 					// 有些单 IP 服务器并不能测 NAT 类型,比如 Google 的
-					var type = Equals(remote1.Address, remote2.Address) || remote1.Port == remote2.Port ? NatType.UnsupportedServer : NatType.FullCone;
+					var type = Equals(response1.Remote.Address, response2.Remote.Address) || response1.Remote.Port == response2.Remote.Port ? NatType.UnsupportedServer : NatType.FullCone;
 					Status.NatType = type;
 					Status.PublicEndPoint = mappedAddress2;
 					return;
 				}
 
 				// Test I(#2)
-				var test12 = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest, MagicCookie = 0 };
-				var (response12, _, _) = await TestAsync(test12, changedAddress1, changedAddress1, cts.Token);
-				var mappedAddress12 = response12.GetMappedAddressAttribute();
+				var response12 = await Test1_2Async(changedAddress, cancellationToken);
+				var mappedAddress12 = response12?.Message.GetMappedAddressAttribute();
 
 				if (mappedAddress12 is null)
 				{
@@ -147,19 +144,22 @@ namespace STUN.Client
 				}
 
 				// Test III
-				var test3 = new StunMessage5389
+				var response3 = await Test3Async(cancellationToken);
+				var mappedAddress3 = response3?.Message.GetMappedAddressAttribute();
+				if (mappedAddress3 is not null && response3?.Remote is not null)
 				{
-					StunMessageType = StunMessageType.BindingRequest,
-					MagicCookie = 0,
-					Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
-				};
-				var (response3, _, _) = await TestAsync(test3, changedAddress1, changedAddress1, cts.Token);
-				var mappedAddress3 = response3.GetMappedAddressAttribute();
-				if (mappedAddress3 is not null)
-				{
-					Status.NatType = NatType.RestrictedCone;
-					Status.PublicEndPoint = mappedAddress3;
-					return;
+					if (Equals(response3.Remote.Address, response1.Remote.Address) && response3.Remote.Port != response1.Remote.Port)
+					{
+						Status.NatType = NatType.RestrictedCone;
+						Status.PublicEndPoint = mappedAddress3;
+						return;
+					}
+					else
+					{
+						Status.NatType = NatType.UnsupportedServer;
+						Status.PublicEndPoint = mappedAddress3;
+						return;
+					}
 				}
 
 				Status.NatType = NatType.PortRestrictedCone;
@@ -167,50 +167,86 @@ namespace STUN.Client
 			}
 			finally
 			{
-				await Proxy.DisconnectAsync();
+				await DisconnectAsync();
 			}
 		}
 
-		protected async Task<(StunMessage5389?, IPEndPoint?, IPAddress?)> TestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken token)
+		private async ValueTask<StunResponse?> RequestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken cancellationToken)
 		{
 			try
 			{
 				using var memoryOwner = MemoryPool<byte>.Shared.Rent(ushort.MaxValue);
 				var sendBuffer = memoryOwner.Memory;
 				var length = sendMessage.WriteTo(sendBuffer.Span);
-				//var t = DateTime.Now;
 
-				// Simple retransmissions
-				//https://tools.ietf.org/html/rfc3489#section-9.3
-				//while (t + TimeSpan.FromSeconds(3) > DateTime.Now)
+				using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+				cts.CancelAfter(Timeout);
+				var (receiveBuffer, ipe, local) = await _proxy.ReceiveAsync(sendBuffer[..length], remote, receive, cts.Token);
+
+				var message = new StunMessage5389();
+				if (message.TryParse(receiveBuffer) && message.IsSameTransaction(sendMessage))
 				{
-					try
-					{
-						var (receive1, ipe, local) = await Proxy.ReceiveAsync(sendBuffer[..length], remote, receive, token);
-
-						var message = new StunMessage5389();
-						if (message.TryParse(receive1) &&
-							message.IsSameTransaction(sendMessage))
-						{
-							return (message, ipe, local);
-						}
-					}
-					catch (Exception ex)
+					var response = new StunResponse
 					{
-						Debug.WriteLine(ex);
-					}
+						Message = message,
+						Remote = ipe,
+						LocalAddress = local
+					};
+					return response;
 				}
 			}
 			catch (Exception ex)
 			{
 				Debug.WriteLine(ex);
 			}
-			return (null, null, null);
+			return default;
+		}
+
+		public virtual async ValueTask<StunResponse?> Test1Async(CancellationToken cancellationToken)
+		{
+			var message = new StunMessage5389
+			{
+				StunMessageType = StunMessageType.BindingRequest,
+				MagicCookie = 0
+			};
+			return await RequestAsync(message, RemoteEndPoint, RemoteEndPoint, cancellationToken);
+		}
+
+		public virtual async ValueTask<StunResponse?> Test2Async(IPEndPoint other, CancellationToken cancellationToken)
+		{
+			var message = new StunMessage5389
+			{
+				StunMessageType = StunMessageType.BindingRequest,
+				MagicCookie = 0,
+				Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
+			};
+			return await RequestAsync(message, RemoteEndPoint, other, cancellationToken);
+		}
+
+		public virtual async ValueTask<StunResponse?> Test1_2Async(IPEndPoint other, CancellationToken cancellationToken)
+		{
+			var message = new StunMessage5389
+			{
+				StunMessageType = StunMessageType.BindingRequest,
+				MagicCookie = 0
+			};
+			return await RequestAsync(message, other, other, cancellationToken);
+		}
+
+		public virtual async ValueTask<StunResponse?> Test3Async(CancellationToken cancellationToken)
+		{
+			var message = new StunMessage5389
+			{
+				StunMessageType = StunMessageType.BindingRequest,
+				MagicCookie = 0,
+				Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
+			};
+			return await RequestAsync(message, RemoteEndPoint, RemoteEndPoint, cancellationToken);
 		}
 
-		public virtual void Dispose()
+		public void Dispose()
 		{
-			Proxy.Dispose();
+			_proxy.Dispose();
 		}
 	}
 }

+ 71 - 3
STUN/Client/StunClient5389UDP.cs

@@ -1,9 +1,12 @@
+using Microsoft;
 using STUN.Enums;
 using STUN.Messages;
 using STUN.Proxy;
 using STUN.StunResult;
 using STUN.Utils;
 using System;
+using System.Buffers;
+using System.Diagnostics;
 using System.Net;
 using System.Threading;
 using System.Threading.Tasks;
@@ -14,13 +17,35 @@ namespace STUN.Client
 	/// https://tools.ietf.org/html/rfc5389#section-7.2.1
 	/// https://tools.ietf.org/html/rfc5780#section-4.2
 	/// </summary>
-	public class StunClient5389UDP : StunClient3489
+	public class StunClient5389UDP : IDisposable
 	{
-		public new StunResult5389 Status { get; } = new();
+		public IPEndPoint LocalEndPoint => Proxy.LocalEndPoint;
+
+		public TimeSpan Timeout
+		{
+			get => Proxy.Timeout;
+			set => Proxy.Timeout = value;
+		}
+
+		protected readonly IPAddress Server;
+		protected readonly ushort Port;
+
+		protected IPEndPoint RemoteEndPoint => new(Server, Port);
+
+		protected readonly IUdpProxy Proxy;
+
+		public StunResult5389 Status { get; } = new();
 
 		public StunClient5389UDP(IPAddress server, ushort port = 3478, IPEndPoint? local = null, IUdpProxy? proxy = null)
-		: base(server, port, local, proxy)
 		{
+			Requires.NotNull(server, nameof(server));
+			Requires.Argument(port > 0, nameof(port), @"Port value must be >= 1 !");
+
+			Proxy = proxy ?? new NoneUdpProxy(local);
+
+			Server = server;
+			Port = port;
+
 			Timeout = TimeSpan.FromSeconds(3);
 			Status.LocalEndPoint = local;
 		}
@@ -256,5 +281,48 @@ namespace STUN.Client
 				await Proxy.DisconnectAsync();
 			}
 		}
+
+		private async Task<(StunMessage5389?, IPEndPoint?, IPAddress?)> TestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken token)
+		{
+			try
+			{
+				using var memoryOwner = MemoryPool<byte>.Shared.Rent(ushort.MaxValue);
+				var sendBuffer = memoryOwner.Memory;
+				var length = sendMessage.WriteTo(sendBuffer.Span);
+				//var t = DateTime.Now;
+
+				// Simple retransmissions
+				//https://tools.ietf.org/html/rfc3489#section-9.3
+				//while (t + TimeSpan.FromSeconds(3) > DateTime.Now)
+				{
+					try
+					{
+						var (receive1, ipe, local) = await Proxy.ReceiveAsync(sendBuffer[..length], remote, receive, token);
+
+						var message = new StunMessage5389();
+						if (message.TryParse(receive1) &&
+							message.IsSameTransaction(sendMessage))
+						{
+							return (message, ipe, local);
+						}
+					}
+					catch (Exception ex)
+					{
+						Debug.WriteLine(ex);
+					}
+				}
+			}
+			catch (Exception ex)
+			{
+				Debug.WriteLine(ex);
+			}
+
+			return (null, null, null);
+		}
+
+		public void Dispose()
+		{
+			Proxy.Dispose();
+		}
 	}
 }

+ 11 - 0
STUN/Messages/StunResponse.cs

@@ -0,0 +1,11 @@
+using System.Net;
+
+namespace STUN.Messages
+{
+	public class StunResponse
+	{
+		public StunMessage5389? Message { get; set; }
+		public IPEndPoint? Remote { get; set; }
+		public IPAddress? LocalAddress { get; set; }
+	}
+}

+ 18 - 10
STUN/Proxy/ProxyFactory.cs

@@ -1,23 +1,31 @@
+using Microsoft;
 using STUN.Enums;
-using System;
 using System.Net;
 
 namespace STUN.Proxy
 {
 	public static class ProxyFactory
 	{
-		public static IUdpProxy CreateProxy(ProxyType type, IPEndPoint? local, IPEndPoint? proxy, string? user = null, string? password = null)
+		public static IUdpProxy CreateProxy(
+			ProxyType type, IPEndPoint? local,
+			IPEndPoint? proxy = default, string? user = default, string? password = default)
 		{
-			if (proxy is null)
+			switch (type)
 			{
-				throw new ArgumentNullException(nameof(proxy), @"Proxy server is null");
+				case ProxyType.Plain:
+				{
+					return new NoneUdpProxy(local);
+				}
+				case ProxyType.Socks5:
+				{
+					Requires.Argument(proxy is not null, nameof(proxy), @"Proxy server is null");
+					return new Socks5UdpProxy(local, proxy, user, password);
+				}
+				default:
+				{
+					throw Assumes.NotReachable();
+				}
 			}
-			return type switch
-			{
-				ProxyType.Plain => new NoneUdpProxy(local),
-				ProxyType.Socks5 => new Socks5UdpProxy(local, proxy, user, password),
-				_ => throw new NotSupportedException(type.ToString())
-			};
 		}
 	}
 }

+ 6 - 0
STUN/StunResult/ClassicStunResult.cs

@@ -5,5 +5,11 @@ namespace STUN.StunResult
 	public class ClassicStunResult : StunResult
 	{
 		public NatType NatType { get; set; } = NatType.Unknown;
+
+		public override void Reset()
+		{
+			base.Reset();
+			NatType = NatType.Unknown;
+		}
 	}
 }

+ 6 - 0
STUN/StunResult/StunResult.cs

@@ -6,5 +6,11 @@ namespace STUN.StunResult
 	{
 		public IPEndPoint? PublicEndPoint { get; set; }
 		public IPEndPoint? LocalEndPoint { get; set; }
+
+		public virtual void Reset()
+		{
+			PublicEndPoint = default;
+			LocalEndPoint = default;
+		}
 	}
 }

+ 2 - 3
STUN/StunResult/StunResult5389.cs

@@ -23,10 +23,9 @@ namespace STUN.StunResult
 			FilteringBehavior = result.FilteringBehavior;
 		}
 
-		public void Reset()
+		public override void Reset()
 		{
-			PublicEndPoint = default;
-			LocalEndPoint = default;
+			base.Reset();
 			OtherEndPoint = default;
 			BindingTestResult = BindingTestResult.Unknown;
 			MappingBehavior = MappingBehavior.Unknown;

+ 43 - 0
STUN/Utils/AttributeExtensions.cs

@@ -1,6 +1,7 @@
 using STUN.Enums;
 using STUN.Messages;
 using STUN.Messages.StunAttributeValues;
+using System;
 using System.Linq;
 using System.Net;
 
@@ -18,6 +19,48 @@ namespace STUN.Utils
 			};
 		}
 
+		public static StunAttribute BuildMapping(IpFamily family, IPAddress ip, ushort port)
+		{
+			var length = family switch
+			{
+				IpFamily.IPv4 => 4,
+				IpFamily.IPv6 => 16,
+				_ => throw new ArgumentOutOfRangeException(nameof(family), family, null)
+			};
+			return new StunAttribute
+			{
+				Type = AttributeType.MappedAddress,
+				Length = (ushort)(4 + length),
+				Value = new MappedAddressStunAttributeValue
+				{
+					Family = family,
+					Address = ip,
+					Port = port
+				}
+			};
+		}
+
+		public static StunAttribute BuildChangeAddress(IpFamily family, IPAddress ip, ushort port)
+		{
+			var length = family switch
+			{
+				IpFamily.IPv4 => 4,
+				IpFamily.IPv6 => 16,
+				_ => throw new ArgumentOutOfRangeException(nameof(family), family, null)
+			};
+			return new StunAttribute
+			{
+				Type = AttributeType.ChangedAddress,
+				Length = (ushort)(4 + length),
+				Value = new ChangedAddressStunAttributeValue
+				{
+					Family = family,
+					Address = ip,
+					Port = port
+				}
+			};
+		}
+
 		public static IPEndPoint? GetMappedAddressAttribute(this StunMessage5389? response)
 		{
 			var mappedAddressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);

+ 1 - 0
STUN/Utils/NetUtils.cs

@@ -18,6 +18,7 @@ namespace STUN.Utils
 			return foo?.State ?? TcpState.Unknown;
 		}
 
+		//TODO .NET6.0
 		public static Task<(IPAddress, int, IPEndPoint)> ReceiveMessageFromAsync(this Socket client, EndPoint receive, byte[] array, SocketFlags flag)
 		{
 			return Task.Run(() =>

+ 162 - 0
UnitTest/StunClient3489Test.cs

@@ -0,0 +1,162 @@
+using Dns.Net.Abstractions;
+using Dns.Net.Clients;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using STUN.Client;
+using STUN.Enums;
+using STUN.Messages;
+using STUN.Utils;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using static STUN.Utils.AttributeExtensions;
+
+namespace UnitTest
+{
+	[TestClass]
+	public class StunClient3489Test
+	{
+		private readonly IDnsClient _dnsClient = new DefaultDnsClient();
+
+		private const string Server = @"stun.syncthing.net";
+		private const ushort Port = 3478;
+
+		private static readonly IPEndPoint Any = new(IPAddress.Any, 0);
+		private static readonly IPEndPoint MappedAddress1 = IPEndPoint.Parse(@"1.1.1.1:114");
+		private static readonly IPEndPoint MappedAddress2 = IPEndPoint.Parse(@"1.1.1.1:514");
+		private static readonly IPEndPoint ServerAddress = IPEndPoint.Parse(@"2.2.2.2:1919");
+		private static readonly IPEndPoint ChangedAddress1 = IPEndPoint.Parse(@"3.3.3.3:23333");
+		private static readonly IPEndPoint ChangedAddress2 = IPEndPoint.Parse(@"2.2.2.2:810");
+
+		private static readonly StunMessage5389 DefaultStunMessage = new();
+
+		[TestMethod]
+		public async Task UdpBlockedTestAsync()
+		{
+			var nullMessage = new StunResponse { Message = null, Remote = Any };
+			var nullRemote = new StunResponse { Message = DefaultStunMessage, Remote = null };
+
+			var mock = new Mock<StunClient3489>(IPAddress.Any, Port, null, null);
+			var client = mock.Object;
+
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).Returns(null);
+			await TestAsync();
+
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(nullMessage);
+			await TestAsync();
+
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(nullRemote);
+			await TestAsync();
+
+			async Task TestAsync()
+			{
+				Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+				await client.QueryAsync();
+				Assert.AreEqual(NatType.UdpBlocked, client.Status.NatType);
+				client.Status.Reset();
+			}
+		}
+
+		[TestMethod]
+		public async Task UnsupportedServerTest1Async()
+		{
+			var mock = new Mock<StunClient3489>(IPAddress.Any, Port, null, null);
+			var client = mock.Object;
+
+			var unknownResponse = new StunResponse { Message = DefaultStunMessage, Remote = Any };
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(unknownResponse);
+			await TestAsync();
+
+			var r1 = new StunResponse
+			{
+				Message = new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)
+					}
+				},
+				Remote = Any
+			};
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r1);
+			await TestAsync();
+
+			var r2 = new StunResponse
+			{
+				Message = new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
+					}
+				},
+				Remote = Any
+			};
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r2);
+			await TestAsync();
+
+			var r3 = new StunResponse
+			{
+				Message = new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+						BuildChangeAddress(IpFamily.IPv4, ServerAddress.Address, (ushort)ChangedAddress1.Port)
+					}
+				},
+				Remote = ServerAddress
+			};
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r3);
+			await TestAsync();
+
+			var r4 = new StunResponse
+			{
+				Message = new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ServerAddress.Port)
+					}
+				},
+				Remote = ServerAddress
+			};
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r4);
+			await TestAsync();
+
+			async Task TestAsync()
+			{
+				Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+				await client.QueryAsync();
+				Assert.AreEqual(NatType.UnsupportedServer, client.Status.NatType);
+				client.Status.Reset();
+			}
+		}
+
+		[TestMethod]
+		public async Task Test1Async()
+		{
+			var ip = await _dnsClient.QueryAsync(Server);
+			using var client = new StunClient3489(ip);
+			var response = await client.Test1Async(default);
+
+			Assert.IsNotNull(response);
+			Assert.IsNotNull(response.Message);
+			Assert.IsNotNull(response.Remote);
+			Assert.IsNotNull(response.LocalAddress);
+
+			Assert.AreEqual(ip, response.Remote.Address);
+			Assert.AreEqual(Port, response.Remote.Port);
+
+			var mappedAddress = response.Message.GetMappedAddressAttribute();
+			var changedAddress = response.Message.GetChangedAddressAttribute();
+
+			Assert.IsNotNull(mappedAddress);
+			Assert.IsNotNull(changedAddress);
+
+			Assert.AreNotEqual(ip, changedAddress.Address);
+			Assert.AreNotEqual(Port, changedAddress.Port);
+		}
+	}
+}

+ 0 - 136
UnitTest/UnitTest.cs

@@ -1,136 +0,0 @@
-using Dns.Net.Abstractions;
-using Dns.Net.Clients;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using STUN.Client;
-using STUN.Enums;
-using STUN.Proxy;
-using System;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace UnitTest
-{
-	[TestClass]
-	public class UnitTest
-	{
-		private readonly IDnsClient dnsClient = new DefaultDnsClient();
-
-		[TestMethod]
-		public async Task BindingTest()
-		{
-			var server = await dnsClient.QueryAsync(@"stun.syncthing.net");
-			using var client = new StunClient5389UDP(server, 3478, new IPEndPoint(IPAddress.Any, 0));
-			await client.BindingTestAsync();
-			var result = client.Status;
-
-			Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
-			Assert.IsNotNull(result.LocalEndPoint);
-			Assert.IsNotNull(result.PublicEndPoint);
-			Assert.IsNotNull(result.OtherEndPoint);
-			Assert.AreNotEqual(result.LocalEndPoint!.Address, IPAddress.Any);
-			Assert.AreEqual(result.MappingBehavior, MappingBehavior.Unknown);
-			Assert.AreEqual(result.FilteringBehavior, FilteringBehavior.Unknown);
-		}
-
-		[TestMethod]
-		public async Task MappingBehaviorTest()
-		{
-			var server = await dnsClient.QueryAsync(@"stun.syncthing.net");
-			using var client = new StunClient5389UDP(server, 3478, new IPEndPoint(IPAddress.Any, 0));
-			await client.MappingBehaviorTestAsync();
-			var result = client.Status;
-
-			Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
-			Assert.IsNotNull(result.LocalEndPoint);
-			Assert.IsNotNull(result.PublicEndPoint);
-			Assert.IsNotNull(result.OtherEndPoint);
-			Assert.AreNotEqual(result.LocalEndPoint!.Address, IPAddress.Any);
-			Assert.IsTrue(result.MappingBehavior is
-				MappingBehavior.Direct or
-				MappingBehavior.EndpointIndependent or
-				MappingBehavior.AddressDependent or
-				MappingBehavior.AddressAndPortDependent
-			);
-			Assert.AreEqual(result.FilteringBehavior, FilteringBehavior.Unknown);
-		}
-
-		[TestMethod]
-		public async Task FilteringBehaviorTest()
-		{
-			var server = await dnsClient.QueryAsync(@"stun.syncthing.net");
-			using var client = new StunClient5389UDP(server, 3478, new IPEndPoint(IPAddress.Any, 0));
-			await client.FilteringBehaviorTestAsync();
-			var result = client.Status;
-
-			Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
-			Assert.IsNotNull(result.LocalEndPoint);
-			Assert.IsNotNull(result.PublicEndPoint);
-			Assert.IsNotNull(result.OtherEndPoint);
-			Assert.AreNotEqual(result.LocalEndPoint!.Address, IPAddress.Any);
-			Assert.AreEqual(result.MappingBehavior, MappingBehavior.Unknown);
-			Assert.IsTrue(result.FilteringBehavior is
-				FilteringBehavior.EndpointIndependent or
-				FilteringBehavior.AddressDependent or
-				FilteringBehavior.AddressAndPortDependent
-			);
-		}
-
-		[TestMethod]
-		public async Task CombiningTest()
-		{
-			var server = await dnsClient.QueryAsync(@"stun.syncthing.net");
-			using var client = new StunClient5389UDP(server, 3478, new IPEndPoint(IPAddress.Any, 0));
-			await client.QueryAsync();
-			var result = client.Status;
-
-			Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
-			Assert.IsNotNull(result.LocalEndPoint);
-			Assert.IsNotNull(result.PublicEndPoint);
-			Assert.IsNotNull(result.OtherEndPoint);
-			Assert.AreNotEqual(result.LocalEndPoint!.Address, IPAddress.Any);
-			Assert.IsTrue(result.MappingBehavior is
-				MappingBehavior.Direct or
-				MappingBehavior.EndpointIndependent or
-				MappingBehavior.AddressDependent or
-				MappingBehavior.AddressAndPortDependent
-			);
-			Assert.IsTrue(result.FilteringBehavior is
-				FilteringBehavior.EndpointIndependent or
-				FilteringBehavior.AddressDependent or
-				FilteringBehavior.AddressAndPortDependent
-			);
-		}
-
-		[TestMethod]
-		public async Task ProxyTest()
-		{
-			using var proxy = ProxyFactory.CreateProxy(ProxyType.Socks5, IPEndPoint.Parse(@"0.0.0.0:0"), IPEndPoint.Parse(@"127.0.0.1:10000"));
-			var server = await dnsClient.QueryAsync(@"stun.syncthing.net");
-			using var client = new StunClient5389UDP(server, 3478, new IPEndPoint(IPAddress.Any, 0));
-			await client.QueryAsync();
-			var result = client.Status;
-
-			Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
-			Assert.IsNotNull(result.LocalEndPoint);
-			Assert.IsNotNull(result.PublicEndPoint);
-			Assert.IsNotNull(result.OtherEndPoint);
-			Assert.AreNotEqual(result.LocalEndPoint!.Address, IPAddress.Any);
-			Assert.IsTrue(
-				result.MappingBehavior is MappingBehavior.Direct
-				or MappingBehavior.EndpointIndependent
-				or MappingBehavior.AddressDependent
-				or MappingBehavior.AddressAndPortDependent);
-			Assert.IsTrue(
-				result.FilteringBehavior is FilteringBehavior.EndpointIndependent
-				or FilteringBehavior.AddressDependent
-				or FilteringBehavior.AddressAndPortDependent);
-
-			Console.WriteLine(result.BindingTestResult);
-			Console.WriteLine(result.MappingBehavior);
-			Console.WriteLine(result.FilteringBehavior);
-			Console.WriteLine(result.OtherEndPoint);
-			Console.WriteLine(result.LocalEndPoint);
-			Console.WriteLine(result.PublicEndPoint);
-		}
-	}
-}

+ 1 - 0
UnitTest/UnitTest.csproj

@@ -9,6 +9,7 @@
   <ItemGroup>
     <PackageReference Include="Dns.Net" Version="0.1.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
+    <PackageReference Include="Moq" Version="4.16.1" />
     <PackageReference Include="MSTest.TestAdapter" Version="2.2.5" />
     <PackageReference Include="MSTest.TestFramework" Version="2.2.5" />
     <PackageReference Include="coverlet.collector" Version="3.1.0">