瀏覽代碼

test: Add more tests for RFC3489 client

Bruce Wayne 4 年之前
父節點
當前提交
8b224f65dd

+ 22 - 2
.github/workflows/CI.yaml

@@ -27,9 +27,29 @@ jobs:
       - name: Run dotnet format check
         run: dotnet format -wsa -v diag --check --no-restore
 
+  test:
+    name: Test
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest, macos-latest]
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+
+      - name: Setup .NET
+        uses: actions/setup-dotnet@v1
+        with:
+          dotnet-version: 5.0.x
+
+      - name: Test
+        shell: pwsh
+        run: dotnet test -c Release UnitTest
+
   build:
     name: Build
-    needs: check_format
+    needs: [test, check_format]
     if: ${{ !startsWith(github.ref, 'refs/tags/') }}
     runs-on: windows-latest
     strategy:
@@ -59,7 +79,7 @@ jobs:
 
   nuget:
     name: Nuget
-    needs: check_format
+    needs: [test, check_format]
     if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
     runs-on: ubuntu-latest
     strategy:

+ 16 - 30
STUN/Client/StunClient3489.cs

@@ -19,7 +19,7 @@ namespace STUN.Client
 	/// </summary>
 	public class StunClient3489 : IDisposable
 	{
-		public IPEndPoint LocalEndPoint => _proxy.LocalEndPoint;
+		public virtual IPEndPoint LocalEndPoint => _proxy.LocalEndPoint;
 
 		public TimeSpan Timeout
 		{
@@ -27,10 +27,7 @@ namespace STUN.Client
 			set => _proxy.Timeout = value;
 		}
 
-		private readonly IPAddress _server;
-		private readonly ushort _port;
-
-		private IPEndPoint RemoteEndPoint => new(_server, _port);
+		private readonly IPEndPoint _remoteEndPoint;
 
 		private readonly IUdpProxy _proxy;
 
@@ -43,8 +40,7 @@ namespace STUN.Client
 
 			_proxy = proxy ?? new NoneUdpProxy(local);
 
-			_server = server;
-			_port = port;
+			_remoteEndPoint = new IPEndPoint(server, port);
 
 			Timeout = TimeSpan.FromSeconds(3);
 			Status.LocalEndPoint = local;
@@ -73,13 +69,13 @@ namespace STUN.Client
 
 				// test I
 				var response1 = await Test1Async(cancellationToken);
-				if (response1?.Message is null || response1.Remote is null)
+				if (response1 is null)
 				{
 					Status.NatType = NatType.UdpBlocked;
 					return;
 				}
 
-				Status.LocalEndPoint = response1.LocalAddress is null ? null : new IPEndPoint(response1.LocalAddress, LocalEndPoint.Port);
+				Status.LocalEndPoint = new IPEndPoint(response1.LocalAddress, LocalEndPoint.Port);
 
 				var mappedAddress1 = response1.Message.GetMappedAddressAttribute();
 				var changedAddress = response1.Message.GetChangedAddressAttribute();
@@ -103,7 +99,7 @@ namespace STUN.Client
 				if (Equals(mappedAddress1.Address, response1.LocalAddress) && mappedAddress1.Port == LocalEndPoint.Port)
 				{
 					// No NAT
-					if (response2?.Message is null)
+					if (response2 is null)
 					{
 						Status.NatType = NatType.SymmetricUdpFirewall;
 						Status.PublicEndPoint = mappedAddress1;
@@ -117,7 +113,7 @@ namespace STUN.Client
 				}
 
 				// NAT
-				if (response2?.Message is not null && response2.Remote is not null)
+				if (response2 is not null)
 				{
 					// 有些单 IP 服务器并不能测 NAT 类型,比如 Google 的
 					var type = Equals(response1.Remote.Address, response2.Remote.Address) || response1.Remote.Port == response2.Remote.Port ? NatType.UnsupportedServer : NatType.FullCone;
@@ -145,21 +141,17 @@ namespace STUN.Client
 
 				// Test III
 				var response3 = await Test3Async(cancellationToken);
-				var mappedAddress3 = response3?.Message.GetMappedAddressAttribute();
-				if (mappedAddress3 is not null && response3?.Remote is not null)
+				if (response3 is not null)
 				{
-					if (Equals(response3.Remote.Address, response1.Remote.Address) && response3.Remote.Port != response1.Remote.Port)
+					var mappedAddress3 = response3.Message.GetMappedAddressAttribute();
+					if (mappedAddress3 is not null
+						&& 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;
@@ -186,13 +178,7 @@ namespace STUN.Client
 				var message = new StunMessage5389();
 				if (message.TryParse(receiveBuffer) && message.IsSameTransaction(sendMessage))
 				{
-					var response = new StunResponse
-					{
-						Message = message,
-						Remote = ipe,
-						LocalAddress = local
-					};
-					return response;
+					return new StunResponse(message, ipe, local);
 				}
 			}
 			catch (Exception ex)
@@ -209,7 +195,7 @@ namespace STUN.Client
 				StunMessageType = StunMessageType.BindingRequest,
 				MagicCookie = 0
 			};
-			return await RequestAsync(message, RemoteEndPoint, RemoteEndPoint, cancellationToken);
+			return await RequestAsync(message, _remoteEndPoint, _remoteEndPoint, cancellationToken);
 		}
 
 		public virtual async ValueTask<StunResponse?> Test2Async(IPEndPoint other, CancellationToken cancellationToken)
@@ -220,7 +206,7 @@ namespace STUN.Client
 				MagicCookie = 0,
 				Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
 			};
-			return await RequestAsync(message, RemoteEndPoint, other, cancellationToken);
+			return await RequestAsync(message, _remoteEndPoint, other, cancellationToken);
 		}
 
 		public virtual async ValueTask<StunResponse?> Test1_2Async(IPEndPoint other, CancellationToken cancellationToken)
@@ -241,7 +227,7 @@ namespace STUN.Client
 				MagicCookie = 0,
 				Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
 			};
-			return await RequestAsync(message, RemoteEndPoint, RemoteEndPoint, cancellationToken);
+			return await RequestAsync(message, _remoteEndPoint, _remoteEndPoint, cancellationToken);
 		}
 
 		public void Dispose()

+ 2 - 2
STUN/Client/StunClient5389UDP.cs

@@ -113,8 +113,8 @@ namespace STUN.Client
 			var result = new StunResult5389();
 			var test = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest };
 			var (response1, _, local1) = await TestAsync(test, remote, remote, token);
-			var mappedAddress1 = response1.GetXorMappedAddressAttribute();
-			var otherAddress = response1.GetOtherAddressAttribute();
+			var mappedAddress1 = response1?.GetXorMappedAddressAttribute();
+			var otherAddress = response1?.GetOtherAddressAttribute();
 			var local = local1 is null ? null : new IPEndPoint(local1, LocalEndPoint.Port);
 
 			if (response1 is null)

+ 10 - 3
STUN/Messages/StunResponse.cs

@@ -4,8 +4,15 @@ namespace STUN.Messages
 {
 	public class StunResponse
 	{
-		public StunMessage5389? Message { get; set; }
-		public IPEndPoint? Remote { get; set; }
-		public IPAddress? LocalAddress { get; set; }
+		public StunMessage5389 Message { get; set; }
+		public IPEndPoint Remote { get; set; }
+		public IPAddress LocalAddress { get; set; }
+
+		public StunResponse(StunMessage5389 message, IPEndPoint remote, IPAddress localAddress)
+		{
+			Message = message;
+			Remote = remote;
+			LocalAddress = localAddress;
+		}
 	}
 }

+ 10 - 10
STUN/Utils/AttributeExtensions.cs

@@ -61,9 +61,9 @@ namespace STUN.Utils
 			};
 		}
 
-		public static IPEndPoint? GetMappedAddressAttribute(this StunMessage5389? response)
+		public static IPEndPoint? GetMappedAddressAttribute(this StunMessage5389 response)
 		{
-			var mappedAddressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
+			var mappedAddressAttribute = response.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
 
 			if (mappedAddressAttribute is null)
 			{
@@ -74,9 +74,9 @@ namespace STUN.Utils
 			return new IPEndPoint(mapped.Address!, mapped.Port);
 		}
 
-		public static IPEndPoint? GetChangedAddressAttribute(this StunMessage5389? response)
+		public static IPEndPoint? GetChangedAddressAttribute(this StunMessage5389 response)
 		{
-			var changedAddressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
+			var changedAddressAttribute = response.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
 
 			if (changedAddressAttribute is null)
 			{
@@ -87,11 +87,11 @@ namespace STUN.Utils
 			return new IPEndPoint(address.Address!, address.Port);
 		}
 
-		public static IPEndPoint? GetXorMappedAddressAttribute(this StunMessage5389? response)
+		public static IPEndPoint? GetXorMappedAddressAttribute(this StunMessage5389 response)
 		{
 			var mappedAddressAttribute =
-				response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.XorMappedAddress) ??
-				response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
+				response.Attributes.FirstOrDefault(t => t.Type == AttributeType.XorMappedAddress) ??
+				response.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
 
 			if (mappedAddressAttribute is null)
 			{
@@ -102,11 +102,11 @@ namespace STUN.Utils
 			return new IPEndPoint(mapped.Address!, mapped.Port);
 		}
 
-		public static IPEndPoint? GetOtherAddressAttribute(this StunMessage5389? response)
+		public static IPEndPoint? GetOtherAddressAttribute(this StunMessage5389 response)
 		{
 			var addressAttribute =
-				response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.OtherAddress) ??
-				response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
+				response.Attributes.FirstOrDefault(t => t.Type == AttributeType.OtherAddress) ??
+				response.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
 
 			if (addressAttribute is null)
 			{

+ 314 - 55
UnitTest/StunClient3489Test.cs

@@ -7,6 +7,7 @@ using STUN.Enums;
 using STUN.Messages;
 using STUN.Utils;
 using System.Net;
+using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using static STUN.Utils.AttributeExtensions;
@@ -22,6 +23,8 @@ namespace UnitTest
 		private const ushort Port = 3478;
 
 		private static readonly IPEndPoint Any = new(IPAddress.Any, 0);
+		private static readonly IPEndPoint IPv6Any = new(IPAddress.IPv6Any, 0);
+		private static readonly IPEndPoint LocalAddress1 = IPEndPoint.Parse(@"127.0.0.1:114");
 		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");
@@ -33,99 +36,207 @@ namespace UnitTest
 		[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>())).ReturnsAsync((StunResponse?)null);
+
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.UdpBlocked, client.Status.NatType);
+		}
 
+		[TestMethod]
+		public async Task UnsupportedServerTestAsync()
+		{
 			var mock = new Mock<StunClient3489>(IPAddress.Any, Port, null, null);
 			var client = mock.Object;
 
-			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).Returns(null);
+			mock.Setup(x => x.LocalEndPoint).Returns(LocalAddress1);
+			var unknownResponse = new StunResponse(DefaultStunMessage, Any, LocalAddress1.Address);
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(unknownResponse);
+			await TestAsync();
+
+			var r1 = new StunResponse(new StunMessage5389
+			{
+				Attributes = new[]
+				{
+					BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)
+				}
+			}, ServerAddress, LocalAddress1.Address);
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r1);
+			await TestAsync();
+
+			var r2 = new StunResponse(new StunMessage5389
+			{
+				Attributes = new[]
+				{
+					BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
+				}
+			}, ServerAddress, LocalAddress1.Address);
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r2);
 			await TestAsync();
 
-			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(nullMessage);
+			var r3 = new StunResponse(new StunMessage5389
+			{
+				Attributes = new[]
+				{
+					BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+					BuildChangeAddress(IpFamily.IPv4, ServerAddress.Address, (ushort)ChangedAddress1.Port)
+				}
+			}, ServerAddress, LocalAddress1.Address);
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r3);
 			await TestAsync();
 
-			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(nullRemote);
+			var r4 = new StunResponse(new StunMessage5389
+			{
+				Attributes = new[]
+				{
+					BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+					BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ServerAddress.Port)
+				}
+			}, ServerAddress, LocalAddress1.Address);
+			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.UdpBlocked, client.Status.NatType);
+				Assert.AreEqual(NatType.UnsupportedServer, client.Status.NatType);
 				client.Status.Reset();
 			}
 		}
 
 		[TestMethod]
-		public async Task UnsupportedServerTest1Async()
+		public async Task NoNatTestAsync()
 		{
 			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
+			var openInternetTest1Response = new StunResponse(
+				new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
+					}
+				},
+				ServerAddress,
+				MappedAddress1.Address
+			);
+			var test2Response = new StunResponse(
+				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();
+				ChangedAddress1,
+				MappedAddress1.Address
+			);
 
-			var r2 = new StunResponse
-			{
-				Message = new StunMessage5389
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(openInternetTest1Response);
+			mock.Setup(x => x.LocalEndPoint).Returns(MappedAddress1);
+			mock.Setup(x => x.Test2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(test2Response);
+
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.OpenInternet, client.Status.NatType);
+			client.Status.Reset();
+
+			mock.Setup(x => x.Test2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync((StunResponse?)null);
+
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.SymmetricUdpFirewall, client.Status.NatType);
+			client.Status.Reset();
+		}
+
+		[TestMethod]
+		public async Task FullConeTestAsync()
+		{
+			var mock = new Mock<StunClient3489>(IPAddress.Any, Port, null, null);
+			var client = mock.Object;
+
+			var test1Response = new StunResponse(
+				new StunMessage5389
 				{
 					Attributes = new[]
 					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
 						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
+				ServerAddress,
+				LocalAddress1.Address
+			);
+			var fullConeResponse = new StunResponse(
+				new StunMessage5389
 				{
 					Attributes = new[]
 					{
-						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
-						BuildChangeAddress(IpFamily.IPv4, ServerAddress.Address, (ushort)ChangedAddress1.Port)
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)
 					}
 				},
-				Remote = ServerAddress
-			};
-			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r3);
-			await TestAsync();
-
-			var r4 = new StunResponse
-			{
-				Message = new StunMessage5389
+				ChangedAddress1,
+				LocalAddress1.Address
+			);
+			var unsupportedResponse1 = new StunResponse(
+				new StunMessage5389
 				{
 					Attributes = new[]
 					{
-						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
-						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ServerAddress.Port)
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)
 					}
 				},
-				Remote = ServerAddress
-			};
-			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r4);
-			await TestAsync();
+				ServerAddress,
+				LocalAddress1.Address
+			);
+			var unsupportedResponse2 = new StunResponse(
+				new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)
+					}
+				},
+				new IPEndPoint(ServerAddress.Address, ChangedAddress1.Port),
+				LocalAddress1.Address
+			);
+			var unsupportedResponse3 = new StunResponse(
+				new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)
+					}
+				},
+				new IPEndPoint(ChangedAddress1.Address, ServerAddress.Port),
+				LocalAddress1.Address
+			);
 
-			async Task TestAsync()
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(test1Response);
+			mock.Setup(x => x.LocalEndPoint).Returns(LocalAddress1);
+			mock.Setup(x => x.Test2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(fullConeResponse);
+
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.FullCone, client.Status.NatType);
+			client.Status.Reset();
+
+			mock.Setup(x => x.Test2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(unsupportedResponse1);
+			await TestUnsupportedServerAsync();
+
+			mock.Setup(x => x.Test2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(unsupportedResponse2);
+			await TestUnsupportedServerAsync();
+
+			mock.Setup(x => x.Test2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(unsupportedResponse3);
+			await TestUnsupportedServerAsync();
+
+			async Task TestUnsupportedServerAsync()
 			{
 				Assert.AreEqual(NatType.Unknown, client.Status.NatType);
 				await client.QueryAsync();
@@ -134,29 +245,177 @@ namespace UnitTest
 			}
 		}
 
+		[TestMethod]
+		public async Task SymmetricTestAsync()
+		{
+			var mock = new Mock<StunClient3489>(IPAddress.Any, Port, null, null);
+			var client = mock.Object;
+
+			var test1Response = new StunResponse(
+				new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
+					}
+				},
+				ServerAddress,
+				LocalAddress1.Address
+			);
+			var test12Response = new StunResponse(
+				new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress2.Address, (ushort)MappedAddress2.Port),
+						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
+					}
+				},
+				ServerAddress,
+				LocalAddress1.Address
+			);
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(test1Response);
+			mock.Setup(x => x.LocalEndPoint).Returns(LocalAddress1);
+			mock.Setup(x => x.Test2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync((StunResponse?)null);
+			mock.Setup(x => x.Test1_2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync((StunResponse?)null);
+
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			client.Status.Reset();
+
+			mock.Setup(x => x.Test1_2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(test12Response);
+
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.Symmetric, client.Status.NatType);
+		}
+
+		[TestMethod]
+		public async Task RestrictedConeTestAsync()
+		{
+			var mock = new Mock<StunClient3489>(IPAddress.Any, Port, null, null);
+			var client = mock.Object;
+
+			var test1Response = new StunResponse(
+				new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
+					}
+				},
+				ServerAddress,
+				LocalAddress1.Address
+			);
+			var test3Response = new StunResponse(
+				new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
+					}
+				},
+				ChangedAddress2,
+				LocalAddress1.Address
+			);
+			var test3ErrorResponse = new StunResponse(
+				new StunMessage5389
+				{
+					Attributes = new[]
+					{
+						BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
+						BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
+					}
+				},
+				ServerAddress,
+				LocalAddress1.Address
+			);
+			mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(test1Response);
+			mock.Setup(x => x.LocalEndPoint).Returns(LocalAddress1);
+			mock.Setup(x => x.Test2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync((StunResponse?)null);
+			mock.Setup(x => x.Test1_2Async(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(test1Response);
+
+			mock.Setup(x => x.Test3Async(It.IsAny<CancellationToken>())).ReturnsAsync(test3Response);
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.RestrictedCone, client.Status.NatType);
+			client.Status.Reset();
+
+			mock.Setup(x => x.Test3Async(It.IsAny<CancellationToken>())).ReturnsAsync(test3ErrorResponse);
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.PortRestrictedCone, client.Status.NatType);
+			client.Status.Reset();
+
+			mock.Setup(x => x.Test3Async(It.IsAny<CancellationToken>())).ReturnsAsync((StunResponse?)null);
+			Assert.AreEqual(NatType.Unknown, client.Status.NatType);
+			await client.QueryAsync();
+			Assert.AreEqual(NatType.PortRestrictedCone, client.Status.NatType);
+		}
+
 		[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);
+			// test I
+			var response1 = await client.Test1Async(default);
 
-			Assert.AreEqual(ip, response.Remote.Address);
-			Assert.AreEqual(Port, response.Remote.Port);
+			Assert.IsNotNull(response1);
+			Assert.AreEqual(ip, response1.Remote.Address);
+			Assert.AreEqual(Port, response1.Remote.Port);
+			Assert.AreNotEqual(Any, client.LocalEndPoint);
 
-			var mappedAddress = response.Message.GetMappedAddressAttribute();
-			var changedAddress = response.Message.GetChangedAddressAttribute();
+			var mappedAddress = response1.Message.GetMappedAddressAttribute();
+			var changedAddress = response1.Message.GetChangedAddressAttribute();
 
 			Assert.IsNotNull(mappedAddress);
 			Assert.IsNotNull(changedAddress);
 
 			Assert.AreNotEqual(ip, changedAddress.Address);
 			Assert.AreNotEqual(Port, changedAddress.Port);
+
+			// Test I(#2)
+			var response12 = await client.Test1_2Async(changedAddress, default);
+
+			Assert.IsNotNull(response12);
+			Assert.AreEqual(changedAddress.Address, response12.Remote.Address);
+			Assert.AreEqual(changedAddress.Port, response12.Remote.Port);
+		}
+
+#if FullCone
+		[TestMethod]
+#endif
+		public async Task Test2Async()
+		{
+			var ip = await _dnsClient.QueryAsync(Server);
+			using var client = new StunClient3489(ip);
+			var response2 = await client.Test2Async(ip.AddressFamily is AddressFamily.InterNetworkV6 ? IPv6Any : Any, default);
+
+			Assert.IsNotNull(response2);
+
+			Assert.AreNotEqual(ip, response2.Remote.Address);
+			Assert.AreNotEqual(Port, response2.Remote.Port);
+		}
+
+#if FullCone
+		[TestMethod]
+#endif
+		public async Task Test3Async()
+		{
+			var ip = await _dnsClient.QueryAsync(Server);
+			using var client = new StunClient3489(ip);
+			var response = await client.Test3Async(default);
+
+			Assert.IsNotNull(response);
+
+			Assert.AreEqual(ip, response.Remote.Address);
+			Assert.AreNotEqual(Port, response.Remote.Port);
 		}
 	}
 }