Browse Source

Merge branch 'reactiveui'

Bruce Wayne 4 years ago
parent
commit
1da57800ac
79 changed files with 2907 additions and 2533 deletions
  1. 25 0
      .github/dependabot.yml
  2. 6 3
      NatTypeTester-Console/NatTypeTester_Console.csproj
  3. 37 33
      NatTypeTester-Console/Program.cs
  4. 23 0
      NatTypeTester.Models/Config.cs
  5. 3 0
      NatTypeTester.Models/FodyWeavers.xml
  6. 17 0
      NatTypeTester.Models/NatTypeTester.Models.csproj
  7. 3 0
      NatTypeTester.ViewModels/FodyWeavers.xml
  8. 71 0
      NatTypeTester.ViewModels/MainWindowViewModel.cs
  9. 17 0
      NatTypeTester.ViewModels/NatTypeTester.ViewModels.csproj
  10. 60 0
      NatTypeTester.ViewModels/RFC3489ViewModel.cs
  11. 63 0
      NatTypeTester.ViewModels/RFC5780ViewModel.cs
  12. 19 0
      NatTypeTester.ViewModels/SettingViewModel.cs
  13. 12 0
      NatTypeTester.sln
  14. 2 2
      NatTypeTester/App.xaml
  15. 12 13
      NatTypeTester/App.xaml.cs
  16. 13 0
      NatTypeTester/Dialogs/DisposableContentDialog.cs
  17. 2 2
      NatTypeTester/FodyWeavers.xml
  18. 24 112
      NatTypeTester/MainWindow.xaml
  19. 64 132
      NatTypeTester/MainWindow.xaml.cs
  20. 0 96
      NatTypeTester/Model/StunServer.cs
  21. 8 6
      NatTypeTester/NatTypeTester.csproj
  22. 34 0
      NatTypeTester/Services/DI.cs
  23. 41 0
      NatTypeTester/Services/ServiceExtensions.cs
  24. 20 0
      NatTypeTester/Utils/Extensions.cs
  25. 0 271
      NatTypeTester/ViewModels/MainWindowViewModel.cs
  26. 37 0
      NatTypeTester/Views/RFC3489View.xaml
  27. 56 0
      NatTypeTester/Views/RFC3489View.xaml.cs
  28. 51 0
      NatTypeTester/Views/RFC5780View.xaml
  29. 70 0
      NatTypeTester/Views/RFC5780View.xaml.cs
  30. 47 0
      NatTypeTester/Views/SettingView.xaml
  31. 36 0
      NatTypeTester/Views/SettingView.xaml.cs
  32. 0 55
      STUN/Client/DefaultDnsQuery.cs
  33. 213 227
      STUN/Client/StunClient3489.cs
  34. 252 290
      STUN/Client/StunClient5389UDP.cs
  35. 64 0
      STUN/DnsClients/DefaultDnsQuery.cs
  36. 11 0
      STUN/DnsClients/IDnsQuery.cs
  37. 42 42
      STUN/Enums/AttributeType.cs
  38. 8 8
      STUN/Enums/BindingTestResult.cs
  39. 9 9
      STUN/Enums/Class.cs
  40. 10 10
      STUN/Enums/FilteringBehavior.cs
  41. 10 10
      STUN/Enums/IpFamily.cs
  42. 11 11
      STUN/Enums/MappingBehavior.cs
  43. 7 7
      STUN/Enums/Method.cs
  44. 56 56
      STUN/Enums/NatType.cs
  45. 7 7
      STUN/Enums/ProxyType.cs
  46. 34 34
      STUN/Enums/StunMessageType.cs
  47. 9 9
      STUN/Enums/TransportType.cs
  48. 3 0
      STUN/FodyWeavers.xml
  49. 0 11
      STUN/Interfaces/IDnsQuery.cs
  50. 0 16
      STUN/Interfaces/IUdpProxy.cs
  51. 93 80
      STUN/Message/Attribute.cs
  52. 62 56
      STUN/Message/Attributes/AddressAttribute.cs
  53. 22 19
      STUN/Message/Attributes/ChangeRequestAttribute.cs
  54. 6 6
      STUN/Message/Attributes/ChangedAddressAttribute.cs
  55. 42 39
      STUN/Message/Attributes/ErrorCodeAttribute.cs
  56. 5 5
      STUN/Message/Attributes/IAttribute.cs
  57. 6 6
      STUN/Message/Attributes/MappedAddressAttribute.cs
  58. 6 6
      STUN/Message/Attributes/OtherAddressAttribute.cs
  59. 2 2
      STUN/Message/Attributes/ReflectedFromAttribute.cs
  60. 6 6
      STUN/Message/Attributes/ResponseAddressAttribute.cs
  61. 6 6
      STUN/Message/Attributes/SourceAddressAttribute.cs
  62. 37 34
      STUN/Message/Attributes/UnknownAttribute.cs
  63. 16 15
      STUN/Message/Attributes/UselessAttribute.cs
  64. 54 51
      STUN/Message/Attributes/XorMappedAddressAttribute.cs
  65. 68 59
      STUN/Message/StunMessage5389.cs
  66. 16 0
      STUN/Proxy/IUdpProxy.cs
  67. 36 45
      STUN/Proxy/NoneUdpProxy.cs
  68. 17 14
      STUN/Proxy/ProxyFactory.cs
  69. 252 254
      STUN/Proxy/Socks5UdpProxy.cs
  70. 2 1
      STUN/STUN.csproj
  71. 6 19
      STUN/StunResult/ClassicStunResult.cs
  72. 15 0
      STUN/StunResult/StunResult.cs
  73. 36 11
      STUN/StunResult/StunResult5389.cs
  74. 69 55
      STUN/Utils/AttributeExtensions.cs
  75. 38 37
      STUN/Utils/BitUtils.cs
  76. 74 50
      STUN/Utils/NetUtils.cs
  77. 97 0
      STUN/Utils/StunServer.cs
  78. 192 174
      UnitTest/UnitTest.cs
  79. 7 11
      UnitTest/UnitTest.csproj

+ 25 - 0
.github/dependabot.yml

@@ -0,0 +1,25 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "daily"
+      time: "07:00"
+      timezone: "Asia/Shanghai"
+    labels:
+      - "Automatic"
+    open-pull-requests-limit: 99
+  - package-ecosystem: "nuget"
+    directory: "/"
+    schedule:
+      interval: "daily"
+      time: "07:15"
+      timezone: "Asia/Shanghai"
+    labels:
+      - "Automatic"
+    open-pull-requests-limit: 99

+ 6 - 3
NatTypeTester-Console/NatTypeTester_Console.csproj

@@ -1,9 +1,12 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFrameworks>net472;net48;netcoreapp3.1</TargetFrameworks>
-    <RootNamespace>NatTypeTester_Console</RootNamespace>
+    <TargetFrameworks>net48;net5.0</TargetFrameworks>
+    <RootNamespace>NatTypeTester</RootNamespace>
+    <LangVersion>latest</LangVersion>
+    <Nullable>enable</Nullable>
+    <AssemblyName>NatTypeTester.Console</AssemblyName>
   </PropertyGroup>
 
   <ItemGroup>

+ 37 - 33
NatTypeTester-Console/Program.cs

@@ -1,40 +1,44 @@
-using STUN.Utils;
+using STUN.Client;
+using STUN.Utils;
 using System;
 using System.Net;
 using System.Threading.Tasks;
 
-namespace NatTypeTester_Console
+namespace NatTypeTester
 {
-    internal static class Program
-    {
-        /// <summary>
-        /// stun.qq.com 3478 0.0.0.0:0
-        /// </summary>
-        private static async Task Main(string[] args)
-        {
-            var server = @"stun.syncthing.net";
-            ushort port = 3478;
-            IPEndPoint local = null;
-            if (args.Length > 0 && (Uri.CheckHostName(args[0]) == UriHostNameType.Dns || IPAddress.TryParse(args[0], out _)))
-            {
-                server = args[0];
-            }
-            if (args.Length > 1)
-            {
-                ushort.TryParse(args[1], out port);
-            }
-            if (args.Length > 2)
-            {
-                local = NetUtils.ParseEndpoint(args[2]);
-            }
+	internal static class Program
+	{
+		/// <summary>
+		/// stun.qq.com 3478 0.0.0.0:0
+		/// </summary>
+		private static async Task Main(string[] args)
+		{
+			var server = @"stun.syncthing.net";
+			ushort port = 3478;
+			IPEndPoint? local = null;
+			if (args.Length > 0 && (Uri.CheckHostName(args[0]) == UriHostNameType.Dns || IPAddress.TryParse(args[0], out _)))
+			{
+				server = args[0];
+			}
+			if (args.Length > 1)
+			{
+				ushort.TryParse(args[1], out port);
+			}
+			if (args.Length > 2)
+			{
+				local = NetUtils.ParseEndpoint(args[2]);
+			}
 
-            var res = await NetUtils.NatBehaviorDiscovery(server, port, local);
-            Console.WriteLine($@"Other address is {res.OtherEndPoint}");
-            Console.WriteLine($@"Binding test: {res.BindingTestResult}");
-            Console.WriteLine($@"Local address: {res.LocalEndPoint}");
-            Console.WriteLine($@"Mapped address: {res.PublicEndPoint}");
-            Console.WriteLine($@"Nat mapping behavior: {res.MappingBehavior}");
-            Console.WriteLine($@"Nat filtering behavior: {res.FilteringBehavior}");
-        }
-    }
+			using var client = new StunClient5389UDP(server, port, local);
+			await client.QueryAsync();
+			var res = client.Status;
+
+			Console.WriteLine($@"Other address is {res.OtherEndPoint}");
+			Console.WriteLine($@"Binding test: {res.BindingTestResult}");
+			Console.WriteLine($@"Local address: {res.LocalEndPoint}");
+			Console.WriteLine($@"Mapped address: {res.PublicEndPoint}");
+			Console.WriteLine($@"Nat mapping behavior: {res.MappingBehavior}");
+			Console.WriteLine($@"Nat filtering behavior: {res.FilteringBehavior}");
+		}
+	}
 }

+ 23 - 0
NatTypeTester.Models/Config.cs

@@ -0,0 +1,23 @@
+using ReactiveUI.Fody.Helpers;
+using STUN.Enums;
+
+namespace NatTypeTester.Models
+{
+	public class Config
+	{
+		[Reactive]
+		public string StunServer { get; set; } = @"stun.syncthing.net";
+
+		[Reactive]
+		public ProxyType ProxyType { get; set; } = ProxyType.Plain;
+
+		[Reactive]
+		public string ProxyServer { get; set; } = @"127.0.0.1:1080";
+
+		[Reactive]
+		public string? ProxyUser { get; set; }
+
+		[Reactive]
+		public string? ProxyPassword { get; set; }
+	}
+}

+ 3 - 0
NatTypeTester.Models/FodyWeavers.xml

@@ -0,0 +1,3 @@
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+  <ReactiveUI />
+</Weavers>

+ 17 - 0
NatTypeTester.Models/NatTypeTester.Models.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>latest</LangVersion>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="ReactiveUI.Fody" Version="13.0.38" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\STUN\STUN.csproj" />
+  </ItemGroup>
+
+</Project>

+ 3 - 0
NatTypeTester.ViewModels/FodyWeavers.xml

@@ -0,0 +1,3 @@
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+  <ReactiveUI />
+</Weavers>

+ 71 - 0
NatTypeTester.ViewModels/MainWindowViewModel.cs

@@ -0,0 +1,71 @@
+using DynamicData;
+using DynamicData.Binding;
+using NatTypeTester.Models;
+using ReactiveUI;
+using STUN.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+
+namespace NatTypeTester.ViewModels
+{
+	public class MainWindowViewModel : ReactiveObject, IScreen
+	{
+		public RoutingState Router { get; } = new();
+
+		public Config Config { get; }
+
+		private readonly IEnumerable<string> _defaultServers = new HashSet<string>
+		{
+				@"stun.syncthing.net",
+				@"stun.qq.com",
+				@"stun.miwifi.com",
+				@"stun.bige0.com",
+				@"stun.stunprotocol.org"
+		};
+
+		private SourceList<string> List { get; } = new();
+		public readonly IObservableCollection<string> StunServers = new ObservableCollectionExtended<string>();
+
+		public MainWindowViewModel(Config config)
+		{
+			Config = config;
+
+			LoadStunServer();
+			List.Connect()
+				.DistinctValues(x => x)
+				.ObserveOn(RxApp.MainThreadScheduler)
+				.Bind(StunServers)
+				.Subscribe();
+		}
+
+		private async void LoadStunServer()
+		{
+			foreach (var server in _defaultServers)
+			{
+				List.Add(server);
+			}
+			Config.StunServer = _defaultServers.First();
+
+			const string path = @"stun.txt";
+
+			if (!File.Exists(path))
+			{
+				return;
+			}
+
+			using var sw = new StreamReader(path);
+			string line;
+			var stun = new StunServer();
+			while ((line = await sw.ReadLineAsync()) != null)
+			{
+				if (!string.IsNullOrWhiteSpace(line) && stun.Parse(line))
+				{
+					List.Add(stun.ToString());
+				}
+			}
+		}
+	}
+}

+ 17 - 0
NatTypeTester.ViewModels/NatTypeTester.ViewModels.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>latest</LangVersion>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="ReactiveUI.Fody" Version="13.0.38" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\NatTypeTester.Models\NatTypeTester.Models.csproj" />
+  </ItemGroup>
+
+</Project>

+ 60 - 0
NatTypeTester.ViewModels/RFC3489ViewModel.cs

@@ -0,0 +1,60 @@
+using NatTypeTester.Models;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using STUN.Client;
+using STUN.Proxy;
+using STUN.StunResult;
+using STUN.Utils;
+using System;
+using System.Net;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NatTypeTester.ViewModels
+{
+	public class RFC3489ViewModel : ReactiveObject, IRoutableViewModel
+	{
+		public string UrlPathSegment { get; } = @"RFC3489";
+		public IScreen HostScreen { get; }
+
+		private readonly Config _config;
+
+		[Reactive]
+		public ClassicStunResult Result3489 { get; set; }
+
+		public ReactiveCommand<Unit, Unit> TestClassicNatType { get; }
+
+		public RFC3489ViewModel(IScreen hostScreen, Config config)
+		{
+			HostScreen = hostScreen;
+			_config = config;
+
+			Result3489 = new ClassicStunResult { LocalEndPoint = new IPEndPoint(IPAddress.Any, 0) };
+			TestClassicNatType = ReactiveCommand.CreateFromTask(TestClassicNatTypeImpl);
+		}
+
+		private async Task TestClassicNatTypeImpl(CancellationToken token)
+		{
+			var server = new StunServer();
+			if (!server.Parse(_config.StunServer))
+			{
+				throw new Exception(@"Wrong STUN Server!");
+			}
+
+			using var proxy = ProxyFactory.CreateProxy(
+					_config.ProxyType,
+					Result3489.LocalEndPoint,
+					NetUtils.ParseEndpoint(_config.ProxyServer),
+					_config.ProxyUser, _config.ProxyPassword
+			);
+
+			using var client = new StunClient3489(server.Hostname, server.Port, Result3489.LocalEndPoint, proxy);
+
+			Result3489 = client.Status;
+			await client.Query3489Async();
+
+			Result3489.LocalEndPoint = client.LocalEndPoint;
+		}
+	}
+}

+ 63 - 0
NatTypeTester.ViewModels/RFC5780ViewModel.cs

@@ -0,0 +1,63 @@
+using NatTypeTester.Models;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using STUN.Client;
+using STUN.Proxy;
+using STUN.StunResult;
+using STUN.Utils;
+using System;
+using System.Net;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NatTypeTester.ViewModels
+{
+	public class RFC5780ViewModel : ReactiveObject, IRoutableViewModel
+	{
+		public string UrlPathSegment { get; } = @"RFC5780";
+		public IScreen HostScreen { get; }
+
+		private readonly Config _config;
+
+		[Reactive]
+		public StunResult5389 Result5389 { get; set; }
+
+		public ReactiveCommand<Unit, Unit> DiscoveryNatType { get; }
+
+		public RFC5780ViewModel(IScreen hostScreen, Config config)
+		{
+			HostScreen = hostScreen;
+			_config = config;
+
+			Result5389 = new StunResult5389 { LocalEndPoint = new IPEndPoint(IPAddress.Any, 0) };
+			DiscoveryNatType = ReactiveCommand.CreateFromTask(DiscoveryNatTypeImpl);
+		}
+
+		private async Task DiscoveryNatTypeImpl(CancellationToken token)
+		{
+			var server = new StunServer();
+			if (!server.Parse(_config.StunServer))
+			{
+				throw new Exception(@"Wrong STUN Server!");
+			}
+
+			using var proxy = ProxyFactory.CreateProxy(
+					_config.ProxyType,
+					Result5389.LocalEndPoint,
+					NetUtils.ParseEndpoint(_config.ProxyServer),
+					_config.ProxyUser, _config.ProxyPassword
+			);
+
+			using var client = new StunClient5389UDP(server.Hostname, server.Port, Result5389.LocalEndPoint, proxy);
+
+			Result5389 = client.Status;
+			await client.QueryAsync();
+
+			var cache = new StunResult5389();
+			cache.Clone(client.Status);
+			cache.LocalEndPoint = client.LocalEndPoint;
+			Result5389 = cache;
+		}
+	}
+}

+ 19 - 0
NatTypeTester.ViewModels/SettingViewModel.cs

@@ -0,0 +1,19 @@
+using NatTypeTester.Models;
+using ReactiveUI;
+
+namespace NatTypeTester.ViewModels
+{
+	public class SettingViewModel : ReactiveObject, IRoutableViewModel
+	{
+		public string UrlPathSegment { get; } = @"Settings";
+		public IScreen HostScreen { get; }
+
+		public Config Config { get; }
+
+		public SettingViewModel(IScreen hostScreen, Config config)
+		{
+			HostScreen = hostScreen;
+			Config = config;
+		}
+	}
+}

+ 12 - 0
NatTypeTester.sln

@@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "STUN", "STUN\STUN.csproj",
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest", "UnitTest\UnitTest.csproj", "{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NatTypeTester.ViewModels", "NatTypeTester.ViewModels\NatTypeTester.ViewModels.csproj", "{D7626B0E-17B0-4743-888F-A32797442750}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NatTypeTester.Models", "NatTypeTester.Models\NatTypeTester.Models.csproj", "{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -33,6 +37,14 @@ Global
 		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{AA8AF9BF-5CFB-4725-B42A-7B3B3890A279}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D7626B0E-17B0-4743-888F-A32797442750}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FC61BC61-E24B-4E78-ABA4-C6CB6C05EFC2}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 2 - 2
NatTypeTester/App.xaml

@@ -1,4 +1,4 @@
-<Application
+<Application
 	x:Class="NatTypeTester.App"
 	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -12,4 +12,4 @@
 			</ResourceDictionary.MergedDictionaries>
 		</ResourceDictionary>
 	</Application.Resources>
-</Application>
+</Application>

+ 12 - 13
NatTypeTester/App.xaml.cs

@@ -1,17 +1,16 @@
-using ReactiveUI;
-using Splat;
-using System.Reflection;
+using NatTypeTester.Services;
 using System.Windows;
 
 namespace NatTypeTester
 {
-    public partial class App
-    {
-        private void Application_Startup(object sender, StartupEventArgs e)
-        {
-            Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetCallingAssembly());
-            MainWindow = new MainWindow();
-            MainWindow.Show();
-        }
-    }
-}
+	public partial class App
+	{
+		private void Application_Startup(object sender, StartupEventArgs e)
+		{
+			DI.Register();
+
+			MainWindow = DI.GetService<MainWindow>();
+			MainWindow.Show();
+		}
+	}
+}

+ 13 - 0
NatTypeTester/Dialogs/DisposableContentDialog.cs

@@ -0,0 +1,13 @@
+using ModernWpf.Controls;
+using System;
+
+namespace NatTypeTester.Dialogs
+{
+	public class DisposableContentDialog : ContentDialog, IDisposable
+	{
+		public void Dispose()
+		{
+			Hide();
+		}
+	}
+}

+ 2 - 2
NatTypeTester/FodyWeavers.xml

@@ -1,3 +1,3 @@
-<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
   <Costura />
-</Weavers>
+</Weavers>

+ 24 - 112
NatTypeTester/MainWindow.xaml

@@ -1,15 +1,15 @@
-<reactiveUi:ReactiveWindow
+<reactiveUi:ReactiveWindow
 	x:TypeArguments="viewModels:MainWindowViewModel"
 	x:Class="NatTypeTester.MainWindow"
 	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 	xmlns:reactiveUi="http://reactiveui.net"
-	xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels"
+    xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels;assembly=NatTypeTester.ViewModels"
 	xmlns:ui="http://schemas.modernwpf.com/2019"
 	Title="NatTypeTester"
-	Width="500"
 	WindowStartupLocation="CenterScreen"
-	Height="480"
+	Height="480" Width="500"
+	MinHeight="480" MinWidth="500"
 	ui:WindowHelper.UseModernWindowStyle="True">
 
 	<Grid>
@@ -27,112 +27,24 @@
 					</DataTemplate>
 				</ComboBox.ItemTemplate>
 			</ComboBox>
-
-			<TabControl  TabStripPlacement="Left">
-				<TabItem Header="RFC 5780" x:Name="RFC5780Tab">
-					<Grid>
-						<Grid.RowDefinitions>
-							<RowDefinition Height="Auto"/>
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition />
-						</Grid.RowDefinitions>
-						<TextBox
-							x:Name="BindingTestTextBox" Grid.Row="0"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Binding test" />
-						<TextBox
-							x:Name="MappingBehaviorTextBox" Grid.Row="1"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Mapping behavior" />
-						<TextBox
-							x:Name="FilteringBehaviorTextBox" Grid.Row="2"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Filtering behavior" />
-						<TextBox
-							x:Name="LocalAddressTextBox" Grid.Row="3"
-							Margin="10,5" IsReadOnly="False"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Local end" />
-						<TextBox
-							x:Name="MappingAddressTextBox" Grid.Row="4"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Public end" />
-
-						<Button x:Name="DiscoveryButton" Grid.Row="5" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test" Margin="0,10,10,10" />
-					</Grid>
-				</TabItem>
-				<TabItem Header="RFC 3489" x:Name="RFC3489Tab">
-					<Grid>
-						<Grid.RowDefinitions>
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-							<RowDefinition />
-						</Grid.RowDefinitions>
-
-						<TextBox x:Name="NatTypeTextBox" Grid.Row="0"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="NAT type" />
-						<TextBox x:Name="LocalEndTextBox" Grid.Row="1"
-							Margin="10,5"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Local end" />
-						<TextBox x:Name="PublicEndTextBox" Grid.Row="2"
-							Margin="10,5" IsReadOnly="True"
-							VerticalContentAlignment="Center" VerticalAlignment="Center"
-							ui:ControlHelper.Header="Public end" />
-
-						<Button x:Name="TestButton" Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test" Margin="0,0,10,10"/>
-					</Grid>
-				</TabItem>
-				<TabItem Header="Proxy">
-					<Grid>
-						<Grid.RowDefinitions>
-							<RowDefinition Height="Auto" />
-							<RowDefinition Height="Auto" />
-						</Grid.RowDefinitions>
-						<Grid Margin="10,0" Grid.Column="0">
-							<Grid.RowDefinitions>
-								<RowDefinition Height="Auto"/>
-								<RowDefinition Height="Auto"/>
-							</Grid.RowDefinitions>
-							<RadioButton Grid.Row="0" Content="Don't use Proxy" GroupName="ProxyTypeGroup" x:Name="ProxyTypeNoneRadio" IsChecked="True"/>
-							<RadioButton Grid.Row="1" Content="SOCKS5" GroupName="ProxyTypeGroup" x:Name="ProxyTypeSocks5Radio" IsChecked="False" />
-						</Grid>
-						<Grid x:Name="ProxyConfigGrid" Margin="10,5" Grid.Row="1" IsEnabled="False">
-							<Grid.RowDefinitions>
-								<RowDefinition Height="Auto" />
-								<RowDefinition Height="Auto" />
-								<RowDefinition Height="Auto"/>
-							</Grid.RowDefinitions>
-							<TextBox
-								x:Name="ProxyServerTextBox" Grid.Row="0"
-								Margin="0,5" IsReadOnly="False"
-								VerticalContentAlignment="Center" VerticalAlignment="Center"
-								ui:ControlHelper.Header="Server" />
-							<TextBox
-								x:Name="ProxyUsernameTextBox" Grid.Row="1"
-								Margin="0,5" IsReadOnly="False"
-								VerticalContentAlignment="Center" VerticalAlignment="Center"
-								ui:ControlHelper.Header="User ID" />
-							<TextBox 
-								x:Name="ProxyPasswordTextBox" Grid.Row="2"
-								Margin="0,5"
-								VerticalContentAlignment="Center" VerticalAlignment="Center"
-								ui:ControlHelper.Header="Password" />
-						</Grid>
-					</Grid>
-				</TabItem>
-			</TabControl>
-
-		</DockPanel>
+            <ui:NavigationView
+                x:Name="NavigationView"
+                IsBackButtonVisible="Collapsed"
+                PaneDisplayMode="LeftCompact"
+                IsTabStop="False"
+                IsPaneOpen="False">
+                <ui:NavigationView.MenuItems>
+                    <ui:NavigationViewItem Icon="60835" Content="RFC 5780" Tag="1" />
+                    <ui:NavigationViewItem Icon="59753" Content="RFC 3489" Tag="2" />
+                </ui:NavigationView.MenuItems>
+                <reactiveUi:RoutedViewHost
+                    x:Name="RoutedViewHost"
+                    HorizontalContentAlignment="Stretch"
+                    VerticalContentAlignment="Stretch"
+                    Transition="Fade"
+                    Direction="Up"
+                    Duration="0:0:0.3" />
+            </ui:NavigationView>
+        </DockPanel>
 	</Grid>
-</reactiveUi:ReactiveWindow>
+</reactiveUi:ReactiveWindow>

+ 64 - 132
NatTypeTester/MainWindow.xaml.cs

@@ -1,141 +1,73 @@
-using ModernWpf;
+using ModernWpf;
+using ModernWpf.Controls;
 using NatTypeTester.ViewModels;
 using ReactiveUI;
-using STUN.Enums;
 using System;
-using System.Reactive;
+using System.Linq;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
-using System.Windows;
-using System.Windows.Input;
 
 namespace NatTypeTester
 {
-    public partial class MainWindow
-    {
-        public MainWindow()
-        {
-            InitializeComponent();
-            ViewModel = new MainWindowViewModel();
-            ThemeManager.Current.ApplicationTheme = null;
-
-            this.WhenActivated(d =>
-            {
-                #region Server
-
-                this.Bind(ViewModel,
-                        vm => vm.StunServer,
-                        v => v.ServersComboBox.Text
-                ).DisposeWith(d);
-
-                this.OneWayBind(ViewModel,
-                        vm => vm.StunServers,
-                        v => v.ServersComboBox.ItemsSource
-                ).DisposeWith(d);
-
-                #endregion
-
-                #region Proxy
-
-                this.Bind(ViewModel,
-                        vm => vm.ProxyServer,
-                        v => v.ProxyServerTextBox.Text
-                ).DisposeWith(d);
-
-                this.Bind(ViewModel,
-                        vm => vm.ProxyUser,
-                        v => v.ProxyUsernameTextBox.Text
-                ).DisposeWith(d);
-
-                this.Bind(ViewModel,
-                        vm => vm.ProxyPassword,
-                        v => v.ProxyPasswordTextBox.Text
-                ).DisposeWith(d);
-
-                this.WhenAnyValue(x => x.ProxyTypeNoneRadio.IsChecked, x => x.ProxyTypeSocks5Radio.IsChecked)
-                    .Subscribe(values =>
-                    {
-                        ProxyConfigGrid.IsEnabled = !values.Item1.GetValueOrDefault(false);
-                        if (values.Item1.GetValueOrDefault(false))
-                        {
-                            ViewModel.ProxyType = ProxyType.Plain;
-                        }
-                        else if (values.Item2.GetValueOrDefault(false))
-                        {
-                            ViewModel.ProxyType = ProxyType.Socks5;
-                        }
-                    }).DisposeWith(d);
-
-                #endregion
-
-                #region RFC3489
-
-                this.OneWayBind(ViewModel,
-                        vm => vm.ClassicNatType,
-                        v => v.NatTypeTextBox.Text
-                ).DisposeWith(d);
-
-                this.Bind(ViewModel,
-                        vm => vm.LocalEnd,
-                        v => v.LocalEndTextBox.Text
-                ).DisposeWith(d);
-
-                this.OneWayBind(ViewModel,
-                        vm => vm.PublicEnd,
-                        v => v.PublicEndTextBox.Text
-                ).DisposeWith(d);
-
-                this.BindCommand(ViewModel,
-                                viewModel => viewModel.TestClassicNatType,
-                                view => view.TestButton)
-                        .DisposeWith(d);
-
-                RFC3489Tab.Events().KeyDown
-                        .Where(x => x.Key == Key.Enter && TestButton.IsEnabled)
-                        .Subscribe(y => { TestButton.Command.Execute(Unit.Default); })
-                        .DisposeWith(d);
-
-                #endregion
-
-                #region RFC5780
-
-                this.OneWayBind(ViewModel,
-                        vm => vm.BindingTest,
-                        v => v.BindingTestTextBox.Text
-                ).DisposeWith(d);
-
-                this.OneWayBind(ViewModel,
-                        vm => vm.MappingBehavior,
-                        v => v.MappingBehaviorTextBox.Text
-                ).DisposeWith(d);
-
-                this.OneWayBind(ViewModel,
-                        vm => vm.FilteringBehavior,
-                        v => v.FilteringBehaviorTextBox.Text
-                ).DisposeWith(d);
-
-                this.Bind(ViewModel,
-                        vm => vm.LocalAddress,
-                        v => v.LocalAddressTextBox.Text
-                ).DisposeWith(d);
-
-                this.OneWayBind(ViewModel,
-                        vm => vm.MappingAddress,
-                        v => v.MappingAddressTextBox.Text
-                ).DisposeWith(d);
-
-                this.BindCommand(ViewModel,
-                                viewModel => viewModel.DiscoveryNatType,
-                                view => view.DiscoveryButton)
-                        .DisposeWith(d);
-
-                RFC5780Tab.Events().KeyDown
-                        .Where(x => x.Key == Key.Enter && DiscoveryButton.IsEnabled)
-                        .Subscribe(y => { DiscoveryButton.Command.Execute(Unit.Default); })
-                        .DisposeWith(d);
-
-                #endregion
-            });
-        }
-    }
+	public partial class MainWindow
+	{
+		public MainWindow(MainWindowViewModel viewModel,
+			RFC5780ViewModel rfc5780ViewModel,
+			RFC3489ViewModel rfc3489ViewModel,
+			SettingViewModel settingViewModel
+			)
+		{
+			InitializeComponent();
+			ViewModel = viewModel;
+			ThemeManager.Current.ApplicationTheme = null;
+
+			this.WhenActivated(d =>
+			{
+				#region Server
+
+				this.Bind(ViewModel,
+						vm => vm.Config.StunServer,
+						v => v.ServersComboBox.Text
+				).DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+						vm => vm.StunServers,
+						v => v.ServersComboBox.ItemsSource
+				).DisposeWith(d);
+
+				#endregion
+
+				this.Bind(ViewModel, vm => vm.Router, v => v.RoutedViewHost.Router).DisposeWith(d);
+				Observable.FromEventPattern<NavigationViewSelectionChangedEventArgs>(NavigationView, nameof(NavigationView.SelectionChanged))
+				.Subscribe(args =>
+				{
+					if (args.EventArgs.IsSettingsSelected)
+					{
+						ViewModel.Router.Navigate.Execute(settingViewModel);
+						return;
+					}
+
+					if (args.EventArgs.SelectedItem is not NavigationViewItem { Tag: string tag })
+					{
+						return;
+					}
+
+					switch (tag)
+					{
+						case @"1":
+						{
+							ViewModel.Router.Navigate.Execute(rfc5780ViewModel);
+							break;
+						}
+						case @"2":
+						{
+							ViewModel.Router.Navigate.Execute(rfc3489ViewModel);
+							break;
+						}
+					}
+				}).DisposeWith(d);
+				NavigationView.SelectedItem = NavigationView.MenuItems.OfType<NavigationViewItem>().First();
+			});
+		}
+	}
 }

+ 0 - 96
NatTypeTester/Model/StunServer.cs

@@ -1,96 +0,0 @@
-using System;
-using System.Linq;
-using System.Net;
-using System.Net.Sockets;
-
-namespace NatTypeTester.Model
-{
-    public class StunServer
-    {
-        public string Hostname { get; set; }
-        public ushort Port { get; set; }
-
-        public StunServer()
-        {
-            Hostname = @"stun.syncthing.net";
-            Port = 3478;
-        }
-
-        public bool Parse(string str)
-        {
-            var ipPort = str.Trim().Split(':', ':');
-            switch (ipPort.Length)
-            {
-                case 0: return false;
-                case 1:
-                {
-                    var host = ipPort[0].Trim();
-                    if (Uri.CheckHostName(host) != UriHostNameType.Dns && !IPAddress.TryParse(host, out _))
-                    {
-                        return false;
-                    }
-                    Hostname = host;
-                    Port = 3478;
-                    return true;
-                }
-                case 2:
-                {
-                    var host = ipPort[0].Trim();
-                    if (Uri.CheckHostName(host) != UriHostNameType.Dns && !IPAddress.TryParse(host, out _))
-                    {
-                        return false;
-                    }
-                    if (ushort.TryParse(ipPort[1], out var port))
-                    {
-                        Hostname = host;
-                        Port = port;
-                        return true;
-                    }
-                    break;
-                }
-                default:
-                {
-                    if (IPAddress.TryParse(str.Trim(), out var ipv6))
-                    {
-                        Hostname = $@"{ipv6}";
-                        Port = ushort.TryParse(ipPort.Last(), out var portV6) ? portV6 : (ushort)3478;
-                        return true;
-                    }
-
-                    var ipStr = string.Join(@":", ipPort, 0, ipPort.Length - 1);
-                    if (!ipStr.StartsWith(@"[") || !ipStr.EndsWith(@"]") || !IPAddress.TryParse(ipStr, out _))
-                    {
-                        return false;
-                    }
-
-                    if (ushort.TryParse(ipPort.Last(), out var port))
-                    {
-                        Port = port;
-                        return true;
-                    }
-
-                    break;
-                }
-            }
-
-            return false;
-        }
-
-        public override string ToString()
-        {
-            if (string.IsNullOrEmpty(Hostname))
-            {
-                return string.Empty;
-            }
-            if (Port == 3478)
-            {
-                return Hostname;
-            }
-            if (IPAddress.TryParse(Hostname, out var ip) && ip.AddressFamily != AddressFamily.InterNetwork)
-            {
-                return $@"[{Hostname}]:{Port}";
-            }
-            return $@"{Hostname}:{Port}";
-        }
-    }
-}

+ 8 - 6
NatTypeTester/NatTypeTester.csproj

@@ -6,7 +6,8 @@
     <UseWPF>true</UseWPF>
     <ApplicationIcon>icon.ico</ApplicationIcon>
     <LangVersion>latest</LangVersion>
-    <Version>3.3</Version>
+    <Nullable>enable</Nullable>
+    <Version>3.4</Version>
     <Authors>HMBSbige</Authors>
   </PropertyGroup>
 
@@ -22,14 +23,15 @@
     <PackageReference Include="Costura.Fody" Version="4.1.0">
       <PrivateAssets>All</PrivateAssets>
     </PackageReference>
-    <PackageReference Include="Fody" Version="6.3.0" />
-    <PackageReference Include="ModernWpfUI" Version="0.9.2" />
-    <PackageReference Include="ReactiveUI.Events.WPF" Version="11.5.35" />
-    <PackageReference Include="ReactiveUI.WPF" Version="11.5.35" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
+    <PackageReference Include="ModernWpfUI" Version="0.9.3" />
+    <PackageReference Include="ReactiveUI.Events.WPF" Version="13.0.38" />
+    <PackageReference Include="ReactiveUI.WPF" Version="13.0.38" />
+    <PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" Version="9.8.1" />
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\STUN\STUN.csproj" />
+    <ProjectReference Include="..\NatTypeTester.ViewModels\NatTypeTester.ViewModels.csproj" />
   </ItemGroup>
 
 </Project>

+ 34 - 0
NatTypeTester/Services/DI.cs

@@ -0,0 +1,34 @@
+using Microsoft.Extensions.DependencyInjection;
+using ReactiveUI;
+using Splat;
+using Splat.Microsoft.Extensions.DependencyInjection;
+
+namespace NatTypeTester.Services
+{
+	public static class DI
+	{
+		public static T GetService<T>()
+		{
+			return Locator.Current.GetService<T>();
+		}
+
+		public static void Register()
+		{
+			var services = new ServiceCollection();
+
+			services.UseMicrosoftDependencyResolver();
+			Locator.CurrentMutable.InitializeSplat();
+			Locator.CurrentMutable.InitializeReactiveUI(RegistrationNamespace.Wpf);
+
+			ConfigureServices(services);
+		}
+
+		private static IServiceCollection ConfigureServices(IServiceCollection services)
+		{
+			return services
+				.AddViewModels()
+				.AddViews()
+				.AddConfig();
+		}
+	}
+}

+ 41 - 0
NatTypeTester/Services/ServiceExtensions.cs

@@ -0,0 +1,41 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using NatTypeTester.Models;
+using NatTypeTester.ViewModels;
+using NatTypeTester.Views;
+using ReactiveUI;
+
+namespace NatTypeTester.Services
+{
+	public static class ServiceExtensions
+	{
+		public static IServiceCollection AddViewModels(this IServiceCollection services)
+		{
+			services.TryAddSingleton<MainWindowViewModel>();
+			services.TryAddSingleton<RFC5780ViewModel>();
+			services.TryAddSingleton<RFC3489ViewModel>();
+			services.TryAddSingleton<SettingViewModel>();
+
+			services.TryAddSingleton<IScreen>(provider => provider.GetRequiredService<MainWindowViewModel>());
+
+			return services;
+		}
+
+		public static IServiceCollection AddViews(this IServiceCollection services)
+		{
+			services.TryAddSingleton<MainWindow>();
+			services.TryAddTransient<IViewFor<RFC5780ViewModel>, RFC5780View>();
+			services.TryAddTransient<IViewFor<RFC3489ViewModel>, RFC3489View>();
+			services.TryAddTransient<IViewFor<SettingViewModel>, SettingView>();
+
+			return services;
+		}
+
+		public static IServiceCollection AddConfig(this IServiceCollection services)
+		{
+			services.TryAddSingleton<Config>();
+
+			return services;
+		}
+	}
+}

+ 20 - 0
NatTypeTester/Utils/Extensions.cs

@@ -0,0 +1,20 @@
+using NatTypeTester.Dialogs;
+using System;
+using System.Threading.Tasks;
+
+namespace NatTypeTester.Utils
+{
+	public static class Extensions
+	{
+		public static async Task HandleExceptionWithContentDialogAsync(this Exception ex)
+		{
+			using var dialog = new DisposableContentDialog
+			{
+				Title = nameof(NatTypeTester),
+				Content = ex.Message,
+				PrimaryButtonText = @"OK"
+			};
+			await dialog.ShowAsync();
+		}
+	}
+}

+ 0 - 271
NatTypeTester/ViewModels/MainWindowViewModel.cs

@@ -1,271 +0,0 @@
-using DynamicData;
-using DynamicData.Binding;
-using NatTypeTester.Model;
-using ReactiveUI;
-using STUN.Client;
-using STUN.Enums;
-using STUN.Proxy;
-using STUN.Utils;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Reactive;
-using System.Reactive.Linq;
-using System.Windows;
-
-namespace NatTypeTester.ViewModels
-{
-    public class MainWindowViewModel : ReactiveObject
-    {
-        #region RFC3489
-
-        private string _classicNatType;
-        public string ClassicNatType
-        {
-            get => _classicNatType;
-            set => this.RaiseAndSetIfChanged(ref _classicNatType, value);
-        }
-
-        private string _localEnd = NetUtils.DefaultLocalEnd;
-        public string LocalEnd
-        {
-            get => _localEnd;
-            set => this.RaiseAndSetIfChanged(ref _localEnd, value);
-        }
-
-        private string _publicEnd;
-        public string PublicEnd
-        {
-            get => _publicEnd;
-            set => this.RaiseAndSetIfChanged(ref _publicEnd, value);
-        }
-
-        public ReactiveCommand<Unit, Unit> TestClassicNatType { get; }
-
-        #endregion
-
-        #region RFC5780
-
-        private string _bindingTest;
-        public string BindingTest
-        {
-            get => _bindingTest;
-            set => this.RaiseAndSetIfChanged(ref _bindingTest, value);
-        }
-
-        private string _mappingBehavior;
-        public string MappingBehavior
-        {
-            get => _mappingBehavior;
-            set => this.RaiseAndSetIfChanged(ref _mappingBehavior, value);
-        }
-
-        private string _filteringBehavior;
-        public string FilteringBehavior
-        {
-            get => _filteringBehavior;
-            set => this.RaiseAndSetIfChanged(ref _filteringBehavior, value);
-        }
-
-        private string _localAddress = NetUtils.DefaultLocalEnd;
-        public string LocalAddress
-        {
-            get => _localAddress;
-            set => this.RaiseAndSetIfChanged(ref _localAddress, value);
-        }
-
-        private string _mappingAddress;
-        public string MappingAddress
-        {
-            get => _mappingAddress;
-            set => this.RaiseAndSetIfChanged(ref _mappingAddress, value);
-        }
-
-        public ReactiveCommand<Unit, Unit> DiscoveryNatType { get; }
-
-        #endregion
-
-        #region Servers
-
-        private string _stunServer;
-        public string StunServer
-        {
-            get => _stunServer;
-            set => this.RaiseAndSetIfChanged(ref _stunServer, value);
-        }
-
-        private readonly IEnumerable<string> _defaultServers = new HashSet<string>
-        {
-                @"stun.syncthing.net",
-                @"stun.qq.com",
-                @"stun.miwifi.com",
-                @"stun.bige0.com",
-                @"stun.stunprotocol.org"
-        };
-
-        private SourceList<string> List { get; } = new SourceList<string>();
-        public readonly IObservableCollection<string> StunServers = new ObservableCollectionExtended<string>();
-
-        #endregion
-
-        #region Proxy
-
-        private ProxyType _proxyType = ProxyType.Socks5;
-        public ProxyType ProxyType
-        {
-            get => _proxyType;
-            set => this.RaiseAndSetIfChanged(ref _proxyType, value);
-        }
-
-        private string _proxyServer = @"127.0.0.1:1080";
-        public string ProxyServer
-        {
-            get => _proxyServer;
-            set => this.RaiseAndSetIfChanged(ref _proxyServer, value);
-        }
-
-        private string _proxyUser;
-        public string ProxyUser
-        {
-            get => _proxyUser;
-            set => this.RaiseAndSetIfChanged(ref _proxyUser, value);
-        }
-
-        private string _proxyPassword;
-        public string ProxyPassword
-        {
-            get => _proxyPassword;
-            set => this.RaiseAndSetIfChanged(ref _proxyPassword, value);
-        }
-
-        #endregion
-
-        public MainWindowViewModel()
-        {
-            LoadStunServer();
-            List.Connect()
-                .DistinctValues(x => x)
-                .ObserveOnDispatcher()
-                .Bind(StunServers)
-                .Subscribe();
-            TestClassicNatType = ReactiveCommand.CreateFromObservable(TestClassicNatTypeImpl);
-            DiscoveryNatType = ReactiveCommand.CreateFromObservable(DiscoveryNatTypeImpl);
-        }
-
-        private async void LoadStunServer()
-        {
-            foreach (var server in _defaultServers)
-            {
-                List.Add(server);
-            }
-            StunServer = _defaultServers.First();
-
-            const string path = @"stun.txt";
-
-            if (!File.Exists(path))
-            {
-                return;
-            }
-
-            using var sw = new StreamReader(path);
-            string line;
-            var stun = new StunServer();
-            while ((line = await sw.ReadLineAsync()) != null)
-            {
-                if (!string.IsNullOrWhiteSpace(line) && stun.Parse(line))
-                {
-                    List.Add(stun.ToString());
-                }
-            }
-        }
-
-        private IObservable<Unit> TestClassicNatTypeImpl()
-        {
-            return Observable.FromAsync(async () =>
-            {
-                try
-                {
-                    var server = new StunServer();
-                    if (server.Parse(StunServer))
-                    {
-                        using var proxy = ProxyFactory.CreateProxy(
-                            ProxyType,
-                            NetUtils.ParseEndpoint(LocalEnd),
-                            NetUtils.ParseEndpoint(ProxyServer),
-                            ProxyUser, ProxyPassword
-                            );
-
-                        using var client = new StunClient3489(server.Hostname, server.Port, NetUtils.ParseEndpoint(LocalEnd), proxy);
-
-                        client.NatTypeChanged.ObserveOn(RxApp.MainThreadScheduler)
-                                .Subscribe(t => ClassicNatType = $@"{t}");
-                        client.PubChanged.ObserveOn(RxApp.MainThreadScheduler).Subscribe(t => PublicEnd = $@"{t}");
-                        client.LocalChanged.ObserveOn(RxApp.MainThreadScheduler).Subscribe(t => LocalEnd = $@"{t}");
-                        await client.Query3489Async();
-                    }
-                    else
-                    {
-                        throw new Exception(@"Wrong STUN Server!");
-                    }
-                }
-                catch (Exception ex)
-                {
-                    MessageBox.Show(ex.Message, nameof(NatTypeTester), MessageBoxButton.OK, MessageBoxImage.Error);
-                }
-            }).SubscribeOn(RxApp.TaskpoolScheduler);
-        }
-
-        private IObservable<Unit> DiscoveryNatTypeImpl()
-        {
-            return Observable.FromAsync(async () =>
-            {
-                try
-                {
-                    var server = new StunServer();
-                    if (server.Parse(StunServer))
-                    {
-                        using var proxy = ProxyFactory.CreateProxy(
-                            ProxyType,
-                            NetUtils.ParseEndpoint(LocalEnd),
-                            NetUtils.ParseEndpoint(ProxyServer),
-                            ProxyUser, ProxyPassword
-                            );
-
-                        using var client = new StunClient5389UDP(server.Hostname, server.Port, NetUtils.ParseEndpoint(LocalAddress), proxy);
-
-                        client.BindingTestResultChanged
-                                .ObserveOn(RxApp.MainThreadScheduler)
-                                .Subscribe(t => BindingTest = $@"{t}");
-
-                        client.MappingBehaviorChanged
-                                .ObserveOn(RxApp.MainThreadScheduler)
-                                .Subscribe(t => MappingBehavior = $@"{t}");
-
-                        client.FilteringBehaviorChanged
-                                .ObserveOn(RxApp.MainThreadScheduler)
-                                .Subscribe(t => FilteringBehavior = $@"{t}");
-
-                        client.PubChanged
-                                .ObserveOn(RxApp.MainThreadScheduler)
-                                .Subscribe(t => MappingAddress = $@"{t}");
-
-                        client.LocalChanged
-                                .ObserveOn(RxApp.MainThreadScheduler)
-                                .Subscribe(t => LocalAddress = $@"{t}");
-
-                        await client.QueryAsync();
-                    }
-                    else
-                    {
-                        throw new Exception(@"Wrong STUN Server!");
-                    }
-                }
-                catch (Exception ex)
-                {
-                    MessageBox.Show(ex.Message, nameof(NatTypeTester), MessageBoxButton.OK, MessageBoxImage.Error);
-                }
-            }).SubscribeOn(RxApp.TaskpoolScheduler);
-        }
-    }
-}

+ 37 - 0
NatTypeTester/Views/RFC3489View.xaml

@@ -0,0 +1,37 @@
+<reactiveUi:ReactiveUserControl
+    x:TypeArguments="viewModels:RFC3489ViewModel"
+    x:Class="NatTypeTester.Views.RFC3489View"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:reactiveUi="http://reactiveui.net"
+    xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels;assembly=NatTypeTester.ViewModels"
+    xmlns:ui="http://schemas.modernwpf.com/2019"
+    mc:Ignorable="d"
+    d:DesignHeight="450" d:DesignWidth="800" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition />
+        </Grid.RowDefinitions>
+
+        <TextBox x:Name="NatTypeTextBox" Grid.Row="0"
+                 Margin="10,5" IsReadOnly="True"
+                 VerticalContentAlignment="Center" VerticalAlignment="Center"
+                 ui:ControlHelper.Header="NAT type" />
+        <TextBox x:Name="LocalEndTextBox" Grid.Row="1"
+                 Margin="10,5"
+                 VerticalContentAlignment="Center" VerticalAlignment="Center"
+                 ui:ControlHelper.Header="Local end" />
+        <TextBox x:Name="PublicEndTextBox" Grid.Row="2"
+                 Margin="10,5" IsReadOnly="True"
+                 VerticalContentAlignment="Center" VerticalAlignment="Center"
+                 ui:ControlHelper.Header="Public end" />
+
+        <Button x:Name="TestButton" Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test"
+                Margin="0,0,10,10" />
+    </Grid>
+</reactiveUi:ReactiveUserControl>

+ 56 - 0
NatTypeTester/Views/RFC3489View.xaml.cs

@@ -0,0 +1,56 @@
+using NatTypeTester.Utils;
+using NatTypeTester.ViewModels;
+using ReactiveUI;
+using STUN.Utils;
+using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace NatTypeTester.Views
+{
+	public partial class RFC3489View
+	{
+		public RFC3489View(RFC3489ViewModel viewModel)
+		{
+			InitializeComponent();
+			ViewModel = viewModel;
+
+			this.WhenActivated(d =>
+			{
+				this.OneWayBind(ViewModel,
+								vm => vm.Result3489.NatType,
+								v => v.NatTypeTextBox.Text,
+								type => type.ToString()
+						)
+						.DisposeWith(d);
+
+				this.Bind(ViewModel,
+								vm => vm.Result3489.LocalEndPoint,
+								v => v.LocalEndTextBox.Text,
+								ipe => ipe is null ? string.Empty : ipe.ToString(),
+								NetUtils.ParseEndpoint
+						)
+						.DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+								vm => vm.Result3489.PublicEndPoint,
+								v => v.PublicEndTextBox.Text,
+								ipe => ipe is null ? string.Empty : ipe.ToString()
+						)
+						.DisposeWith(d);
+
+				this.BindCommand(ViewModel, vm => vm.TestClassicNatType, v => v.TestButton).DisposeWith(d);
+
+				this.Events()
+						.KeyDown
+						.Where(x => x.Key == Key.Enter && TestButton.Command.CanExecute(default))
+						.Subscribe(_ => TestButton.Command.Execute(default))
+						.DisposeWith(d);
+
+				ViewModel.TestClassicNatType.ThrownExceptions.Subscribe(async ex => await ex.HandleExceptionWithContentDialogAsync()).DisposeWith(d);
+			});
+		}
+	}
+}

+ 51 - 0
NatTypeTester/Views/RFC5780View.xaml

@@ -0,0 +1,51 @@
+<reactiveUi:ReactiveUserControl
+    x:TypeArguments="viewModels:RFC5780ViewModel"
+    x:Class="NatTypeTester.Views.RFC5780View"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:reactiveUi="http://reactiveui.net"
+    xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels;assembly=NatTypeTester.ViewModels"
+    xmlns:ui="http://schemas.modernwpf.com/2019"
+    mc:Ignorable="d"
+    d:DesignHeight="450" d:DesignWidth="800" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+            <RowDefinition />
+        </Grid.RowDefinitions>
+        <TextBox
+            x:Name="BindingTestTextBox" Grid.Row="0"
+            Margin="10,5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Binding test" />
+        <TextBox
+            x:Name="MappingBehaviorTextBox" Grid.Row="1"
+            Margin="10,5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Mapping behavior" />
+        <TextBox
+            x:Name="FilteringBehaviorTextBox" Grid.Row="2"
+            Margin="10,5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Filtering behavior" />
+        <TextBox
+            x:Name="LocalAddressTextBox" Grid.Row="3"
+            Margin="10,5" IsReadOnly="False"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Local end" />
+        <TextBox
+            x:Name="MappingAddressTextBox" Grid.Row="4"
+            Margin="10,5" IsReadOnly="True"
+            VerticalContentAlignment="Center" VerticalAlignment="Center"
+            ui:ControlHelper.Header="Public end" />
+
+        <Button x:Name="DiscoveryButton" Grid.Row="5" HorizontalAlignment="Right" VerticalAlignment="Bottom"
+                Content="Test" Margin="0,10,10,10" />
+    </Grid>
+</reactiveUi:ReactiveUserControl>

+ 70 - 0
NatTypeTester/Views/RFC5780View.xaml.cs

@@ -0,0 +1,70 @@
+using NatTypeTester.Utils;
+using NatTypeTester.ViewModels;
+using ReactiveUI;
+using STUN.Utils;
+using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace NatTypeTester.Views
+{
+	public partial class RFC5780View
+	{
+		public RFC5780View(RFC5780ViewModel viewModel)
+		{
+			InitializeComponent();
+			ViewModel = viewModel;
+
+			this.WhenActivated(d =>
+			{
+				this.OneWayBind(ViewModel,
+								vm => vm.Result5389.BindingTestResult,
+								v => v.BindingTestTextBox.Text,
+								res => res.ToString()
+						)
+						.DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+								vm => vm.Result5389.MappingBehavior,
+								v => v.MappingBehaviorTextBox.Text,
+								res => res.ToString()
+						)
+						.DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+								vm => vm.Result5389.FilteringBehavior,
+								v => v.FilteringBehaviorTextBox.Text,
+								res => res.ToString()
+						)
+						.DisposeWith(d);
+
+				this.Bind(ViewModel,
+								vm => vm.Result5389.LocalEndPoint,
+								v => v.LocalAddressTextBox.Text,
+								ipe => ipe is null ? string.Empty : ipe.ToString(),
+								NetUtils.ParseEndpoint
+						)
+						.DisposeWith(d);
+
+				this.OneWayBind(ViewModel,
+								vm => vm.Result5389.PublicEndPoint,
+								v => v.MappingAddressTextBox.Text,
+								ipe => ipe is null ? string.Empty : ipe.ToString()
+						)
+						.DisposeWith(d);
+
+				this.BindCommand(ViewModel, vm => vm.DiscoveryNatType, v => v.DiscoveryButton).DisposeWith(d);
+
+				this.Events()
+						.KeyDown
+						.Where(x => x.Key == Key.Enter && DiscoveryButton.Command.CanExecute(default))
+						.Subscribe(_ => DiscoveryButton.Command.Execute(default))
+						.DisposeWith(d);
+
+				ViewModel.DiscoveryNatType.ThrownExceptions.Subscribe(async ex => await ex.HandleExceptionWithContentDialogAsync()).DisposeWith(d);
+			});
+		}
+	}
+}

+ 47 - 0
NatTypeTester/Views/SettingView.xaml

@@ -0,0 +1,47 @@
+<reactiveUi:ReactiveUserControl
+    x:TypeArguments="viewModels:SettingViewModel"
+    x:Class="NatTypeTester.Views.SettingView"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+    xmlns:reactiveUi="http://reactiveui.net"
+    xmlns:viewModels="clr-namespace:NatTypeTester.ViewModels;assembly=NatTypeTester.ViewModels"
+    xmlns:ui="http://schemas.modernwpf.com/2019"
+    mc:Ignorable="d"
+    d:DesignHeight="450" d:DesignWidth="800" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}">
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <Grid Margin="10,0" Grid.Column="0">
+            <ui:RadioButtons Header="Proxy" x:Name="ProxyRadioButtons">
+                <RadioButton Content="Don't use Proxy" />
+                <RadioButton Content="SOCKS5" />
+            </ui:RadioButtons>
+        </Grid>
+        <Grid x:Name="ProxyConfigGrid" Margin="10,5" Grid.Row="1">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="Auto" />
+            </Grid.RowDefinitions>
+            <TextBox
+                x:Name="ProxyServerTextBox" Grid.Row="0"
+                Margin="0,5" IsReadOnly="False"
+                VerticalContentAlignment="Center" VerticalAlignment="Center"
+                ui:ControlHelper.Header="Server" />
+            <TextBox
+                x:Name="ProxyUsernameTextBox" Grid.Row="1"
+                Margin="0,5" IsReadOnly="False"
+                VerticalContentAlignment="Center" VerticalAlignment="Center"
+                ui:ControlHelper.Header="Username" />
+            <TextBox
+                x:Name="ProxyPasswordTextBox" Grid.Row="2"
+                Margin="0,5"
+                VerticalContentAlignment="Center" VerticalAlignment="Center"
+                ui:ControlHelper.Header="Password" />
+        </Grid>
+    </Grid>
+</reactiveUi:ReactiveUserControl>

+ 36 - 0
NatTypeTester/Views/SettingView.xaml.cs

@@ -0,0 +1,36 @@
+using NatTypeTester.ViewModels;
+using ReactiveUI;
+using STUN.Enums;
+using System.Reactive.Disposables;
+
+namespace NatTypeTester.Views
+{
+	public partial class SettingView
+	{
+		public SettingView(SettingViewModel viewModel)
+		{
+			InitializeComponent();
+			ViewModel = viewModel;
+
+			this.WhenActivated(d =>
+			{
+				this.Bind(ViewModel, vm => vm.Config.ProxyServer, v => v.ProxyServerTextBox.Text).DisposeWith(d);
+
+				this.Bind(ViewModel, vm => vm.Config.ProxyUser, v => v.ProxyUsernameTextBox.Text).DisposeWith(d);
+
+				this.Bind(ViewModel, vm => vm.Config.ProxyPassword, v => v.ProxyPasswordTextBox.Text).DisposeWith(d);
+
+				this.Bind(ViewModel,
+					vm => vm.Config.ProxyType,
+					v => v.ProxyRadioButtons.SelectedIndex,
+					type => (int)type,
+					index =>
+					{
+						var type = (ProxyType)index;
+						ProxyConfigGrid.IsEnabled = type is not ProxyType.Plain;
+						return type;
+					}).DisposeWith(d);
+			});
+		}
+	}
+}

+ 0 - 55
STUN/Client/DefaultDnsQuery.cs

@@ -1,55 +0,0 @@
-using STUN.Interfaces;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace STUN.Client
-{
-    public class DefaultDnsQuery : IDnsQuery
-    {
-        public async Task<IPAddress> QueryAsync(string host)
-        {
-            try
-            {
-                var ip = IsIPAddress(host);
-                if (ip != null)
-                {
-                    return ip;
-                }
-                var res = await Dns.GetHostAddressesAsync(host);
-                return res.FirstOrDefault();
-            }
-            catch
-            {
-                return null;
-            }
-        }
-
-        public IPAddress Query(string host)
-        {
-            try
-            {
-                var ip = IsIPAddress(host);
-                if (ip != null)
-                {
-                    return ip;
-                }
-                var res = Dns.GetHostAddresses(host);
-                return res.FirstOrDefault();
-            }
-            catch
-            {
-                return null;
-            }
-        }
-
-        private static IPAddress IsIPAddress(string host)
-        {
-            if (host != null && IPAddress.TryParse(host, out var ip))
-            {
-                return ip;
-            }
-            return null;
-        }
-    }
-}

+ 213 - 227
STUN/Client/StunClient3489.cs

@@ -1,5 +1,6 @@
-using STUN.Enums;
-using STUN.Interfaces;
+using ReactiveUI.Fody.Helpers;
+using STUN.DnsClients;
+using STUN.Enums;
 using STUN.Message;
 using STUN.Proxy;
 using STUN.StunResult;
@@ -8,234 +9,219 @@ using System;
 using System.Diagnostics;
 using System.Linq;
 using System.Net;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace STUN.Client
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc3489#section-10.1
-    /// https://upload.wikimedia.org/wikipedia/commons/6/63/STUN_Algorithm3.svg
-    /// </summary>
-    public class StunClient3489 : IDisposable
-    {
-        #region Subject
-
-        private readonly Subject<NatType> _natTypeSubj = new Subject<NatType>();
-        public IObservable<NatType> NatTypeChanged => _natTypeSubj.AsObservable();
-
-        protected readonly Subject<IPEndPoint> PubSubj = new Subject<IPEndPoint>();
-        public IObservable<IPEndPoint> PubChanged => PubSubj.AsObservable();
-
-        protected readonly Subject<IPEndPoint> LocalSubj = new Subject<IPEndPoint>();
-        public IObservable<IPEndPoint> LocalChanged => LocalSubj.AsObservable();
-
-        #endregion
-
-        public IPEndPoint LocalEndPoint => Proxy.LocalEndPoint;
-
-        public TimeSpan Timeout
-        {
-            get => Proxy.Timeout;
-            set => Proxy.Timeout = value;
-        }
-
-        protected readonly IPAddress Server;
-        protected readonly ushort Port;
-
-        public IPEndPoint RemoteEndPoint => Server == null ? null : new IPEndPoint(Server, Port);
-
-        protected readonly IUdpProxy Proxy;
-
-        public StunClient3489(string server, ushort port = 3478, IPEndPoint local = null, IUdpProxy proxy = null, IDnsQuery dnsQuery = null)
-        {
-            Proxy = proxy ?? new NoneUdpProxy(local);
-
-            if (string.IsNullOrEmpty(server))
-            {
-                throw new ArgumentException(@"Please specify STUN server !");
-            }
-
-            if (port < 1)
-            {
-                throw new ArgumentException(@"Port value must be >= 1 !");
-            }
-
-            dnsQuery ??= new DefaultDnsQuery();
-
-            Server = dnsQuery.Query(server);
-            if (Server == null)
-            {
-                throw new ArgumentException(@"Wrong STUN server !");
-            }
-            Port = port;
-
-            Timeout = TimeSpan.FromSeconds(1.6);
-        }
-
-        public async Task<ClassicStunResult> Query3489Async()
-        {
-            var res = new ClassicStunResult();
-            _natTypeSubj.OnNext(res.NatType);
-            PubSubj.OnNext(res.PublicEndPoint);
-
-            using var cts = new CancellationTokenSource(Timeout);
-            try
-            {
-                await Proxy.ConnectAsync(cts.Token);
-                // test I
-                var test1 = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest, MagicCookie = 0 };
-
-                var (response1, remote1, local1) = await TestAsync(test1, RemoteEndPoint, RemoteEndPoint, cts.Token);
-                if (response1 == null)
-                {
-                    res.NatType = NatType.UdpBlocked;
-                    return res;
-                }
-
-                if (local1 != null)
-                {
-                    LocalSubj.OnNext(LocalEndPoint);
-                }
-
-                var mappedAddress1 = AttributeExtensions.GetMappedAddressAttribute(response1);
-                var changedAddress1 = AttributeExtensions.GetChangedAddressAttribute(response1);
-
-                // 某些单 IP 服务器的迷惑操作
-                if (mappedAddress1 == null
-                || changedAddress1 == null
-                || Equals(changedAddress1.Address, remote1.Address)
-                || changedAddress1.Port == remote1.Port)
-                {
-                    res.NatType = NatType.UnsupportedServer;
-                    return res;
-                }
-
-                PubSubj.OnNext(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 = AttributeExtensions.GetMappedAddressAttribute(response2);
-
-                if (Equals(mappedAddress1.Address, local1) && mappedAddress1.Port == LocalEndPoint.Port)
-                {
-                    // No NAT
-                    if (response2 == null)
-                    {
-                        res.NatType = NatType.SymmetricUdpFirewall;
-                        res.PublicEndPoint = mappedAddress1;
-                        return res;
-                    }
-                    res.NatType = NatType.OpenInternet;
-                    res.PublicEndPoint = mappedAddress2;
-                    return res;
-                }
-
-                // NAT
-                if (response2 != null)
-                {
-                    // 有些单 IP 服务器并不能测 NAT 类型,比如 Google 的
-                    var type = Equals(remote1.Address, remote2.Address) || remote1.Port == remote2.Port ? NatType.UnsupportedServer : NatType.FullCone;
-                    res.NatType = type;
-                    res.PublicEndPoint = mappedAddress2;
-                    return res;
-                }
-
-                // Test I(#2)
-                var test12 = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest, MagicCookie = 0 };
-                var (response12, _, _) = await TestAsync(test12, changedAddress1, changedAddress1, cts.Token);
-                var mappedAddress12 = AttributeExtensions.GetMappedAddressAttribute(response12);
-
-                if (mappedAddress12 == null)
-                {
-                    res.NatType = NatType.Unknown;
-                    return res;
-                }
-
-                if (!Equals(mappedAddress12, mappedAddress1))
-                {
-                    res.NatType = NatType.Symmetric;
-                    res.PublicEndPoint = mappedAddress12;
-                    return res;
-                }
-
-                // Test III
-                var test3 = new StunMessage5389
-                {
-                    StunMessageType = StunMessageType.BindingRequest,
-                    MagicCookie = 0,
-                    Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
-                };
-                var (response3, _, _) = await TestAsync(test3, changedAddress1, changedAddress1, cts.Token);
-                var mappedAddress3 = AttributeExtensions.GetMappedAddressAttribute(response3);
-                if (mappedAddress3 != null)
-                {
-                    res.NatType = NatType.RestrictedCone;
-                    res.PublicEndPoint = mappedAddress3;
-                    return res;
-                }
-                res.NatType = NatType.PortRestrictedCone;
-                res.PublicEndPoint = mappedAddress12;
-                return res;
-            }
-            finally
-            {
-                await Proxy.DisconnectAsync();
-                _natTypeSubj.OnNext(res.NatType);
-                PubSubj.OnNext(res.PublicEndPoint);
-            }
-        }
-
-        protected async Task<(StunMessage5389, IPEndPoint, IPAddress)> TestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken token)
-        {
-            try
-            {
-                var b1 = sendMessage.Bytes.ToArray();
-                //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(b1, remote, receive, token);
-
-                        var message = new StunMessage5389();
-                        if (message.TryParse(receive1) &&
-                            message.ClassicTransactionId.IsEqual(sendMessage.ClassicTransactionId))
-                        {
-                            return (message, ipe, local);
-                        }
-                    }
-                    catch (Exception ex)
-                    {
-                        Debug.WriteLine(ex);
-                    }
-                }
-            }
-            catch (Exception ex)
-            {
-                Debug.WriteLine(ex);
-            }
-            return (null, null, null);
-        }
-
-        public virtual void Dispose()
-        {
-            Proxy?.Dispose();
-            _natTypeSubj.OnCompleted();
-            PubSubj.OnCompleted();
-            LocalSubj.OnCompleted();
-        }
-    }
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc3489#section-10.1
+	/// https://upload.wikimedia.org/wikipedia/commons/6/63/STUN_Algorithm3.svg
+	/// </summary>
+	public class StunClient3489 : IDisposable
+	{
+		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;
+
+		[Reactive]
+		public ClassicStunResult Status { get; } = new();
+
+		public StunClient3489(string server, ushort port = 3478, IPEndPoint? local = null, IUdpProxy? proxy = null, IDnsQuery? dnsQuery = null)
+		{
+			Proxy = proxy ?? new NoneUdpProxy(local);
+
+			if (string.IsNullOrEmpty(server))
+			{
+				throw new ArgumentException(@"Please specify STUN server !");
+			}
+
+			if (port < 1)
+			{
+				throw new ArgumentException(@"Port value must be >= 1 !");
+			}
+
+			dnsQuery ??= new DefaultDnsQuery();
+
+			var ip = dnsQuery.Query(server);
+
+			Server = ip ?? throw new ArgumentException(@"Wrong STUN server !");
+			Port = port;
+
+			Timeout = TimeSpan.FromSeconds(1.6);
+			Status.LocalEndPoint = local;
+		}
+
+		private void Init()
+		{
+			Status.PublicEndPoint = default;
+			Status.LocalEndPoint = default;
+			Status.NatType = NatType.Unknown;
+		}
+
+		public async Task Query3489Async()
+		{
+			try
+			{
+				Init();
+				using var cts = new CancellationTokenSource(Timeout);
+				await Proxy.ConnectAsync(cts.Token);
+				// test I
+				var test1 = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest, MagicCookie = 0 };
+
+				var (response1, remote1, local1) = await TestAsync(test1, RemoteEndPoint, RemoteEndPoint, cts.Token);
+				if (response1 is null || remote1 is null)
+				{
+					Status.NatType = NatType.UdpBlocked;
+					return;
+				}
+
+				Status.LocalEndPoint = local1 is null ? null : new IPEndPoint(local1, LocalEndPoint.Port);
+
+				var mappedAddress1 = response1.GetMappedAddressAttribute();
+				var changedAddress1 = response1.GetChangedAddressAttribute();
+
+				// 某些单 IP 服务器的迷惑操作
+				if (mappedAddress1 is null
+				|| changedAddress1 is null
+				|| Equals(changedAddress1.Address, remote1.Address)
+				|| changedAddress1.Port == remote1.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();
+
+				if (Equals(mappedAddress1.Address, local1) && mappedAddress1.Port == LocalEndPoint.Port)
+				{
+					// No NAT
+					if (response2 is null)
+					{
+						Status.NatType = NatType.SymmetricUdpFirewall;
+						Status.PublicEndPoint = mappedAddress1;
+					}
+					else
+					{
+						Status.NatType = NatType.OpenInternet;
+						Status.PublicEndPoint = mappedAddress2;
+					}
+					return;
+				}
+
+				// NAT
+				if (response2 is not null && remote2 is not null)
+				{
+					// 有些单 IP 服务器并不能测 NAT 类型,比如 Google 的
+					var type = Equals(remote1.Address, remote2.Address) || remote1.Port == remote2.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();
+
+				if (mappedAddress12 is null)
+				{
+					Status.NatType = NatType.Unknown;
+					return;
+				}
+
+				if (!Equals(mappedAddress12, mappedAddress1))
+				{
+					Status.NatType = NatType.Symmetric;
+					Status.PublicEndPoint = mappedAddress12;
+					return;
+				}
+
+				// Test III
+				var test3 = new StunMessage5389
+				{
+					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;
+				}
+
+				Status.NatType = NatType.PortRestrictedCone;
+				Status.PublicEndPoint = mappedAddress12;
+			}
+			finally
+			{
+				await Proxy.DisconnectAsync();
+			}
+		}
+
+		protected async Task<(StunMessage5389?, IPEndPoint?, IPAddress?)> TestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken token)
+		{
+			try
+			{
+				var b1 = sendMessage.Bytes.ToArray();
+				//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(b1, remote, receive, token);
+
+						var message = new StunMessage5389();
+						if (message.TryParse(receive1) &&
+							message.ClassicTransactionId.IsEqual(sendMessage.ClassicTransactionId))
+						{
+							return (message, ipe, local);
+						}
+					}
+					catch (Exception ex)
+					{
+						Debug.WriteLine(ex);
+					}
+				}
+			}
+			catch (Exception ex)
+			{
+				Debug.WriteLine(ex);
+			}
+			return (null, null, null);
+		}
+
+		public virtual void Dispose()
+		{
+			Proxy.Dispose();
+		}
+	}
 }

+ 252 - 290
STUN/Client/StunClient5389UDP.cs

@@ -1,301 +1,263 @@
-using STUN.Enums;
-using STUN.Interfaces;
+using ReactiveUI.Fody.Helpers;
+using STUN.DnsClients;
+using STUN.Enums;
 using STUN.Message;
+using STUN.Proxy;
 using STUN.StunResult;
 using STUN.Utils;
 using System;
 using System.Net;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace STUN.Client
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-7.2.1
-    /// https://tools.ietf.org/html/rfc5780#section-4.2
-    /// </summary>
-    public class StunClient5389UDP : StunClient3489
-    {
-        #region Subject
-
-        private readonly Subject<BindingTestResult> _bindingSubj = new Subject<BindingTestResult>();
-        public IObservable<BindingTestResult> BindingTestResultChanged => _bindingSubj.AsObservable();
-
-        private readonly Subject<MappingBehavior> _mappingBehaviorSubj = new Subject<MappingBehavior>();
-        public IObservable<MappingBehavior> MappingBehaviorChanged => _mappingBehaviorSubj.AsObservable();
-
-        private readonly Subject<FilteringBehavior> _filteringBehaviorSubj = new Subject<FilteringBehavior>();
-        public IObservable<FilteringBehavior> FilteringBehaviorChanged => _filteringBehaviorSubj.AsObservable();
-
-        #endregion
-
-        public StunClient5389UDP(string server, ushort port = 3478, IPEndPoint local = null, IUdpProxy proxy = null, IDnsQuery dnsQuery = null)
-        : base(server, port, local, proxy, dnsQuery)
-        {
-            Timeout = TimeSpan.FromSeconds(3);
-        }
-
-        public async Task<StunResult5389> QueryAsync()
-        {
-            var result = new StunResult5389();
-            try
-            {
-                _bindingSubj.OnNext(result.BindingTestResult);
-                _mappingBehaviorSubj.OnNext(result.MappingBehavior);
-                _filteringBehaviorSubj.OnNext(result.FilteringBehavior);
-                PubSubj.OnNext(result.PublicEndPoint);
-
-                using var cts = new CancellationTokenSource(Timeout);
-
-                await Proxy.ConnectAsync(cts.Token);
-
-                result = await FilteringBehaviorTestBaseAsync(cts.Token);
-                if (result.BindingTestResult != BindingTestResult.Success
-                || result.FilteringBehavior == FilteringBehavior.UnsupportedServer
-                )
-                {
-                    return result;
-                }
-
-                if (Equals(result.PublicEndPoint, result.LocalEndPoint))
-                {
-                    result.MappingBehavior = MappingBehavior.Direct;
-                    return result;
-                }
-
-                // MappingBehaviorTest test II
-                var result2 = await BindingTestBaseAsync(new IPEndPoint(result.OtherEndPoint.Address, RemoteEndPoint.Port), false, cts.Token);
-                if (result2.BindingTestResult != BindingTestResult.Success)
-                {
-                    result.MappingBehavior = MappingBehavior.Fail;
-                    return result;
-                }
-
-                if (Equals(result2.PublicEndPoint, result.PublicEndPoint))
-                {
-                    result.MappingBehavior = MappingBehavior.EndpointIndependent;
-                    return result;
-                }
-
-                // MappingBehaviorTest test III
-                var result3 = await BindingTestBaseAsync(result.OtherEndPoint, false, cts.Token);
-                if (result3.BindingTestResult != BindingTestResult.Success)
-                {
-                    result.MappingBehavior = MappingBehavior.Fail;
-                    return result;
-                }
-
-                result.MappingBehavior = Equals(result3.PublicEndPoint, result2.PublicEndPoint) ? MappingBehavior.AddressDependent : MappingBehavior.AddressAndPortDependent;
-
-                return result;
-            }
-            finally
-            {
-                _mappingBehaviorSubj.OnNext(result.MappingBehavior);
-                await Proxy.DisconnectAsync();
-            }
-        }
-
-        public async Task<StunResult5389> BindingTestAsync()
-        {
-            try
-            {
-                using var cts = new CancellationTokenSource(Timeout);
-                await Proxy.ConnectAsync(cts.Token);
-                var result = await BindingTestBaseAsync(RemoteEndPoint, true, cts.Token);
-                return result;
-            }
-            finally
-            {
-                await Proxy.DisconnectAsync();
-            }
-        }
-
-        private async Task<StunResult5389> BindingTestBaseAsync(IPEndPoint remote, bool notifyChanged, CancellationToken token)
-        {
-            BindingTestResult res;
-            var test = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest };
-            var (response1, _, local1) = await TestAsync(test, remote, remote, token);
-            var mappedAddress1 = AttributeExtensions.GetXorMappedAddressAttribute(response1);
-            var otherAddress = AttributeExtensions.GetOtherAddressAttribute(response1);
-            var local = local1 == null ? null : new IPEndPoint(local1, LocalEndPoint.Port);
-
-            if (response1 == null)
-            {
-                res = BindingTestResult.Fail;
-            }
-            else if (mappedAddress1 == null)
-            {
-                res = BindingTestResult.UnsupportedServer;
-            }
-            else
-            {
-                res = BindingTestResult.Success;
-            }
-
-            if (notifyChanged)
-            {
-                _bindingSubj.OnNext(res);
-                PubSubj.OnNext(mappedAddress1);
-            }
-            LocalSubj.OnNext(LocalEndPoint);
-
-            return new StunResult5389
-            {
-                BindingTestResult = res,
-                LocalEndPoint = local,
-                PublicEndPoint = mappedAddress1,
-                OtherEndPoint = otherAddress
-            };
-        }
-
-        public async Task<StunResult5389> MappingBehaviorTestAsync()
-        {
-            var result = new StunResult5389();
-            using var cts = new CancellationTokenSource(Timeout);
-            try
-            {
-                await Proxy.ConnectAsync(cts.Token);
-                // test I
-                result = await BindingTestBaseAsync(RemoteEndPoint, true, cts.Token);
-                if (result.BindingTestResult != BindingTestResult.Success)
-                {
-                    return result;
-                }
-
-                if (result.OtherEndPoint == null
-                    || Equals(result.OtherEndPoint.Address, RemoteEndPoint.Address)
-                    || result.OtherEndPoint.Port == RemoteEndPoint.Port)
-                {
-                    result.MappingBehavior = MappingBehavior.UnsupportedServer;
-                    return result;
-                }
-
-                if (Equals(result.PublicEndPoint, result.LocalEndPoint))
-                {
-                    result.MappingBehavior = MappingBehavior.Direct;
-                    return result;
-                }
-
-                // test II
-                var result2 = await BindingTestBaseAsync(new IPEndPoint(result.OtherEndPoint.Address, RemoteEndPoint.Port), false, cts.Token);
-                if (result2.BindingTestResult != BindingTestResult.Success)
-                {
-                    result.MappingBehavior = MappingBehavior.Fail;
-                    return result;
-                }
-
-                if (Equals(result2.PublicEndPoint, result.PublicEndPoint))
-                {
-                    result.MappingBehavior = MappingBehavior.EndpointIndependent;
-                    return result;
-                }
-
-                // test III
-                var result3 = await BindingTestBaseAsync(result.OtherEndPoint, false, cts.Token);
-                if (result3.BindingTestResult != BindingTestResult.Success)
-                {
-                    result.MappingBehavior = MappingBehavior.Fail;
-                    return result;
-                }
-
-                result.MappingBehavior = Equals(result3.PublicEndPoint, result2.PublicEndPoint) ? MappingBehavior.AddressDependent : MappingBehavior.AddressAndPortDependent;
-
-                return result;
-            }
-            finally
-            {
-                _mappingBehaviorSubj.OnNext(result.MappingBehavior);
-                await Proxy.DisconnectAsync();
-            }
-        }
-
-        private async Task<StunResult5389> FilteringBehaviorTestBaseAsync(CancellationToken token)
-        {
-            // test I
-            var result1 = await BindingTestBaseAsync(RemoteEndPoint, true, token);
-            try
-            {
-                if (result1.BindingTestResult != BindingTestResult.Success)
-                {
-                    return result1;
-                }
-
-                if (result1.OtherEndPoint == null
-                    || Equals(result1.OtherEndPoint.Address, RemoteEndPoint.Address)
-                    || result1.OtherEndPoint.Port == RemoteEndPoint.Port)
-                {
-                    result1.FilteringBehavior = FilteringBehavior.UnsupportedServer;
-                    return result1;
-                }
-
-                // test II
-                var test2 = new StunMessage5389
-                {
-                    StunMessageType = StunMessageType.BindingRequest,
-                    Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
-                };
-                var (response2, _, _) = await TestAsync(test2, RemoteEndPoint, result1.OtherEndPoint, token);
-
-                if (response2 != null)
-                {
-                    result1.FilteringBehavior = FilteringBehavior.EndpointIndependent;
-                    return result1;
-                }
-
-                // test III
-                var test3 = new StunMessage5389
-                {
-                    StunMessageType = StunMessageType.BindingRequest,
-                    Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
-                };
-                var (response3, remote3, _) = await TestAsync(test3, RemoteEndPoint, RemoteEndPoint, token);
-
-                if (response3 == null)
-                {
-                    result1.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
-                    return result1;
-                }
-
-                if (Equals(remote3.Address, RemoteEndPoint.Address) && remote3.Port != RemoteEndPoint.Port)
-                {
-                    result1.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
-                }
-                else
-                {
-                    result1.FilteringBehavior = FilteringBehavior.UnsupportedServer;
-                }
-                return result1;
-            }
-            finally
-            {
-                _filteringBehaviorSubj.OnNext(result1.FilteringBehavior);
-            }
-        }
-
-        public async Task<StunResult5389> FilteringBehaviorTestAsync()
-        {
-            try
-            {
-                using var cts = new CancellationTokenSource(Timeout);
-                await Proxy.ConnectAsync(cts.Token);
-                var result = await FilteringBehaviorTestBaseAsync(cts.Token);
-                return result;
-            }
-            finally
-            {
-                await Proxy.DisconnectAsync();
-            }
-        }
-
-        public override void Dispose()
-        {
-            base.Dispose();
-            _bindingSubj.OnCompleted();
-            _mappingBehaviorSubj.OnCompleted();
-            _filteringBehaviorSubj.OnCompleted();
-        }
-    }
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-7.2.1
+	/// https://tools.ietf.org/html/rfc5780#section-4.2
+	/// </summary>
+	public class StunClient5389UDP : StunClient3489
+	{
+		[Reactive]
+		public new StunResult5389 Status { get; } = new();
+
+		public StunClient5389UDP(string server, ushort port = 3478, IPEndPoint? local = null, IUdpProxy? proxy = null, IDnsQuery? dnsQuery = null)
+		: base(server, port, local, proxy, dnsQuery)
+		{
+			Timeout = TimeSpan.FromSeconds(3);
+			Status.LocalEndPoint = local;
+		}
+
+		public async Task QueryAsync()
+		{
+			try
+			{
+				Status.Reset();
+				using var cts = new CancellationTokenSource(Timeout);
+				await Proxy.ConnectAsync(cts.Token);
+
+				await FilteringBehaviorTestBaseAsync(cts.Token);
+				if (Status.BindingTestResult != BindingTestResult.Success
+					|| Status.FilteringBehavior == FilteringBehavior.UnsupportedServer
+				)
+				{
+					return;
+				}
+
+				if (Equals(Status.PublicEndPoint, Status.LocalEndPoint))
+				{
+					Status.MappingBehavior = MappingBehavior.Direct;
+					return;
+				}
+
+				// MappingBehaviorTest test II
+				var (success2, result2) = await MappingBehaviorTestBase2Async(cts.Token);
+				if (!success2)
+				{
+					return;
+				}
+
+				// MappingBehaviorTest test III
+				await MappingBehaviorTestBase3Async(result2, cts.Token);
+			}
+			finally
+			{
+				await Proxy.DisconnectAsync();
+			}
+		}
+
+		public async Task BindingTestAsync()
+		{
+			try
+			{
+				Status.Reset();
+				using var cts = new CancellationTokenSource(Timeout);
+				await Proxy.ConnectAsync(cts.Token);
+				await BindingTestInternalAsync(cts.Token);
+			}
+			finally
+			{
+				await Proxy.DisconnectAsync();
+			}
+		}
+
+		private async Task BindingTestInternalAsync(CancellationToken token)
+		{
+			Status.Clone(await BindingTestBaseAsync(RemoteEndPoint, token));
+		}
+
+		private async Task<StunResult5389> BindingTestBaseAsync(IPEndPoint remote, CancellationToken token)
+		{
+			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 local = local1 is null ? null : new IPEndPoint(local1, LocalEndPoint.Port);
+
+			if (response1 is null)
+			{
+				result.BindingTestResult = BindingTestResult.Fail;
+			}
+			else if (mappedAddress1 is null)
+			{
+				result.BindingTestResult = BindingTestResult.UnsupportedServer;
+			}
+			else
+			{
+				result.BindingTestResult = BindingTestResult.Success;
+			}
+
+			result.LocalEndPoint = local;
+			result.PublicEndPoint = mappedAddress1;
+			result.OtherEndPoint = otherAddress;
+
+			return result;
+		}
+
+		public async Task MappingBehaviorTestAsync()
+		{
+			try
+			{
+				Status.Reset();
+				using var cts = new CancellationTokenSource(Timeout);
+				await Proxy.ConnectAsync(cts.Token);
+
+				// test I
+				await BindingTestInternalAsync(cts.Token);
+				if (Status.BindingTestResult != BindingTestResult.Success)
+				{
+					return;
+				}
+
+				if (Status.OtherEndPoint is null
+					|| Equals(Status.OtherEndPoint.Address, RemoteEndPoint.Address)
+					|| Status.OtherEndPoint.Port == RemoteEndPoint.Port)
+				{
+					Status.MappingBehavior = MappingBehavior.UnsupportedServer;
+					return;
+				}
+
+				if (Equals(Status.PublicEndPoint, Status.LocalEndPoint))
+				{
+					Status.MappingBehavior = MappingBehavior.Direct;
+					return;
+				}
+
+				// test II
+				var (success2, result2) = await MappingBehaviorTestBase2Async(cts.Token);
+				if (!success2)
+				{
+					return;
+				}
+
+				// test III
+				await MappingBehaviorTestBase3Async(result2, cts.Token);
+			}
+			finally
+			{
+				await Proxy.DisconnectAsync();
+			}
+		}
+
+		private async Task<(bool, StunResult5389)> MappingBehaviorTestBase2Async(CancellationToken token)
+		{
+			var result2 = await BindingTestBaseAsync(new IPEndPoint(Status.OtherEndPoint!.Address, RemoteEndPoint.Port), token);
+			if (result2.BindingTestResult != BindingTestResult.Success)
+			{
+				Status.MappingBehavior = MappingBehavior.Fail;
+				return (false, result2);
+			}
+
+			if (Equals(result2.PublicEndPoint, Status.PublicEndPoint))
+			{
+				Status.MappingBehavior = MappingBehavior.EndpointIndependent;
+				return (false, result2);
+			}
+
+			return (true, result2);
+		}
+
+		private async Task MappingBehaviorTestBase3Async(StunResult5389 result2, CancellationToken token)
+		{
+			var result3 = await BindingTestBaseAsync(Status.OtherEndPoint!, token);
+			if (result3.BindingTestResult != BindingTestResult.Success)
+			{
+				Status.MappingBehavior = MappingBehavior.Fail;
+				return;
+			}
+
+			Status.MappingBehavior = Equals(result3.PublicEndPoint, result2.PublicEndPoint) ? MappingBehavior.AddressDependent : MappingBehavior.AddressAndPortDependent;
+		}
+
+		private async Task FilteringBehaviorTestBaseAsync(CancellationToken token)
+		{
+			// test I
+			await BindingTestInternalAsync(token);
+			if (Status.BindingTestResult != BindingTestResult.Success)
+			{
+				return;
+			}
+
+			if (Status.OtherEndPoint is null
+				|| Equals(Status.OtherEndPoint.Address, RemoteEndPoint.Address)
+				|| Status.OtherEndPoint.Port == RemoteEndPoint.Port)
+			{
+				Status.FilteringBehavior = FilteringBehavior.UnsupportedServer;
+				return;
+			}
+
+			// test II
+			var test2 = new StunMessage5389
+			{
+				StunMessageType = StunMessageType.BindingRequest,
+				Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
+			};
+			var (response2, _, _) = await TestAsync(test2, RemoteEndPoint, Status.OtherEndPoint, token);
+
+			if (response2 is not null)
+			{
+				Status.FilteringBehavior = FilteringBehavior.EndpointIndependent;
+				return;
+			}
+
+			// test III
+			var test3 = new StunMessage5389
+			{
+				StunMessageType = StunMessageType.BindingRequest,
+				Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
+			};
+			var (response3, remote3, _) = await TestAsync(test3, RemoteEndPoint, RemoteEndPoint, token);
+
+			if (response3 is null || remote3 is null)
+			{
+				Status.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
+				return;
+			}
+
+			if (Equals(remote3.Address, RemoteEndPoint.Address) && remote3.Port != RemoteEndPoint.Port)
+			{
+				Status.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
+			}
+			else
+			{
+				Status.FilteringBehavior = FilteringBehavior.UnsupportedServer;
+			}
+		}
+
+		public async Task FilteringBehaviorTestAsync()
+		{
+			try
+			{
+				Status.Reset();
+				using var cts = new CancellationTokenSource(Timeout);
+				await Proxy.ConnectAsync(cts.Token);
+				await FilteringBehaviorTestBaseAsync(cts.Token);
+			}
+			finally
+			{
+				await Proxy.DisconnectAsync();
+			}
+		}
+	}
 }

+ 64 - 0
STUN/DnsClients/DefaultDnsQuery.cs

@@ -0,0 +1,64 @@
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace STUN.DnsClients
+{
+	public class DefaultDnsQuery : IDnsQuery
+	{
+		public async Task<IPAddress?> QueryAsync(string? host)
+		{
+			try
+			{
+				if (host is null)
+				{
+					return null;
+				}
+
+				var ip = IsIPAddress(host);
+				if (ip is not null)
+				{
+					return ip;
+				}
+				var res = await Dns.GetHostAddressesAsync(host);
+				return res.FirstOrDefault();
+			}
+			catch
+			{
+				return null;
+			}
+		}
+
+		public IPAddress? Query(string? host)
+		{
+			try
+			{
+				if (host is null)
+				{
+					return null;
+				}
+
+				var ip = IsIPAddress(host);
+				if (ip is not null)
+				{
+					return ip;
+				}
+				var res = Dns.GetHostAddresses(host);
+				return res.FirstOrDefault();
+			}
+			catch
+			{
+				return null;
+			}
+		}
+
+		private static IPAddress? IsIPAddress(string? host)
+		{
+			if (host is not null && IPAddress.TryParse(host, out var ip))
+			{
+				return ip;
+			}
+			return null;
+		}
+	}
+}

+ 11 - 0
STUN/DnsClients/IDnsQuery.cs

@@ -0,0 +1,11 @@
+using System.Net;
+using System.Threading.Tasks;
+
+namespace STUN.DnsClients
+{
+	public interface IDnsQuery
+	{
+		public Task<IPAddress?> QueryAsync(string? host);
+		public IPAddress? Query(string? host);
+	}
+}

+ 42 - 42
STUN/Enums/AttributeType.cs

@@ -1,43 +1,43 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    /// <summary>
-    /// STUN Attribute Registry
-    /// </summary>
-    /// <remarks>
-    /// https://tools.ietf.org/html/rfc3489#section-11.2
-    /// https://tools.ietf.org/html/rfc5389#section-18.2
-    /// https://tools.ietf.org/html/rfc5780#section-9.1
-    /// https://tools.ietf.org/html/rfc8489#section-18.3
-    /// </remarks>
-    public enum AttributeType : ushort
-    {
-        Useless = 0x0000,
-        MappedAddress = 0x0001,
-        ResponseAddress = 0x0002,
-        ChangeRequest = 0x0003,
-        SourceAddress = 0x0004,
-        ChangedAddress = 0x0005,
-        Username = 0x0006,
-        Password = 0x0007,
-        MessageIntegrity = 0x0008,
-        ErrorCode = 0x0009,
-        UnknownAttribute = 0x000A,
-        ReflectedFrom = 0x000B,
-        Realm = 0x0014,
-        Nonce = 0x0015,
-        MessageIntegritySha256 = 0x001C,
-        PasswordAlgorithm = 0x001D,
-        UserHash = 0x001E,
-        XorMappedAddress = 0x0020,
-        Padding = 0x0026,
-        ResponsePort = 0x0027,
-        PasswordAlgorithms = 0x8002,
-        AlternateDomain = 0x8003,
-        Software = 0x8022,
-        AlternateServer = 0x8023,
-        CacheTimeout = 0x8027,
-        Fingerprint = 0x8028,
-        ResponseOrigin = 0x802B,
-        OtherAddress = 0x802C,
-    }
-}
+	/// <summary>
+	/// STUN Attribute Registry
+	/// </summary>
+	/// <remarks>
+	/// https://tools.ietf.org/html/rfc3489#section-11.2
+	/// https://tools.ietf.org/html/rfc5389#section-18.2
+	/// https://tools.ietf.org/html/rfc5780#section-9.1
+	/// https://tools.ietf.org/html/rfc8489#section-18.3
+	/// </remarks>
+	public enum AttributeType : ushort
+	{
+		Useless = 0x0000,
+		MappedAddress = 0x0001,
+		ResponseAddress = 0x0002,
+		ChangeRequest = 0x0003,
+		SourceAddress = 0x0004,
+		ChangedAddress = 0x0005,
+		Username = 0x0006,
+		Password = 0x0007,
+		MessageIntegrity = 0x0008,
+		ErrorCode = 0x0009,
+		UnknownAttribute = 0x000A,
+		ReflectedFrom = 0x000B,
+		Realm = 0x0014,
+		Nonce = 0x0015,
+		MessageIntegritySha256 = 0x001C,
+		PasswordAlgorithm = 0x001D,
+		UserHash = 0x001E,
+		XorMappedAddress = 0x0020,
+		Padding = 0x0026,
+		ResponsePort = 0x0027,
+		PasswordAlgorithms = 0x8002,
+		AlternateDomain = 0x8003,
+		Software = 0x8022,
+		AlternateServer = 0x8023,
+		CacheTimeout = 0x8027,
+		Fingerprint = 0x8028,
+		ResponseOrigin = 0x802B,
+		OtherAddress = 0x802C,
+	}
+}

+ 8 - 8
STUN/Enums/BindingTestResult.cs

@@ -1,10 +1,10 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    public enum BindingTestResult
-    {
-        Unknown,
-        UnsupportedServer,
-        Success,
-        Fail
-    }
+	public enum BindingTestResult
+	{
+		Unknown,
+		UnsupportedServer,
+		Success,
+		Fail
+	}
 }

+ 9 - 9
STUN/Enums/Class.cs

@@ -1,10 +1,10 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    internal enum Class : ushort
-    {
-        Request = 0b00000_0_000_0_0000,
-        Indication = 0b00000_0_000_1_0000,
-        SuccessResponse = 0b00000_1_000_0_0000,
-        ErrorResponse = 0b00000_1_000_1_0000,
-    }
-}
+	internal enum Class : ushort
+	{
+		Request = 0b00000_0_000_0_0000,
+		Indication = 0b00000_0_000_1_0000,
+		SuccessResponse = 0b00000_1_000_0_0000,
+		ErrorResponse = 0b00000_1_000_1_0000,
+	}
+}

+ 10 - 10
STUN/Enums/FilteringBehavior.cs

@@ -1,12 +1,12 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    public enum FilteringBehavior
-    {
-        Unknown,
-        UnsupportedServer,
-        EndpointIndependent,
-        AddressDependent,
-        AddressAndPortDependent,
-        Fail
-    }
+	public enum FilteringBehavior
+	{
+		Unknown,
+		UnsupportedServer,
+		EndpointIndependent,
+		AddressDependent,
+		AddressAndPortDependent,
+		Fail
+	}
 }

+ 10 - 10
STUN/Enums/IpFamily.cs

@@ -1,11 +1,11 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-15.1
-    /// </summary>
-    public enum IpFamily : byte
-    {
-        IPv4 = 0x01,
-        IPv6 = 0x02
-    }
-}
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-15.1
+	/// </summary>
+	public enum IpFamily : byte
+	{
+		IPv4 = 0x01,
+		IPv6 = 0x02
+	}
+}

+ 11 - 11
STUN/Enums/MappingBehavior.cs

@@ -1,13 +1,13 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    public enum MappingBehavior
-    {
-        Unknown,
-        UnsupportedServer,
-        Direct,
-        EndpointIndependent,
-        AddressDependent,
-        AddressAndPortDependent,
-        Fail
-    }
+	public enum MappingBehavior
+	{
+		Unknown,
+		UnsupportedServer,
+		Direct,
+		EndpointIndependent,
+		AddressDependent,
+		AddressAndPortDependent,
+		Fail
+	}
 }

+ 7 - 7
STUN/Enums/Method.cs

@@ -1,8 +1,8 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    internal enum Method : ushort
-    {
-        Binding = 0b00000_0_000_0_0001,
-        SharedSecret = 0b00000_0_000_0_0010,
-    }
-}
+	internal enum Method : ushort
+	{
+		Binding = 0b00000_0_000_0_0001,
+		SharedSecret = 0b00000_0_000_0_0010,
+	}
+}

+ 56 - 56
STUN/Enums/NatType.cs

@@ -1,66 +1,66 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc3489#section-5
-    /// https://tools.ietf.org/html/rfc3489#section-10.1
-    /// </summary>
-    public enum NatType
-    {
-        /// <summary>
-        /// Unknown
-        /// </summary>
-        Unknown,
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc3489#section-5
+	/// https://tools.ietf.org/html/rfc3489#section-10.1
+	/// </summary>
+	public enum NatType
+	{
+		/// <summary>
+		/// Unknown
+		/// </summary>
+		Unknown,
 
-        /// <summary>
-        /// Server is not unsupported for testing NAT type
-        /// </summary>
-        UnsupportedServer,
+		/// <summary>
+		/// Server is not unsupported for testing NAT type
+		/// </summary>
+		UnsupportedServer,
 
-        /// <summary>
-        /// UDP is always blocked.
-        /// </summary>
-        UdpBlocked,
+		/// <summary>
+		/// UDP is always blocked.
+		/// </summary>
+		UdpBlocked,
 
-        /// <summary>
-        /// No NAT, public IP, no firewall.
-        /// </summary>
-        OpenInternet,
+		/// <summary>
+		/// No NAT, public IP, no firewall.
+		/// </summary>
+		OpenInternet,
 
-        /// <summary>
-        /// No NAT, public IP, but symmetric UDP firewall.
-        /// </summary>
-        SymmetricUdpFirewall,
+		/// <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 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 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 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
-    }
+		/// <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
+	}
 }

+ 7 - 7
STUN/Enums/ProxyType.cs

@@ -1,8 +1,8 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    public enum ProxyType
-    {
-        Plain,
-        Socks5,
-    }
-}
+	public enum ProxyType
+	{
+		Plain = 0,
+		Socks5 = 1
+	}
+}

+ 34 - 34
STUN/Enums/StunMessageType.cs

@@ -1,41 +1,41 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    /// <summary>
-    /// This enum specifies STUN message type.
-    /// </summary>
-    /// <returns>
-    /// https://tools.ietf.org/html/rfc5389#section-6
-    /// </returns>
-    public enum StunMessageType : ushort
-    {
-        /// <summary>
-        /// STUN message is binding request.
-        /// </summary>
-        BindingRequest = Class.Request | Method.Binding,
+	/// <summary>
+	/// This enum specifies STUN message type.
+	/// </summary>
+	/// <returns>
+	/// https://tools.ietf.org/html/rfc5389#section-6
+	/// </returns>
+	public enum StunMessageType : ushort
+	{
+		/// <summary>
+		/// STUN message is binding request.
+		/// </summary>
+		BindingRequest = Class.Request | Method.Binding,
 
-        /// <summary>
-        /// STUN message is binding request success response.
-        /// </summary>
-        BindingResponse = Class.SuccessResponse | Method.Binding,
+		/// <summary>
+		/// STUN message is binding request success response.
+		/// </summary>
+		BindingResponse = Class.SuccessResponse | Method.Binding,
 
-        /// <summary>
-        /// STUN message is binding request error response.
-        /// </summary>
-        BindingErrorResponse = Class.ErrorResponse | Method.Binding,
+		/// <summary>
+		/// STUN message is binding request error response.
+		/// </summary>
+		BindingErrorResponse = Class.ErrorResponse | Method.Binding,
 
-        /// <summary>
-        /// STUN message is "shared secret" request.
-        /// </summary>
-        SharedSecretRequest = Class.Request | Method.SharedSecret,
+		/// <summary>
+		/// STUN message is "shared secret" request.
+		/// </summary>
+		SharedSecretRequest = Class.Request | Method.SharedSecret,
 
-        /// <summary>
-        /// STUN message is "shared secret" request success response.
-        /// </summary>
-        SharedSecretResponse = Class.SuccessResponse | Method.SharedSecret,
+		/// <summary>
+		/// STUN message is "shared secret" request success response.
+		/// </summary>
+		SharedSecretResponse = Class.SuccessResponse | Method.SharedSecret,
 
-        /// <summary>
-        /// STUN message is "shared secret" request error response.
-        /// </summary>
-        SharedSecretErrorResponse = Class.ErrorResponse | Method.SharedSecret,
-    }
+		/// <summary>
+		/// STUN message is "shared secret" request error response.
+		/// </summary>
+		SharedSecretErrorResponse = Class.ErrorResponse | Method.SharedSecret,
+	}
 }

+ 9 - 9
STUN/Enums/TransportType.cs

@@ -1,12 +1,12 @@
-namespace STUN.Enums
+namespace STUN.Enums
 {
-    // Only UDP is supported
+	// Only UDP is supported
 
-    public enum TransportType
-    {
-        Udp,
-        Tcp,
-        Tls,
-        Dtls,
-    }
+	public enum TransportType
+	{
+		Udp,
+		Tcp,
+		Tls,
+		Dtls,
+	}
 }

+ 3 - 0
STUN/FodyWeavers.xml

@@ -0,0 +1,3 @@
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+  <ReactiveUI />
+</Weavers>

+ 0 - 11
STUN/Interfaces/IDnsQuery.cs

@@ -1,11 +0,0 @@
-using System.Net;
-using System.Threading.Tasks;
-
-namespace STUN.Interfaces
-{
-    public interface IDnsQuery
-    {
-        public Task<IPAddress> QueryAsync(string host);
-        public IPAddress Query(string host);
-    }
-}

+ 0 - 16
STUN/Interfaces/IUdpProxy.cs

@@ -1,16 +0,0 @@
-using System;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace STUN.Interfaces
-{
-    public interface IUdpProxy : IDisposable
-    {
-        TimeSpan Timeout { get; set; }
-        IPEndPoint LocalEndPoint { get; }
-        Task ConnectAsync(CancellationToken token = default);
-        Task<(byte[], IPEndPoint, IPAddress)> ReceiveAsync(byte[] bytes, IPEndPoint remote, EndPoint receive, CancellationToken token = default);
-        Task DisconnectAsync();
-    }
-}

+ 93 - 80
STUN/Message/Attribute.cs

@@ -1,18 +1,18 @@
-using STUN.Message.Attributes;
+using STUN.Enums;
+using STUN.Message.Attributes;
 using STUN.Utils;
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using STUN.Enums;
 
 namespace STUN.Message
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-15
-    /// </summary>
-    public class Attribute
-    {
-        /*
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-15
+	/// </summary>
+	public class Attribute
+	{
+		/*
             Length 是大端
             必须4字节对齐
             对齐的字节可以是任意值
@@ -25,76 +25,89 @@ namespace STUN.Message
             +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
          */
 
-        public AttributeType Type { get; set; } = AttributeType.Useless;
-
-        public ushort Length { get; set; }
-
-        public int RealLength => Type == AttributeType.Useless ? 0 : 4 + Length + (4 - Length % 4) % 4;
-
-        public IAttribute Value { get; set; }
-
-        private readonly byte[] _magicCookie;
-        private readonly byte[] _transactionId;
-
-        public Attribute() { }
-
-        public Attribute(byte[] magicCookie, byte[] transactionId)
-        {
-            if (magicCookie.Length != 4 || transactionId.Length != 12)
-            {
-                throw new ArgumentException(@"Wrong Transaction ID length");
-            }
-
-            _magicCookie = magicCookie;
-
-            _transactionId = transactionId;
-        }
-
-        public IEnumerable<byte> ToBytes()
-        {
-            var res = new List<byte>();
-
-            res.AddRange(Convert.ToUInt16(Type).ToBe());
-            res.AddRange(Length.ToBe());
-            res.AddRange(Value.Bytes);
-
-            var n = (4 - res.Count % 4) % 4; // 填充的字节数
-            res.AddRange(BitUtils.GetRandomBytes(n));
-
-            return res;
-        }
-
-        /// <returns>
-        /// Parse 成功字节,0 则表示 Parse 失败
-        /// </returns>
-        public int TryParse(byte[] bytes)
-        {
-            if (bytes.Length < 4) return 0;
-
-            Type = (AttributeType)BitUtils.FromBe(bytes[0], bytes[1]);
-
-            Length = BitUtils.FromBe(bytes[2], bytes[3]);
-
-            if (bytes.Length < 4 + Length) return 0;
-
-            var value = bytes.Skip(4).Take(Length).ToArray();
-
-            IAttribute t = Type switch
-            {
-                AttributeType.MappedAddress => new MappedAddressAttribute(),
-                AttributeType.XorMappedAddress => new XorMappedAddressAttribute(_magicCookie, _transactionId),
-                AttributeType.ResponseAddress => new ResponseAddressAttribute(),
-                AttributeType.ChangeRequest => new ChangeRequestAttribute(),
-                AttributeType.SourceAddress => new SourceAddressAttribute(),
-                AttributeType.ChangedAddress => new ChangedAddressAttribute(),
-                AttributeType.OtherAddress => new OtherAddressAttribute(),
-                AttributeType.ReflectedFrom => new ReflectedFromAttribute(),
-                AttributeType.ErrorCode => new ErrorCodeAttribute(),
-                _ => new UselessAttribute()
-            };
-            Value = t.TryParse(value) ? t : null;
-
-            return 4 + Length + (4 - Length % 4) % 4; // 对齐
-        }
-    }
+		public AttributeType Type { get; set; } = AttributeType.Useless;
+
+		public ushort Length { get; set; }
+
+		public int RealLength => Type == AttributeType.Useless ? 0 : 4 + Length + (4 - Length % 4) % 4;
+
+		public IAttribute Value { get; set; } = new UselessAttribute();
+
+		private readonly byte[] _magicCookie;
+		private readonly byte[] _transactionId;
+
+		public Attribute()
+		{
+			_magicCookie = new byte[4];
+			_transactionId = new byte[12];
+		}
+
+		public Attribute(byte[] magicCookie, byte[] transactionId)
+		{
+			if (magicCookie.Length != 4 || transactionId.Length != 12)
+			{
+				throw new ArgumentException(@"Wrong Transaction ID length");
+			}
+
+			_magicCookie = magicCookie;
+
+			_transactionId = transactionId;
+		}
+
+		public IEnumerable<byte> ToBytes()
+		{
+			var res = new List<byte>();
+
+			res.AddRange(Convert.ToUInt16(Type).ToBe());
+			res.AddRange(Length.ToBe());
+			res.AddRange(Value.Bytes);
+
+			var n = (4 - res.Count % 4) % 4; // 填充的字节数
+			res.AddRange(BitUtils.GetRandomBytes(n));
+
+			return res;
+		}
+
+		/// <returns>
+		/// Parse 成功字节,0 则表示 Parse 失败
+		/// </returns>
+		public int TryParse(byte[] bytes)
+		{
+			if (bytes.Length < 4)
+			{
+				return 0;
+			}
+
+			Type = (AttributeType)BitUtils.FromBe(bytes[0], bytes[1]);
+
+			Length = BitUtils.FromBe(bytes[2], bytes[3]);
+
+			if (bytes.Length < 4 + Length)
+			{
+				return 0;
+			}
+
+			var value = bytes.Skip(4).Take(Length).ToArray();
+
+			IAttribute t = Type switch
+			{
+				AttributeType.MappedAddress => new MappedAddressAttribute(),
+				AttributeType.XorMappedAddress => new XorMappedAddressAttribute(_magicCookie, _transactionId),
+				AttributeType.ResponseAddress => new ResponseAddressAttribute(),
+				AttributeType.ChangeRequest => new ChangeRequestAttribute(),
+				AttributeType.SourceAddress => new SourceAddressAttribute(),
+				AttributeType.ChangedAddress => new ChangedAddressAttribute(),
+				AttributeType.OtherAddress => new OtherAddressAttribute(),
+				AttributeType.ReflectedFrom => new ReflectedFromAttribute(),
+				AttributeType.ErrorCode => new ErrorCodeAttribute(),
+				_ => new UselessAttribute()
+			};
+			if (t.TryParse(value))
+			{
+				Value = t;
+			}
+
+			return 4 + Length + (4 - Length % 4) % 4; // 对齐
+		}
+	}
 }

+ 62 - 56
STUN/Message/Attributes/AddressAttribute.cs

@@ -1,76 +1,82 @@
-using STUN.Utils;
+using STUN.Enums;
+using STUN.Utils;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Net;
 using System.Net.Sockets;
-using STUN.Enums;
 
 namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-15.1
-    /// </summary>
-    public abstract class AddressAttribute : IAttribute
-    {
-        public virtual IEnumerable<byte> Bytes
-        {
-            get
-            {
-                if (Address == null)
-                {
-                    return Array.Empty<byte>();
-                }
-                var res = new List<byte> { 0, (byte)Family };
-                res.AddRange(Port.ToBe());
-                res.AddRange(Address.GetAddressBytes());
-                return res;
-            }
-        }
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-15.1
+	/// </summary>
+	public abstract class AddressAttribute : IAttribute
+	{
+		public virtual IEnumerable<byte> Bytes
+		{
+			get
+			{
+				if (Address is null)
+				{
+					return Array.Empty<byte>();
+				}
+				var res = new List<byte> { 0, (byte)Family };
+				res.AddRange(Port.ToBe());
+				res.AddRange(Address.GetAddressBytes());
+				return res;
+			}
+		}
 
-        public IpFamily Family { get; set; }
+		public IpFamily Family { get; set; }
 
-        public ushort Port { get; set; }
+		public ushort Port { get; set; }
 
-        public IPAddress Address { get; set; }
+		public IPAddress? Address { get; set; }
 
-        public virtual bool TryParse(byte[] bytes)
-        {
-            var length = 4;
+		public virtual bool TryParse(byte[] bytes)
+		{
+			var length = 4;
 
-            if (bytes.Length < length) return false;
+			if (bytes.Length < length)
+			{
+				return false;
+			}
 
-            Family = (IpFamily)bytes[1];
+			Family = (IpFamily)bytes[1];
 
-            switch (Family)
-            {
-                case IpFamily.IPv4:
-                    length += 4;
-                    break;
-                case IpFamily.IPv6:
-                    length += 16;
-                    break;
-                default:
-                    return false;
-            }
+			switch (Family)
+			{
+				case IpFamily.IPv4:
+					length += 4;
+					break;
+				case IpFamily.IPv6:
+					length += 16;
+					break;
+				default:
+					return false;
+			}
 
-            if (bytes.Length != length) return false;
+			if (bytes.Length != length)
+			{
+				return false;
+			}
 
-            Port = BitUtils.FromBe(bytes[2], bytes[3]);
+			Port = BitUtils.FromBe(bytes[2], bytes[3]);
 
-            Address = new IPAddress(bytes.Skip(4).ToArray());
+			Address = new IPAddress(bytes.Skip(4).ToArray());
 
-            return true;
-        }
+			return true;
+		}
 
-        public override string ToString()
-        {
-            return Address?.AddressFamily switch
-            {
-                AddressFamily.InterNetwork => $@"{Address}:{Port}",
-                AddressFamily.InterNetworkV6 => $@"[{Address}]:{Port}",
-                _ => base.ToString()
-            };
-        }
-    }
-}
+		public override string ToString()
+		{
+			return Address?.AddressFamily switch
+			{
+				AddressFamily.InterNetwork => $@"{Address}:{Port}",
+				AddressFamily.InterNetworkV6 => $@"[{Address}]:{Port}",
+				_ => base.ToString()
+			};
+		}
+	}
+}

+ 22 - 19
STUN/Message/Attributes/ChangeRequestAttribute.cs

@@ -1,30 +1,33 @@
-using System;
+using System;
 using System.Collections;
 using System.Collections.Generic;
 
 namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5780#section-7.2
-    /// </summary>
-    public class ChangeRequestAttribute : IAttribute
-    {
-        public IEnumerable<byte> Bytes => new byte[] { 0, 0, 0, (byte)(Convert.ToInt32(ChangeIp) << 2 | Convert.ToInt32(ChangePort) << 1) };
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5780#section-7.2
+	/// </summary>
+	public class ChangeRequestAttribute : IAttribute
+	{
+		public IEnumerable<byte> Bytes => new byte[] { 0, 0, 0, (byte)(Convert.ToInt32(ChangeIp) << 2 | Convert.ToInt32(ChangePort) << 1) };
 
-        public bool ChangeIp { get; set; }
+		public bool ChangeIp { get; set; }
 
-        public bool ChangePort { get; set; }
+		public bool ChangePort { get; set; }
 
-        public bool TryParse(byte[] bytes)
-        {
-            if (bytes.Length != 4) return false;
+		public bool TryParse(byte[] bytes)
+		{
+			if (bytes.Length != 4)
+			{
+				return false;
+			}
 
-            var bits = new BitArray(bytes);
+			var bits = new BitArray(bytes);
 
-            ChangeIp = bits[29];
-            ChangePort = bits[30];
+			ChangeIp = bits[29];
+			ChangePort = bits[30];
 
-            return true;
-        }
-    }
-}
+			return true;
+		}
+	}
+}

+ 6 - 6
STUN/Message/Attributes/ChangedAddressAttribute.cs

@@ -1,7 +1,7 @@
-namespace STUN.Message.Attributes
+namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc3489#section-11.2.3
-    /// </summary>
-    public class ChangedAddressAttribute : AddressAttribute { }
-}
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc3489#section-11.2.3
+	/// </summary>
+	public class ChangedAddressAttribute : AddressAttribute { }
+}

+ 42 - 39
STUN/Message/Attributes/ErrorCodeAttribute.cs

@@ -1,45 +1,48 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 
 namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-15.6
-    /// </summary>
-    public class ErrorCodeAttribute : IAttribute
-    {
-        public IEnumerable<byte> Bytes
-        {
-            get
-            {
-                var res = new List<byte> { 0, 0, Class, Number };
-                res.AddRange(Encoding.UTF8.GetBytes(ReasonPhrase).Take(MaxReasonPhraseBytesLength));
-                return res;
-            }
-        }
-
-        public ushort ErrorCode { get; set; }
-        public string ReasonPhrase { get; set; }
-
-        public byte Class => (byte)(ErrorCode % 1000 / 100);
-        public byte Number => (byte)(ErrorCode % 100);
-
-        public const int MaxReasonPhraseBytesLength = 762;
-
-        public bool TryParse(byte[] bytes)
-        {
-            if (bytes.Length < 4 || bytes.Length > 4 + MaxReasonPhraseBytesLength) return false;
-
-            var @class = (byte)(bytes[2] & 0b111);
-            var number = Math.Min(bytes[3], (ushort)99);
-
-            ErrorCode = (ushort)(@class * 100 + number);
-
-            ReasonPhrase = bytes.Length > 4 ? Encoding.UTF8.GetString(bytes, 4, bytes.Length - 4) : string.Empty;
-
-            return true;
-        }
-    }
-}
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-15.6
+	/// </summary>
+	public class ErrorCodeAttribute : IAttribute
+	{
+		public IEnumerable<byte> Bytes
+		{
+			get
+			{
+				var res = new List<byte> { 0, 0, Class, Number };
+				res.AddRange(Encoding.UTF8.GetBytes(ReasonPhrase ?? string.Empty).Take(MaxReasonPhraseBytesLength));
+				return res;
+			}
+		}
+
+		public ushort ErrorCode { get; set; }
+		public string? ReasonPhrase { get; set; }
+
+		public byte Class => (byte)(ErrorCode % 1000 / 100);
+		public byte Number => (byte)(ErrorCode % 100);
+
+		public const int MaxReasonPhraseBytesLength = 762;
+
+		public bool TryParse(byte[] bytes)
+		{
+			if (bytes.Length is < 4 or > (4 + MaxReasonPhraseBytesLength))
+			{
+				return false;
+			}
+
+			var @class = (byte)(bytes[2] & 0b111);
+			var number = Math.Min(bytes[3], (ushort)99);
+
+			ErrorCode = (ushort)(@class * 100 + number);
+
+			ReasonPhrase = bytes.Length > 4 ? Encoding.UTF8.GetString(bytes, 4, bytes.Length - 4) : string.Empty;
+
+			return true;
+		}
+	}
+}

+ 5 - 5
STUN/Message/Attributes/IAttribute.cs

@@ -2,10 +2,10 @@ using System.Collections.Generic;
 
 namespace STUN.Message.Attributes
 {
-    public interface IAttribute
-    {
-        public IEnumerable<byte> Bytes { get; }
+	public interface IAttribute
+	{
+		public IEnumerable<byte> Bytes { get; }
 
-        public bool TryParse(byte[] bytes);
-    }
+		public bool TryParse(byte[] bytes);
+	}
 }

+ 6 - 6
STUN/Message/Attributes/MappedAddressAttribute.cs

@@ -1,7 +1,7 @@
-namespace STUN.Message.Attributes
+namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-15.1
-    /// </summary>
-    public class MappedAddressAttribute : AddressAttribute { }
-}
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-15.1
+	/// </summary>
+	public class MappedAddressAttribute : AddressAttribute { }
+}

+ 6 - 6
STUN/Message/Attributes/OtherAddressAttribute.cs

@@ -1,7 +1,7 @@
-namespace STUN.Message.Attributes
+namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5780#section-7.4
-    /// </summary>
-    public class OtherAddressAttribute : AddressAttribute { }
-}
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5780#section-7.4
+	/// </summary>
+	public class OtherAddressAttribute : AddressAttribute { }
+}

+ 2 - 2
STUN/Message/Attributes/ReflectedFromAttribute.cs

@@ -1,7 +1,7 @@
-namespace STUN.Message.Attributes
+namespace STUN.Message.Attributes
 {
     /// <summary>
     /// https://tools.ietf.org/html/rfc3489#section-11.2.11
     /// </summary>
     public class ReflectedFromAttribute : AddressAttribute { }
-}
+}

+ 6 - 6
STUN/Message/Attributes/ResponseAddressAttribute.cs

@@ -1,7 +1,7 @@
-namespace STUN.Message.Attributes
+namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc3489#section-11.2.2
-    /// </summary>
-    public class ResponseAddressAttribute : AddressAttribute { }
-}
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc3489#section-11.2.2
+	/// </summary>
+	public class ResponseAddressAttribute : AddressAttribute { }
+}

+ 6 - 6
STUN/Message/Attributes/SourceAddressAttribute.cs

@@ -1,7 +1,7 @@
-namespace STUN.Message.Attributes
+namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc3489#section-11.2.5
-    /// </summary>
-    public class SourceAddressAttribute : AddressAttribute { }
-}
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc3489#section-11.2.5
+	/// </summary>
+	public class SourceAddressAttribute : AddressAttribute { }
+}

+ 37 - 34
STUN/Message/Attributes/UnknownAttribute.cs

@@ -1,44 +1,47 @@
-using STUN.Utils;
+using STUN.Enums;
+using STUN.Utils;
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using STUN.Enums;
 
 namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-15.9
-    /// </summary>
-    public class UnknownAttribute : IAttribute
-    {
-        public IEnumerable<byte> Bytes
-        {
-            get
-            {
-                var res = new List<byte>();
-                foreach (var type in Types)
-                {
-                    res.AddRange(Convert.ToUInt16(type).ToBe());
-                }
-                return res;
-            }
-        }
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-15.9
+	/// </summary>
+	public class UnknownAttribute : IAttribute
+	{
+		public IEnumerable<byte> Bytes
+		{
+			get
+			{
+				var res = new List<byte>();
+				foreach (var type in Types)
+				{
+					res.AddRange(Convert.ToUInt16(type).ToBe());
+				}
+				return res;
+			}
+		}
 
-        public IEnumerable<AttributeType> Types { get; set; }
+		public IEnumerable<AttributeType> Types { get; set; } = Array.Empty<AttributeType>();
 
-        public bool TryParse(byte[] bytes)
-        {
-            if (bytes.Length < 2 || (bytes.Length & 1) == 1) return false;
+		public bool TryParse(byte[] bytes)
+		{
+			if (bytes.Length < 2 || (bytes.Length & 1) == 1)
+			{
+				return false;
+			}
 
-            var list = new List<AttributeType>();
-            for (var i = 0; i < bytes.Length >> 1; ++i)
-            {
-                var b = bytes.Skip(i << 1).Take(2);
-                list.Add((AttributeType)BitUtils.FromBe(b));
-            }
-            Types = list;
+			var list = new List<AttributeType>();
+			for (var i = 0; i < bytes.Length >> 1; ++i)
+			{
+				var b = bytes.Skip(i << 1).Take(2);
+				list.Add((AttributeType)BitUtils.FromBe(b));
+			}
+			Types = list;
 
-            return true;
-        }
-    }
-}
+			return true;
+		}
+	}
+}

+ 16 - 15
STUN/Message/Attributes/UselessAttribute.cs

@@ -1,20 +1,21 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 
 namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// 无法理解的属性
-    /// </summary>
-    public class UselessAttribute : IAttribute
-    {
-        public IEnumerable<byte> Bytes => _bytes;
+	/// <summary>
+	/// 无法理解的属性
+	/// </summary>
+	public class UselessAttribute : IAttribute
+	{
+		public IEnumerable<byte> Bytes => _bytes;
 
-        private byte[] _bytes;
+		private byte[] _bytes = Array.Empty<byte>();
 
-        public bool TryParse(byte[] bytes)
-        {
-            _bytes = bytes;
-            return _bytes != null;
-        }
-    }
-}
+		public bool TryParse(byte[] bytes)
+		{
+			_bytes = bytes;
+			return true;
+		}
+	}
+}

+ 54 - 51
STUN/Message/Attributes/XorMappedAddressAttribute.cs

@@ -1,4 +1,4 @@
-using STUN.Utils;
+using STUN.Utils;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -6,63 +6,66 @@ using System.Net;
 
 namespace STUN.Message.Attributes
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-15.2
-    /// </summary>
-    public class XorMappedAddressAttribute : AddressAttribute
-    {
-        private readonly byte[] _magicCookie;
-        private readonly byte[] _transactionId;
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-15.2
+	/// </summary>
+	public class XorMappedAddressAttribute : AddressAttribute
+	{
+		private readonly byte[] _magicCookie;
+		private readonly byte[] _transactionId;
 
-        public XorMappedAddressAttribute(byte[] magicCookie, byte[] transactionId)
-        {
-            _magicCookie = magicCookie;
-            _transactionId = transactionId;
-        }
+		public XorMappedAddressAttribute(byte[] magicCookie, byte[] transactionId)
+		{
+			_magicCookie = magicCookie;
+			_transactionId = transactionId;
+		}
 
-        public override IEnumerable<byte> Bytes
-        {
-            get
-            {
-                if (Address == null)
-                {
-                    return Array.Empty<byte>();
-                }
+		public override IEnumerable<byte> Bytes
+		{
+			get
+			{
+				if (Address is null)
+				{
+					return Array.Empty<byte>();
+				}
 
-                var res = new List<byte> { 0, (byte)Family };
-                res.AddRange(Xor(Port).ToBe());
-                res.AddRange(Xor(Address).GetAddressBytes());
-                return res;
-            }
-        }
+				var res = new List<byte> { 0, (byte)Family };
+				res.AddRange(Xor(Port).ToBe());
+				res.AddRange(Xor(Address).GetAddressBytes());
+				return res;
+			}
+		}
 
-        public override bool TryParse(byte[] bytes)
-        {
-            if (!base.TryParse(bytes)) return false;
+		public override bool TryParse(byte[] bytes)
+		{
+			if (!base.TryParse(bytes))
+			{
+				return false;
+			}
 
-            Port = Xor(Port);
+			Port = Xor(Port);
 
-            Address = Xor(Address);
+			Address = Xor(Address!);
 
-            return true;
-        }
+			return true;
+		}
 
-        private ushort Xor(ushort port)
-        {
-            var b = port.ToBe().ToArray();
-            var xPort = BitUtils.FromBe((byte)(_magicCookie[0] ^ b[0]), (byte)(_magicCookie[1] ^ b[1]));
-            return xPort;
-        }
+		private ushort Xor(ushort port)
+		{
+			var b = port.ToBe().ToArray();
+			var xPort = BitUtils.FromBe((byte)(_magicCookie[0] ^ b[0]), (byte)(_magicCookie[1] ^ b[1]));
+			return xPort;
+		}
 
-        private IPAddress Xor(IPAddress address)
-        {
-            var b = address.GetAddressBytes();
-            var xor = _magicCookie.Concat(_transactionId).ToArray();
-            for (var i = 0; i < b.Length; ++i)
-            {
-                b[i] ^= xor[i];
-            }
-            return new IPAddress(b);
-        }
-    }
+		private IPAddress Xor(IPAddress address)
+		{
+			var b = address.GetAddressBytes();
+			var xor = _magicCookie.Concat(_transactionId).ToArray();
+			for (var i = 0; i < b.Length; ++i)
+			{
+				b[i] ^= xor[i];
+			}
+			return new IPAddress(b);
+		}
+	}
 }

+ 68 - 59
STUN/Message/StunMessage5389.cs

@@ -1,91 +1,100 @@
-using STUN.Utils;
+using STUN.Enums;
+using STUN.Utils;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
-using STUN.Enums;
 
 namespace STUN.Message
 {
-    /// <summary>
-    /// https://tools.ietf.org/html/rfc5389#section-6
-    /// </summary>
-    public class StunMessage5389
-    {
-        public IEnumerable<byte> Header =>
-                Convert.ToUInt16(StunMessageType).ToBe().Concat(MessageLengthBytes)
-                        .Concat(MagicCookieBytes).Concat(TransactionId);
+	/// <summary>
+	/// https://tools.ietf.org/html/rfc5389#section-6
+	/// </summary>
+	public class StunMessage5389
+	{
+		public IEnumerable<byte> Header =>
+				Convert.ToUInt16(StunMessageType).ToBe().Concat(MessageLengthBytes)
+						.Concat(MagicCookieBytes).Concat(TransactionId);
 
-        public IEnumerable<Attribute> Attributes { get; set; }
+		public IEnumerable<Attribute> Attributes { get; set; }
 
-        #region Header
+		#region Header
 
-        public StunMessageType StunMessageType { get; set; }
+		public StunMessageType StunMessageType { get; set; }
 
-        public ushort MessageLength => Attributes.Aggregate<Attribute, ushort>(0, (current, attribute) => (ushort)(current + Convert.ToUInt16(attribute.RealLength)));
+		public ushort MessageLength => Attributes.Aggregate<Attribute, ushort>(0, (current, attribute) => (ushort)(current + Convert.ToUInt16(attribute.RealLength)));
 
-        public IEnumerable<byte> MessageLengthBytes => MessageLength.ToBe();
+		public IEnumerable<byte> MessageLengthBytes => MessageLength.ToBe();
 
-        public int MagicCookie { get; set; }
+		public int MagicCookie { get; set; }
 
-        public IEnumerable<byte> MagicCookieBytes => MagicCookie.ToBe();
+		public IEnumerable<byte> MagicCookieBytes => MagicCookie.ToBe();
 
-        public byte[] TransactionId { get; private set; }
+		public byte[] TransactionId { get; private set; }
 
-        public IEnumerable<byte> ClassicTransactionId => MagicCookieBytes.Concat(TransactionId);
+		public IEnumerable<byte> ClassicTransactionId => MagicCookieBytes.Concat(TransactionId);
 
-        #endregion
+		#endregion
 
-        public IEnumerable<byte> Bytes =>
-                Attributes.Aggregate(Header, (current, attribute) => current.Concat(attribute.ToBytes()));
+		public IEnumerable<byte> Bytes =>
+				Attributes.Aggregate(Header, (current, attribute) => current.Concat(attribute.ToBytes()));
 
-        public StunMessage5389()
-        {
-            Attributes = Array.Empty<Attribute>();
-            StunMessageType = StunMessageType.BindingRequest;
-            MagicCookie = 0x2112A442;
-            TransactionId = BitUtils.GetRandomBytes(12).ToArray();
-        }
+		public StunMessage5389()
+		{
+			Attributes = Array.Empty<Attribute>();
+			StunMessageType = StunMessageType.BindingRequest;
+			MagicCookie = 0x2112A442;
+			TransactionId = BitUtils.GetRandomBytes(12).ToArray();
+		}
 
-        public bool TryParse(byte[] bytes)
-        {
-            if (bytes.Length < 20) return false; // Check length
+		public bool TryParse(byte[] bytes)
+		{
+			if (bytes.Length < 20)
+			{
+				return false; // Check length
+			}
 
-            StunMessageType = (StunMessageType)BitUtils.FromBe((byte)(bytes[0] & 0b0011_1111), bytes[1]);
+			StunMessageType = (StunMessageType)BitUtils.FromBe((byte)(bytes[0] & 0b0011_1111), bytes[1]);
 
-            if (!Enum.IsDefined(typeof(StunMessageType), StunMessageType)) return false;
+			if (!Enum.IsDefined(typeof(StunMessageType), StunMessageType))
+			{
+				return false;
+			}
 
-            var length = BitUtils.FromBe(bytes[2], bytes[3]);
+			var length = BitUtils.FromBe(bytes[2], bytes[3]);
 
-            MagicCookie = BitUtils.FromBeToInt(bytes.Skip(4).Take(4));
+			MagicCookie = BitUtils.FromBeToInt(bytes.Skip(4).Take(4));
 
-            TransactionId = bytes.Skip(8).Take(12).ToArray();
+			TransactionId = bytes.Skip(8).Take(12).ToArray();
 
-            if (bytes.Length != length + 20) return false; // Check length
+			if (bytes.Length != length + 20)
+			{
+				return false; // Check length
+			}
 
-            var list = new List<Attribute>();
+			var list = new List<Attribute>();
 
-            var b = bytes.Skip(20).ToArray();
+			var b = bytes.Skip(20).ToArray();
 
-            while (b.Length > 0)
-            {
-                var attribute = new Attribute(MagicCookieBytes.ToArray(), TransactionId);
-                var offset = attribute.TryParse(b);
-                if (offset > 0)
-                {
-                    list.Add(attribute);
-                    b = b.Skip(offset).ToArray();
-                }
-                else
-                {
-                    Debug.WriteLine($@"[Warning] Ignore wrong attribute: {BitConverter.ToString(b)}");
-                    break;
-                }
-            }
+			while (b.Length > 0)
+			{
+				var attribute = new Attribute(MagicCookieBytes.ToArray(), TransactionId);
+				var offset = attribute.TryParse(b);
+				if (offset > 0)
+				{
+					list.Add(attribute);
+					b = b.Skip(offset).ToArray();
+				}
+				else
+				{
+					Debug.WriteLine($@"[Warning] Ignore wrong attribute: {BitConverter.ToString(b)}");
+					break;
+				}
+			}
 
-            Attributes = list;
+			Attributes = list;
 
-            return true;
-        }
-    }
+			return true;
+		}
+	}
 }

+ 16 - 0
STUN/Proxy/IUdpProxy.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace STUN.Proxy
+{
+	public interface IUdpProxy : IDisposable
+	{
+		TimeSpan Timeout { get; set; }
+		IPEndPoint LocalEndPoint { get; }
+		Task ConnectAsync(CancellationToken token = default);
+		Task<(byte[], IPEndPoint, IPAddress)> ReceiveAsync(byte[] bytes, IPEndPoint remote, EndPoint receive, CancellationToken token = default);
+		Task DisconnectAsync();
+	}
+}

+ 36 - 45
STUN/Proxy/NoneUdpProxy.cs

@@ -1,4 +1,4 @@
-using STUN.Interfaces;
+using STUN.Utils;
 using System;
 using System.Diagnostics;
 using System.Linq;
@@ -9,59 +9,50 @@ using System.Threading.Tasks;
 
 namespace STUN.Proxy
 {
-    public class NoneUdpProxy : IUdpProxy
-    {
-        public TimeSpan Timeout
-        {
-            get => TimeSpan.FromMilliseconds(UdpClient.Client.ReceiveTimeout);
-            set => UdpClient.Client.ReceiveTimeout = Convert.ToInt32(value.TotalMilliseconds);
-        }
+	public class NoneUdpProxy : IUdpProxy
+	{
+		public TimeSpan Timeout
+		{
+			get => TimeSpan.FromMilliseconds(_udpClient.Client.ReceiveTimeout);
+			set => _udpClient.Client.ReceiveTimeout = Convert.ToInt32(value.TotalMilliseconds);
+		}
 
-        public IPEndPoint LocalEndPoint => (IPEndPoint)UdpClient.Client.LocalEndPoint;
+		public IPEndPoint LocalEndPoint => (IPEndPoint)_udpClient.Client.LocalEndPoint;
 
-        protected UdpClient UdpClient;
+		private readonly UdpClient _udpClient;
 
-        public NoneUdpProxy(IPEndPoint local)
-        {
-            UdpClient = local == null ? new UdpClient() : new UdpClient(local);
-        }
+		public NoneUdpProxy(IPEndPoint? local)
+		{
+			_udpClient = local is null ? new UdpClient() : new UdpClient(local);
+		}
 
-        public Task ConnectAsync(CancellationToken token = default)
-        {
-            return Task.CompletedTask;
-        }
+		public Task ConnectAsync(CancellationToken token = default)
+		{
+			return Task.CompletedTask;
+		}
 
-        public Task DisconnectAsync()
-        {
-            UdpClient.Close();
-            return Task.CompletedTask;
-        }
+		public Task DisconnectAsync()
+		{
+			return Task.CompletedTask;
+		}
 
-        public async Task<(byte[], IPEndPoint, IPAddress)> ReceiveAsync(byte[] bytes, IPEndPoint remote, EndPoint receive, CancellationToken token = default)
-        {
-            var localEndPoint = (IPEndPoint)UdpClient.Client.LocalEndPoint;
+		public async Task<(byte[], IPEndPoint, IPAddress)> ReceiveAsync(byte[] bytes, IPEndPoint remote, EndPoint receive, CancellationToken token = default)
+		{
+			var localEndPoint = (IPEndPoint)_udpClient.Client.LocalEndPoint;
 
-            Debug.WriteLine($@"{localEndPoint} => {remote} {bytes.Length} 字节");
+			Debug.WriteLine($@"{localEndPoint} => {remote} {bytes.Length} 字节");
 
-            await UdpClient.SendAsync(bytes, bytes.Length, remote);
+			await _udpClient.SendAsync(bytes, bytes.Length, remote);
 
-            var res = new byte[ushort.MaxValue];
-            var flag = SocketFlags.None;
+			var res = new byte[ushort.MaxValue];
 
-            var length = UdpClient.Client.ReceiveMessageFrom(res, 0, res.Length, ref flag, ref receive, out var ipPacketInformation);
+			var (local, length, rec) = await _udpClient.Client.ReceiveMessageFromAsync(receive, res, SocketFlags.None);
+			return (res.Take(length).ToArray(), rec, local);
+		}
 
-            var local = ipPacketInformation.Address;
-
-            Debug.WriteLine($@"{(IPEndPoint)receive} => {local} {length} 字节");
-
-            return (res.Take(length).ToArray(),
-                    (IPEndPoint)receive
-                    , local);
-        }
-
-        public void Dispose()
-        {
-            UdpClient?.Dispose();
-        }
-    }
+		public void Dispose()
+		{
+			_udpClient.Dispose();
+		}
+	}
 }

+ 17 - 14
STUN/Proxy/ProxyFactory.cs

@@ -1,20 +1,23 @@
-using STUN.Enums;
-using STUN.Interfaces;
+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)
-        {
-            return type switch
-            {
-                ProxyType.Plain => new NoneUdpProxy(local),
-                ProxyType.Socks5 => new Socks5UdpProxy(local, proxy, user, password),
-                _ => throw new NotSupportedException(type.ToString())
-            };
-        }
-    }
+	public static class ProxyFactory
+	{
+		public static IUdpProxy CreateProxy(ProxyType type, IPEndPoint? local, IPEndPoint? proxy, string? user = null, string? password = null)
+		{
+			if (proxy is null)
+			{
+				throw new ArgumentNullException(nameof(proxy), @"Proxy server is null");
+			}
+			return type switch
+			{
+				ProxyType.Plain => new NoneUdpProxy(local),
+				ProxyType.Socks5 => new Socks5UdpProxy(local, proxy, user, password),
+				_ => throw new NotSupportedException(type.ToString())
+			};
+		}
+	}
 }

+ 252 - 254
STUN/Proxy/Socks5UdpProxy.cs

@@ -1,4 +1,3 @@
-using STUN.Interfaces;
 using STUN.Utils;
 using System;
 using System.Diagnostics;
@@ -11,257 +10,256 @@ using System.Threading.Tasks;
 
 namespace STUN.Proxy
 {
-    public class Socks5UdpProxy : IUdpProxy
-    {
-        private readonly TcpClient _assoc;
-        private readonly IPEndPoint _socksTcpEndPoint;
-
-        private IPEndPoint _assocEndPoint;
-
-        public TimeSpan Timeout
-        {
-            get => TimeSpan.FromMilliseconds(_udpClient.Client.ReceiveTimeout);
-            set
-            {
-                var timeout = Convert.ToInt32(value.TotalMilliseconds);
-                _udpClient.Client.ReceiveTimeout = timeout;
-                _assoc.ReceiveTimeout = timeout;
-            }
-        }
-
-        public IPEndPoint LocalEndPoint => (IPEndPoint)_udpClient.Client.LocalEndPoint;
-
-        private readonly UdpClient _udpClient;
-
-        private readonly string _user;
-        private readonly string _password;
-
-        public Socks5UdpProxy(IPEndPoint local, IPEndPoint proxy)
-        {
-            _udpClient = local == null ? new UdpClient() : new UdpClient(local);
-            _assoc = new TcpClient(proxy.AddressFamily);
-            _socksTcpEndPoint = proxy;
-        }
-
-        public Socks5UdpProxy(IPEndPoint local, IPEndPoint proxy, string user, string password) : this(local, proxy)
-        {
-            _user = user;
-            _password = password;
-        }
-
-        public async Task ConnectAsync(CancellationToken token = default)
-        {
-            try
-            {
-                var buf = new byte[1024];
-                await _assoc.ConnectAsync(_socksTcpEndPoint.Address, _socksTcpEndPoint.Port);
-                var s = _assoc.GetStream();
-                using var _ = token.Register(() => s.Close());
-                var requestPasswordAuth = !string.IsNullOrEmpty(_user);
-
-                #region Handshake
-
-                // we have no GSS-API support
-                var handShake = requestPasswordAuth ? new byte[] { 5, 2, 0, 2 } : new byte[] { 5, 1, 0 };
-                await s.WriteAsync(handShake, 0, handShake.Length, token);
-
-                // 5 auth(ff=deny)
-                if (await s.ReadAsync(buf, 0, 2, token) != 2)
-                {
-                    throw new ProtocolViolationException();
-                }
-
-                if (buf[0] != 5)
-                {
-                    throw new ProtocolViolationException();
-                }
-
-                #endregion
-
-                #region Auth
-
-                var auth = buf[1];
-                switch (auth)
-                {
-                    case 0:
-                        break;
-                    case 2:
-                        var uByte = Encoding.UTF8.GetBytes(_user);
-                        var pByte = Encoding.UTF8.GetBytes(_password);
-                        buf[0] = 1;
-                        buf[1] = (byte)uByte.Length;
-                        Array.Copy(uByte, 0, buf, 2, uByte.Length);
-                        buf[uByte.Length + 2] = (byte)pByte.Length;
-                        Array.Copy(pByte, 0, buf, uByte.Length + 3, pByte.Length);
-                        // 1 userLen user passLen pass
-                        await s.WriteAsync(buf, 0, uByte.Length + pByte.Length + 3, token);
-                        // 1 state(0=ok)
-                        if (await s.ReadAsync(buf, 0, 2, token) != 2)
-                        {
-                            throw new ProtocolViolationException();
-                        }
-
-                        if (buf[0] != 1)
-                        {
-                            throw new ProtocolViolationException();
-                        }
-
-                        if (buf[1] != 0)
-                        {
-                            throw new UnauthorizedAccessException();
-                        }
-
-                        break;
-                    case 0xff:
-                        throw new UnauthorizedAccessException();
-                    default:
-                        throw new ProtocolViolationException();
-                }
-
-                #endregion
-
-                #region UDP Assoc Send
-
-                buf[0] = 5;
-                buf[1] = 3;
-                buf[2] = 0;
-
-                var abyte = GetEndPointByte(new IPEndPoint(IPAddress.Any, IPEndPoint.MinPort));
-                var addrLen = abyte.Length;
-                Array.Copy(abyte, 0, buf, 3, addrLen);
-                // 5 cmd(3=udpassoc) 0 atyp(1=v4 3=dns 4=v5) addr port
-                await s.WriteAsync(buf, 0, addrLen + 3, token);
-
-                #endregion
-
-                #region UDP Assoc Response
-
-                if (await s.ReadAsync(buf, 0, 4, token) != 4)
-                {
-                    throw new ProtocolViolationException();
-                }
-
-                if (buf[0] != 5 || buf[2] != 0)
-                {
-                    throw new ProtocolViolationException();
-                }
-
-                if (buf[1] != 0)
-                {
-                    throw new UnauthorizedAccessException();
-                }
-
-                addrLen = GetAddressLength(buf[3]);
-
-                var addr = new byte[addrLen];
-                if (await s.ReadAsync(addr, 0, addrLen, token) != addrLen)
-                {
-                    throw new ProtocolViolationException();
-                }
-
-                var assocIP = new IPAddress(addr);
-                if (await s.ReadAsync(buf, 0, 2, token) != 2)
-                {
-                    throw new ProtocolViolationException();
-                }
-
-                var assocPort = buf[0] * 256 + buf[1];
-
-                #endregion
-
-                _assocEndPoint = new IPEndPoint(assocIP, assocPort);
-            }
-            catch (ObjectDisposedException ex) when (ex.ObjectName == typeof(NetworkStream).FullName)
-            {
-                await DisconnectAsync();
-                throw new TimeoutException();
-            }
-            catch (Exception e)
-            {
-                Debug.WriteLine(e);
-                await DisconnectAsync();
-                throw;
-            }
-        }
-
-        public async Task<(byte[], IPEndPoint, IPAddress)> ReceiveAsync(byte[] bytes, IPEndPoint remote, EndPoint receive, CancellationToken token = default)
-        {
-            var state = _assoc.GetState();
-            if (state != TcpState.Established)
-            {
-                throw new InvalidOperationException("No UDP association, maybe already disconnected or not connected");
-            }
-
-            var remoteBytes = GetEndPointByte(remote);
-            var proxyBytes = new byte[bytes.Length + remoteBytes.Length + 3];
-            Array.Copy(remoteBytes, 0, proxyBytes, 3, remoteBytes.Length);
-            Array.Copy(bytes, 0, proxyBytes, remoteBytes.Length + 3, bytes.Length);
-
-            await _udpClient.SendAsync(proxyBytes, proxyBytes.Length, _assocEndPoint);
-            var res = new byte[ushort.MaxValue];
-            var flag = SocketFlags.None;
-            EndPoint ep = new IPEndPoint(0, 0);
-            var length = _udpClient.Client.ReceiveMessageFrom(res, 0, res.Length, ref flag, ref ep, out var ipPacketInformation);
-
-            if (res[0] != 0 || res[1] != 0 || res[2] != 0)
-            {
-                throw new Exception();
-            }
-
-            var addressLen = GetAddressLength(res[3]);
-
-            var ipByte = new byte[addressLen];
-            Array.Copy(res, 4, ipByte, 0, addressLen);
-
-            var ip = new IPAddress(ipByte);
-            var port = res[addressLen + 4] * 256 + res[addressLen + 5];
-            var ret = new byte[length - addressLen - 6];
-            Array.Copy(res, addressLen + 6, ret, 0, length - addressLen - 6);
-            return (
-                ret,
-                new IPEndPoint(ip, port),
-                ipPacketInformation.Address);
-        }
-
-        public Task DisconnectAsync()
-        {
-            try
-            {
-                _assoc.Close();
-            }
-            catch
-            {
-                // ignored
-            }
-
-            return Task.CompletedTask;
-        }
-
-        private static byte[] GetEndPointByte(IPEndPoint ep)
-        {
-            var ipByte = ep.Address.GetAddressBytes();
-            var ret = new byte[ipByte.Length + 3];
-            ret[0] = (byte)(ipByte.Length == 4 ? 1 : 4);
-            Array.Copy(ipByte, 0, ret, 1, ipByte.Length);
-            ret[ipByte.Length + 1] = (byte)(ep.Port / 256);
-            ret[ipByte.Length + 2] = (byte)(ep.Port % 256);
-            return ret;
-        }
-
-        private static int GetAddressLength(byte b)
-        {
-            return b switch
-            {
-                1 => 4,
-                4 => 16,
-                _ => throw new NotSupportedException()
-            };
-        }
-
-        public void Dispose()
-        {
-            _udpClient?.Dispose();
-            _assoc?.Dispose();
-        }
-    }
+	public class Socks5UdpProxy : IUdpProxy
+	{
+		private readonly TcpClient _assoc;
+		private readonly IPEndPoint _socksTcpEndPoint;
+
+		private IPEndPoint? _assocEndPoint;
+
+		public TimeSpan Timeout
+		{
+			get => TimeSpan.FromMilliseconds(_udpClient.Client.ReceiveTimeout);
+			set
+			{
+				var timeout = Convert.ToInt32(value.TotalMilliseconds);
+				_udpClient.Client.ReceiveTimeout = timeout;
+				_assoc.ReceiveTimeout = timeout;
+			}
+		}
+
+		public IPEndPoint LocalEndPoint => (IPEndPoint)_udpClient.Client.LocalEndPoint;
+
+		private readonly UdpClient _udpClient;
+
+		private readonly string? _user;
+		private readonly string? _password;
+
+		public Socks5UdpProxy(IPEndPoint? local, IPEndPoint proxy)
+		{
+			_udpClient = local is null ? new UdpClient() : new UdpClient(local);
+			_assoc = new TcpClient(proxy.AddressFamily);
+			_socksTcpEndPoint = proxy;
+		}
+
+		public Socks5UdpProxy(IPEndPoint? local, IPEndPoint proxy, string? user, string? password) : this(local, proxy)
+		{
+			_user = user;
+			_password = password;
+		}
+
+		public async Task ConnectAsync(CancellationToken token = default)
+		{
+			try
+			{
+				var buf = new byte[1024];
+				await _assoc.ConnectAsync(_socksTcpEndPoint.Address, _socksTcpEndPoint.Port);
+				var s = _assoc.GetStream();
+				using var _ = token.Register(() => s.Close());
+				var requestPasswordAuth = !string.IsNullOrEmpty(_user);
+
+				#region Handshake
+
+				// we have no GSS-API support
+				var handShake = requestPasswordAuth ? new byte[] { 5, 2, 0, 2 } : new byte[] { 5, 1, 0 };
+				await s.WriteAsync(handShake, 0, handShake.Length, token);
+
+				// 5 auth(ff=deny)
+				if (await s.ReadAsync(buf, 0, 2, token) != 2)
+				{
+					throw new ProtocolViolationException();
+				}
+
+				if (buf[0] != 5)
+				{
+					throw new ProtocolViolationException();
+				}
+
+				#endregion
+
+				#region Auth
+
+				var auth = buf[1];
+				switch (auth)
+				{
+					case 0:
+						break;
+					case 2:
+						var uByte = Encoding.UTF8.GetBytes(_user ?? string.Empty);
+						var pByte = Encoding.UTF8.GetBytes(_password ?? string.Empty);
+						buf[0] = 1;
+						buf[1] = (byte)uByte.Length;
+						Array.Copy(uByte, 0, buf, 2, uByte.Length);
+						buf[uByte.Length + 2] = (byte)pByte.Length;
+						Array.Copy(pByte, 0, buf, uByte.Length + 3, pByte.Length);
+						// 1 userLen user passLen pass
+						await s.WriteAsync(buf, 0, uByte.Length + pByte.Length + 3, token);
+						// 1 state(0=ok)
+						if (await s.ReadAsync(buf, 0, 2, token) != 2)
+						{
+							throw new ProtocolViolationException();
+						}
+
+						if (buf[0] != 1)
+						{
+							throw new ProtocolViolationException();
+						}
+
+						if (buf[1] != 0)
+						{
+							throw new UnauthorizedAccessException();
+						}
+
+						break;
+					case 0xff:
+						throw new UnauthorizedAccessException();
+					default:
+						throw new ProtocolViolationException();
+				}
+
+				#endregion
+
+				#region UDP Assoc Send
+
+				buf[0] = 5;
+				buf[1] = 3;
+				buf[2] = 0;
+
+				var abyte = GetEndPointByte(new IPEndPoint(IPAddress.Any, IPEndPoint.MinPort));
+				var addrLen = abyte.Length;
+				Array.Copy(abyte, 0, buf, 3, addrLen);
+				// 5 cmd(3=udpassoc) 0 atyp(1=v4 3=dns 4=v5) addr port
+				await s.WriteAsync(buf, 0, addrLen + 3, token);
+
+				#endregion
+
+				#region UDP Assoc Response
+
+				if (await s.ReadAsync(buf, 0, 4, token) != 4)
+				{
+					throw new ProtocolViolationException();
+				}
+
+				if (buf[0] != 5 || buf[2] != 0)
+				{
+					throw new ProtocolViolationException();
+				}
+
+				if (buf[1] != 0)
+				{
+					throw new UnauthorizedAccessException();
+				}
+
+				addrLen = GetAddressLength(buf[3]);
+
+				var addr = new byte[addrLen];
+				if (await s.ReadAsync(addr, 0, addrLen, token) != addrLen)
+				{
+					throw new ProtocolViolationException();
+				}
+
+				var assocIP = new IPAddress(addr);
+				if (await s.ReadAsync(buf, 0, 2, token) != 2)
+				{
+					throw new ProtocolViolationException();
+				}
+
+				var assocPort = buf[0] * 256 + buf[1];
+
+				#endregion
+
+				_assocEndPoint = new IPEndPoint(assocIP, assocPort);
+			}
+			catch (ObjectDisposedException ex) when (ex.ObjectName == typeof(NetworkStream).FullName)
+			{
+				await DisconnectAsync();
+				throw new TimeoutException();
+			}
+			catch (Exception e)
+			{
+				Debug.WriteLine(e);
+				await DisconnectAsync();
+				throw;
+			}
+		}
+
+		public async Task<(byte[], IPEndPoint, IPAddress)> ReceiveAsync(byte[] bytes, IPEndPoint remote, EndPoint receive, CancellationToken token = default)
+		{
+			var state = _assoc.GetState();
+			if (state != TcpState.Established)
+			{
+				throw new InvalidOperationException(@"No UDP association, maybe already disconnected or not connected");
+			}
+
+			var remoteBytes = GetEndPointByte(remote);
+			var proxyBytes = new byte[bytes.Length + remoteBytes.Length + 3];
+			Array.Copy(remoteBytes, 0, proxyBytes, 3, remoteBytes.Length);
+			Array.Copy(bytes, 0, proxyBytes, remoteBytes.Length + 3, bytes.Length);
+
+			await _udpClient.SendAsync(proxyBytes, proxyBytes.Length, _assocEndPoint);
+			var res = new byte[ushort.MaxValue];
+
+			var (local, length, _) = await _udpClient.Client.ReceiveMessageFromAsync(_assocEndPoint!, res, SocketFlags.None);
+
+			if (res[0] != 0 || res[1] != 0 || res[2] != 0)
+			{
+				throw new Exception();
+			}
+
+			var addressLen = GetAddressLength(res[3]);
+
+			var ipByte = new byte[addressLen];
+			Array.Copy(res, 4, ipByte, 0, addressLen);
+
+			var ip = new IPAddress(ipByte);
+			var port = res[addressLen + 4] * 256 + res[addressLen + 5];
+			var ret = new byte[length - addressLen - 6];
+			Array.Copy(res, addressLen + 6, ret, 0, length - addressLen - 6);
+			return (
+				ret,
+				new IPEndPoint(ip, port),
+				local);
+		}
+
+		public Task DisconnectAsync()
+		{
+			try
+			{
+				_assoc.Close();
+			}
+			catch
+			{
+				// ignored
+			}
+
+			return Task.CompletedTask;
+		}
+
+		private static byte[] GetEndPointByte(IPEndPoint ep)
+		{
+			var ipByte = ep.Address.GetAddressBytes();
+			var ret = new byte[ipByte.Length + 3];
+			ret[0] = ipByte.Length == 4 ? 1 : 4;
+			Array.Copy(ipByte, 0, ret, 1, ipByte.Length);
+			ret[ipByte.Length + 1] = (byte)(ep.Port / 256);
+			ret[ipByte.Length + 2] = (byte)(ep.Port % 256);
+			return ret;
+		}
+
+		private static int GetAddressLength(byte b)
+		{
+			return b switch
+			{
+				1 => 4,
+				4 => 16,
+				_ => throw new NotSupportedException()
+			};
+		}
+
+		public void Dispose()
+		{
+			_udpClient.Dispose();
+			_assoc.Dispose();
+		}
+	}
 }

+ 2 - 1
STUN/STUN.csproj

@@ -3,10 +3,11 @@
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
     <LangVersion>latest</LangVersion>
+    <Nullable>enable</Nullable>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="ReactiveUI" Version="11.5.17" />
+    <PackageReference Include="ReactiveUI.Fody" Version="13.0.38" />
   </ItemGroup>
 
 </Project>

+ 6 - 19
STUN/StunResult/ClassicStunResult.cs

@@ -1,24 +1,11 @@
-using System.Net;
-using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
 using STUN.Enums;
 
 namespace STUN.StunResult
 {
-    public class ClassicStunResult : ReactiveObject
-    {
-        private NatType _natType = NatType.Unknown;
-        private IPEndPoint _publicEndPoint;
-
-        public NatType NatType
-        {
-            get => _natType;
-            set => this.RaiseAndSetIfChanged(ref _natType, value);
-        }
-
-        public IPEndPoint PublicEndPoint
-        {
-            get => _publicEndPoint;
-            set => this.RaiseAndSetIfChanged(ref _publicEndPoint, value);
-        }
-    }
+	public class ClassicStunResult : StunResult
+	{
+		[Reactive]
+		public NatType NatType { get; set; } = NatType.Unknown;
+	}
 }

+ 15 - 0
STUN/StunResult/StunResult.cs

@@ -0,0 +1,15 @@
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using System.Net;
+
+namespace STUN.StunResult
+{
+	public abstract class StunResult : ReactiveObject
+	{
+		[Reactive]
+		public IPEndPoint? PublicEndPoint { get; set; }
+
+		[Reactive]
+		public IPEndPoint? LocalEndPoint { get; set; }
+	}
+}

+ 36 - 11
STUN/StunResult/StunResult5389.cs

@@ -1,16 +1,41 @@
-using STUN.Enums;
+using ReactiveUI.Fody.Helpers;
+using STUN.Enums;
 using System.Net;
-using ReactiveUI;
 
 namespace STUN.StunResult
 {
-    public class StunResult5389 : ReactiveObject
-    {
-        public IPEndPoint PublicEndPoint { get; set; }
-        public IPEndPoint LocalEndPoint { get; set; }
-        public IPEndPoint OtherEndPoint { get; set; }
-        public BindingTestResult BindingTestResult { get; set; } = BindingTestResult.Unknown;
-        public MappingBehavior MappingBehavior { get; set; } = MappingBehavior.Unknown;
-        public FilteringBehavior FilteringBehavior { get; set; } = FilteringBehavior.Unknown;
-    }
+	public class StunResult5389 : StunResult
+	{
+		[Reactive]
+		public IPEndPoint? OtherEndPoint { get; set; }
+
+		[Reactive]
+		public BindingTestResult BindingTestResult { get; set; } = BindingTestResult.Unknown;
+
+		[Reactive]
+		public MappingBehavior MappingBehavior { get; set; } = MappingBehavior.Unknown;
+
+		[Reactive]
+		public FilteringBehavior FilteringBehavior { get; set; } = FilteringBehavior.Unknown;
+
+		public void Clone(StunResult5389 result)
+		{
+			PublicEndPoint = result.PublicEndPoint;
+			LocalEndPoint = result.LocalEndPoint;
+			OtherEndPoint = result.OtherEndPoint;
+			BindingTestResult = result.BindingTestResult;
+			MappingBehavior = result.MappingBehavior;
+			FilteringBehavior = result.FilteringBehavior;
+		}
+
+		public void Reset()
+		{
+			PublicEndPoint = default;
+			LocalEndPoint = default;
+			OtherEndPoint = default;
+			BindingTestResult = BindingTestResult.Unknown;
+			MappingBehavior = MappingBehavior.Unknown;
+			FilteringBehavior = FilteringBehavior.Unknown;
+		}
+	}
 }

+ 69 - 55
STUN/Utils/AttributeExtensions.cs

@@ -1,4 +1,4 @@
-using STUN.Enums;
+using STUN.Enums;
 using STUN.Message;
 using STUN.Message.Attributes;
 using System.Linq;
@@ -6,58 +6,72 @@ using System.Net;
 
 namespace STUN.Utils
 {
-    public static class AttributeExtensions
-    {
-        public static Attribute BuildChangeRequest(bool changeIp, bool changePort)
-        {
-            return new Attribute
-            {
-                Type = AttributeType.ChangeRequest,
-                Length = 4,
-                Value = new ChangeRequestAttribute { ChangeIp = changeIp, ChangePort = changePort }
-            };
-        }
-
-        public static IPEndPoint GetMappedAddressAttribute(StunMessage5389 response)
-        {
-            var mappedAddressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
-
-            if (mappedAddressAttribute == null) return null;
-
-            var mapped = (MappedAddressAttribute)mappedAddressAttribute.Value;
-            return new IPEndPoint(mapped.Address, mapped.Port);
-        }
-
-        public static IPEndPoint GetChangedAddressAttribute(StunMessage5389 response)
-        {
-            var changedAddressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
-
-            if (changedAddressAttribute == null) return null;
-
-            var address = (ChangedAddressAttribute)changedAddressAttribute.Value;
-            return new IPEndPoint(address.Address, address.Port);
-        }
-
-        public static IPEndPoint GetXorMappedAddressAttribute(StunMessage5389 response)
-        {
-            var mappedAddressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.XorMappedAddress) ??
-                                         response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
-
-            if (mappedAddressAttribute == null) return null;
-
-            var mapped = (AddressAttribute)mappedAddressAttribute.Value;
-            return new IPEndPoint(mapped.Address, mapped.Port);
-        }
-
-        public static IPEndPoint GetOtherAddressAttribute(StunMessage5389 response)
-        {
-            var addressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.OtherAddress)
-                                   ?? response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
-
-            if (addressAttribute == null) return null;
-
-            var address = (AddressAttribute)addressAttribute.Value;
-            return new IPEndPoint(address.Address, address.Port);
-        }
-    }
+	public static class AttributeExtensions
+	{
+		public static Attribute BuildChangeRequest(bool changeIp, bool changePort)
+		{
+			return new()
+			{
+				Type = AttributeType.ChangeRequest,
+				Length = 4,
+				Value = new ChangeRequestAttribute { ChangeIp = changeIp, ChangePort = changePort }
+			};
+		}
+
+		public static IPEndPoint? GetMappedAddressAttribute(this StunMessage5389? response)
+		{
+			var mappedAddressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.MappedAddress);
+
+			if (mappedAddressAttribute is null)
+			{
+				return null;
+			}
+
+			var mapped = (MappedAddressAttribute)mappedAddressAttribute.Value;
+			return new IPEndPoint(mapped.Address!, mapped.Port);
+		}
+
+		public static IPEndPoint? GetChangedAddressAttribute(this StunMessage5389? response)
+		{
+			var changedAddressAttribute = response?.Attributes.FirstOrDefault(t => t.Type == AttributeType.ChangedAddress);
+
+			if (changedAddressAttribute is null)
+			{
+				return null;
+			}
+
+			var address = (ChangedAddressAttribute)changedAddressAttribute.Value;
+			return new IPEndPoint(address.Address!, address.Port);
+		}
+
+		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);
+
+			if (mappedAddressAttribute is null)
+			{
+				return null;
+			}
+
+			var mapped = (AddressAttribute)mappedAddressAttribute.Value;
+			return new IPEndPoint(mapped.Address!, mapped.Port);
+		}
+
+		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);
+
+			if (addressAttribute is null)
+			{
+				return null;
+			}
+
+			var address = (AddressAttribute)addressAttribute.Value;
+			return new IPEndPoint(address.Address!, address.Port);
+		}
+	}
 }

+ 38 - 37
STUN/Utils/BitUtils.cs

@@ -1,50 +1,51 @@
-using System;
+using DynamicData.Kernel;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Security.Cryptography;
 
 namespace STUN.Utils
 {
-    public static class BitUtils
-    {
-        public static IEnumerable<byte> ToBe(this int num)
-        {
-            var res = BitConverter.GetBytes(num);
-            return BitConverter.IsLittleEndian ? res.Reverse() : res;
-        }
+	public static class BitUtils
+	{
+		public static IEnumerable<byte> ToBe(this int num)
+		{
+			var res = BitConverter.GetBytes(num);
+			return BitConverter.IsLittleEndian ? res.Reverse() : res;
+		}
 
-        public static IEnumerable<byte> ToBe(this ushort num)
-        {
-            var res = BitConverter.GetBytes(num);
-            return BitConverter.IsLittleEndian ? res.Reverse() : res;
-        }
+		public static IEnumerable<byte> ToBe(this ushort num)
+		{
+			var res = BitConverter.GetBytes(num);
+			return BitConverter.IsLittleEndian ? res.Reverse() : res;
+		}
 
-        public static ushort FromBe(byte b1, byte b2)
-        {
-            return BitConverter.ToUInt16(BitConverter.IsLittleEndian ? new[] { b2, b1 } : new[] { b1, b2 }, 0);
-        }
+		public static ushort FromBe(byte b1, byte b2)
+		{
+			return BitConverter.ToUInt16(BitConverter.IsLittleEndian ? new[] { b2, b1 } : new[] { b1, b2 }, 0);
+		}
 
-        public static ushort FromBe(IEnumerable<byte> b)
-        {
-            return BitConverter.ToUInt16(BitConverter.IsLittleEndian ? b.Reverse().ToArray() : b.ToArray(), 0);
-        }
+		public static ushort FromBe(IEnumerable<byte> b)
+		{
+			return BitConverter.ToUInt16(BitConverter.IsLittleEndian ? b.Reverse().AsArray() : b.AsArray(), 0);
+		}
 
-        public static int FromBeToInt(IEnumerable<byte> b)
-        {
-            return BitConverter.ToInt32(BitConverter.IsLittleEndian ? b.Reverse().ToArray() : b.ToArray(), 0);
-        }
+		public static int FromBeToInt(IEnumerable<byte> b)
+		{
+			return BitConverter.ToInt32(BitConverter.IsLittleEndian ? b.Reverse().AsArray() : b.AsArray(), 0);
+		}
 
-        public static IEnumerable<byte> GetRandomBytes(int n)
-        {
-            var temp = new byte[n];
-            using var rng = new RNGCryptoServiceProvider();
-            rng.GetBytes(temp);
-            return temp;
-        }
+		public static IEnumerable<byte> GetRandomBytes(int n)
+		{
+			var temp = new byte[n];
+			using var rng = new RNGCryptoServiceProvider();
+			rng.GetBytes(temp);
+			return temp;
+		}
 
-        public static bool IsEqual(this IEnumerable<byte> a, IEnumerable<byte> b)
-        {
-            return a != null && b != null && a.SequenceEqual(b);
-        }
-    }
+		public static bool IsEqual(this IEnumerable<byte>? a, IEnumerable<byte>? b)
+		{
+			return a != null && b != null && a.SequenceEqual(b);
+		}
+	}
 }

+ 74 - 50
STUN/Utils/NetUtils.cs

@@ -1,5 +1,5 @@
-using STUN.Client;
-using STUN.StunResult;
+using System;
+using System.Diagnostics;
 using System.Linq;
 using System.Net;
 using System.Net.NetworkInformation;
@@ -8,52 +8,76 @@ using System.Threading.Tasks;
 
 namespace STUN.Utils
 {
-    public static class NetUtils
-    {
-        public const string DefaultLocalEnd = @"0.0.0.0:0";
-
-        public static IPEndPoint ParseEndpoint(string str)
-        {
-            var ipPort = str.Trim().Split(':');
-            if (ipPort.Length < 2) return null;
-            IPAddress ip = null;
-            if (ipPort.Length == 2 && IPAddress.TryParse(ipPort[0], out ip))
-            {
-                if (!IPAddress.TryParse(ipPort[0], out ip))
-                {
-                    return null;
-                }
-            }
-            else if (ipPort.Length > 2)
-            {
-                var ipStr = string.Join(@":", ipPort, 0, ipPort.Length - 1);
-                if (!ipStr.StartsWith(@"[") || !ipStr.EndsWith(@"]") || !IPAddress.TryParse(ipStr, out ip))
-                {
-                    return null;
-                }
-            }
-
-            if (ip != null && ushort.TryParse(ipPort.Last(), out var port))
-            {
-                return new IPEndPoint(ip, port);
-            }
-
-            return null;
-        }
-
-        public static async Task<StunResult5389> NatBehaviorDiscovery(string server, ushort port, IPEndPoint local)
-        {
-            // proxy is not supported yet
-            using var client = new StunClient5389UDP(server, port, local);
-            return await client.QueryAsync();
-        }
-
-        public static TcpState GetState(this TcpClient tcpClient)
-        {
-            var foo = IPGlobalProperties.GetIPGlobalProperties()
-              .GetActiveTcpConnections()
-              .SingleOrDefault(x => x.LocalEndPoint.Equals(tcpClient.Client.LocalEndPoint));
-            return foo?.State ?? TcpState.Unknown;
-        }
-    }
+	public static class NetUtils
+	{
+		public static IPEndPoint? ParseEndpoint(string? str)
+		{
+			if (str is null)
+			{
+				return null;
+			}
+
+			var ipPort = str.Trim().Split(':');
+			if (ipPort.Length < 2)
+			{
+				return null;
+			}
+
+			IPAddress? ip = null;
+			if (ipPort.Length == 2 && IPAddress.TryParse(ipPort[0], out ip))
+			{
+				if (!IPAddress.TryParse(ipPort[0], out ip))
+				{
+					return null;
+				}
+			}
+			else if (ipPort.Length > 2)
+			{
+				var ipStr = string.Join(@":", ipPort, 0, ipPort.Length - 1);
+				if (!ipStr.StartsWith(@"[") || !ipStr.EndsWith(@"]") || !IPAddress.TryParse(ipStr, out ip))
+				{
+					return null;
+				}
+			}
+
+			if (ip != null && ushort.TryParse(ipPort.Last(), out var port))
+			{
+				return new IPEndPoint(ip, port);
+			}
+
+			return null;
+		}
+
+		public static TcpState GetState(this TcpClient tcpClient)
+		{
+			var foo = IPGlobalProperties
+				.GetIPGlobalProperties()
+				.GetActiveTcpConnections()
+				.SingleOrDefault(x => x.LocalEndPoint.Equals(tcpClient.Client.LocalEndPoint));
+			return foo?.State ?? TcpState.Unknown;
+		}
+
+		public static async Task<(IPAddress, int, IPEndPoint)> ReceiveMessageFromAsync(this Socket client, EndPoint receive, byte[] array, SocketFlags flag)
+		{
+			var tcs = new TaskCompletionSource<(IPAddress, int, IPEndPoint)>(TaskCreationOptions.RunContinuationsAsynchronously);
+			_ = Task.Run(() =>
+			{
+				try
+				{
+					var length = client.ReceiveMessageFrom(array, 0, array.Length, ref flag, ref receive, out var ipPacketInformation);
+
+					var local = ipPacketInformation.Address;
+
+					Debug.WriteLine($@"{(IPEndPoint)receive} => {local} {length} 字节");
+					tcs.SetResult((local, length, (IPEndPoint)receive));
+				}
+				catch (Exception ex)
+				{
+					tcs.SetException(ex);
+				}
+			});
+
+			return await tcs.Task;
+		}
+	}
 }

+ 97 - 0
STUN/Utils/StunServer.cs

@@ -0,0 +1,97 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+
+namespace STUN.Utils
+{
+	public class StunServer
+	{
+		public string Hostname { get; set; }
+		public ushort Port { get; set; }
+
+		public StunServer()
+		{
+			Hostname = @"stun.syncthing.net";
+			Port = 3478;
+		}
+
+		public bool Parse(string str)
+		{
+			var ipPort = str.Trim().Split(':', ':');
+			switch (ipPort.Length)
+			{
+				case 0:
+					return false;
+				case 1:
+				{
+					var host = ipPort[0].Trim();
+					if (Uri.CheckHostName(host) != UriHostNameType.Dns && !IPAddress.TryParse(host, out _))
+					{
+						return false;
+					}
+					Hostname = host;
+					Port = 3478;
+					return true;
+				}
+				case 2:
+				{
+					var host = ipPort[0].Trim();
+					if (Uri.CheckHostName(host) != UriHostNameType.Dns && !IPAddress.TryParse(host, out _))
+					{
+						return false;
+					}
+					if (ushort.TryParse(ipPort[1], out var port))
+					{
+						Hostname = host;
+						Port = port;
+						return true;
+					}
+					break;
+				}
+				default:
+				{
+					if (IPAddress.TryParse(str.Trim(), out var ipv6))
+					{
+						Hostname = $@"{ipv6}";
+						Port = ushort.TryParse(ipPort.Last(), out var portV6) ? portV6 : (ushort)3478;
+						return true;
+					}
+
+					var ipStr = string.Join(@":", ipPort, 0, ipPort.Length - 1);
+					if (!ipStr.StartsWith(@"[") || !ipStr.EndsWith(@"]") || !IPAddress.TryParse(ipStr, out _))
+					{
+						return false;
+					}
+
+					if (ushort.TryParse(ipPort.Last(), out var port))
+					{
+						Port = port;
+						return true;
+					}
+
+					break;
+				}
+			}
+
+			return false;
+		}
+
+		public override string ToString()
+		{
+			if (string.IsNullOrEmpty(Hostname))
+			{
+				return string.Empty;
+			}
+			if (Port == 3478)
+			{
+				return Hostname;
+			}
+			if (IPAddress.TryParse(Hostname, out var ip) && ip.AddressFamily != AddressFamily.InterNetwork)
+			{
+				return $@"[{Hostname}]:{Port}";
+			}
+			return $@"{Hostname}:{Port}";
+		}
+	}
+}

+ 192 - 174
UnitTest/UnitTest.cs

@@ -1,4 +1,4 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
 using STUN.Client;
 using STUN.Enums;
 using STUN.Message.Attributes;
@@ -11,177 +11,195 @@ using System.Threading.Tasks;
 
 namespace UnitTest
 {
-    [TestClass]
-    public class UnitTest
-    {
-        private readonly byte[] _magicCookie = { 0x21, 0x12, 0xa4, 0x42 };
-        private readonly byte[] _transactionId =
-        {
-                0xb7, 0xe7, 0xa7, 0x01,
-                0xbc, 0x34, 0xd6, 0x86,
-                0xfa, 0x87, 0xdf, 0xae
-        };
-
-        private static readonly byte[] XorPort = { 0xa1, 0x47 };
-        private static readonly byte[] XorIPv4 = { 0xe1, 0x12, 0xa6, 0x43 };
-        private static readonly byte[] XorIPv6 =
-        {
-                0x01, 0x13, 0xa9, 0xfa,
-                0xa5, 0xd3, 0xf1, 0x79,
-                0xbc, 0x25, 0xf4, 0xb5,
-                0xbe, 0xd2, 0xb9, 0xd9
-        };
-
-        private const ushort Port = 32853;
-        private readonly IPAddress IPv4 = IPAddress.Parse(@"192.0.2.1");
-        private readonly IPAddress IPv6 = IPAddress.Parse(@"2001:db8:1234:5678:11:2233:4455:6677");
-
-        private readonly byte[] _ipv4Response = new byte[] { 0x00, (byte)IpFamily.IPv4 }.Concat(XorPort).Concat(XorIPv4).ToArray();
-        private readonly byte[] _ipv6Response = new byte[] { 0x00, (byte)IpFamily.IPv6 }.Concat(XorPort).Concat(XorIPv6).ToArray();
-
-        /// <summary>
-        /// https://tools.ietf.org/html/rfc5769.html
-        /// </summary>
-        [TestMethod]
-        public void TestXorMapped()
-        {
-            var t = new XorMappedAddressAttribute(_magicCookie, _transactionId)
-            {
-                Port = Port,
-                Family = IpFamily.IPv4,
-                Address = IPv4
-            };
-            Assert.IsTrue(_ipv4Response.SequenceEqual(t.Bytes));
-
-            t = new XorMappedAddressAttribute(_magicCookie, _transactionId);
-            Assert.IsTrue(t.TryParse(_ipv4Response));
-            Assert.AreEqual(t.Port, Port);
-            Assert.AreEqual(t.Family, IpFamily.IPv4);
-            Assert.AreEqual(t.Address, IPv4);
-
-            t = new XorMappedAddressAttribute(_magicCookie, _transactionId);
-            Assert.IsTrue(t.TryParse(_ipv6Response));
-            Assert.AreEqual(t.Port, Port);
-            Assert.AreEqual(t.Family, IpFamily.IPv6);
-            Assert.AreEqual(t.Address, IPv6);
-
-            Assert.IsTrue(_ipv6Response.SequenceEqual(t.Bytes));
-        }
-
-        [TestMethod]
-        public void ParseEndpointTest()
-        {
-            Assert.IsNull(NetUtils.ParseEndpoint(@"1.2.3.4"));
-            Assert.IsNull(NetUtils.ParseEndpoint(@"1.2.256.5:80"));
-            Assert.AreEqual(NetUtils.ParseEndpoint(@"0.0.0.0:123"), IPEndPoint.Parse(@"0.0.0.0:123"));
-            Assert.AreEqual(NetUtils.ParseEndpoint(@"192.168.1.1:2136"), IPEndPoint.Parse(@"192.168.1.1:2136"));
-            Assert.AreEqual(NetUtils.ParseEndpoint(@"[2001:db8:1234:5678:11:2233:4455:6677]:32853"), IPEndPoint.Parse(@"[2001:db8:1234:5678:11:2233:4455:6677]:32853"));
-            Assert.IsNull(NetUtils.ParseEndpoint(@"2001:db8:1234:5678:11:2233:4455:6677:32853"));
-            Assert.IsNull(NetUtils.ParseEndpoint(@"2001:db8:1234:5678:11:2233:4455:6677"));
-            Assert.AreEqual(NetUtils.ParseEndpoint(@"[2001:db8:1234:5678:11:2233:4455:6677]:0"), IPEndPoint.Parse(@"[2001:db8:1234:5678:11:2233:4455:6677]:0"));
-            Assert.AreEqual(NetUtils.ParseEndpoint(@"[::1]:0"), IPEndPoint.Parse(@"[::1]:0"));
-        }
-
-        [TestMethod]
-        public async Task BindingTest()
-        {
-            using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-            var result = await client.BindingTestAsync();
-
-            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()
-        {
-            using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-            var result = await client.MappingBehaviorTestAsync();
-
-            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 == MappingBehavior.Direct
-            || result.MappingBehavior == MappingBehavior.EndpointIndependent
-            || result.MappingBehavior == MappingBehavior.AddressDependent
-            || result.MappingBehavior == MappingBehavior.AddressAndPortDependent
-            );
-            Assert.AreEqual(result.FilteringBehavior, FilteringBehavior.Unknown);
-        }
-
-        [TestMethod]
-        public async Task FilteringBehaviorTest()
-        {
-            using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-            var result = await client.FilteringBehaviorTestAsync();
-
-            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 == FilteringBehavior.EndpointIndependent
-            || result.FilteringBehavior == FilteringBehavior.AddressDependent
-            || result.FilteringBehavior == FilteringBehavior.AddressAndPortDependent
-            );
-        }
-
-        [TestMethod]
-        public async Task CombiningTest()
-        {
-            using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-            var result = await client.QueryAsync();
-
-            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 == MappingBehavior.Direct
-                          || result.MappingBehavior == MappingBehavior.EndpointIndependent
-                          || result.MappingBehavior == MappingBehavior.AddressDependent
-                          || result.MappingBehavior == MappingBehavior.AddressAndPortDependent
-            );
-            Assert.IsTrue(result.FilteringBehavior == FilteringBehavior.EndpointIndependent
-                          || result.FilteringBehavior == FilteringBehavior.AddressDependent
-                          || result.FilteringBehavior == 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"));
-            using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0), proxy);
-            var result = await client.QueryAsync();
-
-            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 == MappingBehavior.Direct ||
-                          result.MappingBehavior == MappingBehavior.EndpointIndependent ||
-                          result.MappingBehavior == MappingBehavior.AddressDependent ||
-                          result.MappingBehavior == MappingBehavior.AddressAndPortDependent);
-            Assert.IsTrue(result.FilteringBehavior == FilteringBehavior.EndpointIndependent ||
-                          result.FilteringBehavior == FilteringBehavior.AddressDependent ||
-                          result.FilteringBehavior == 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);
-        }
-    }
+	[TestClass]
+	public class UnitTest
+	{
+		private readonly byte[] _magicCookie = { 0x21, 0x12, 0xa4, 0x42 };
+		private readonly byte[] _transactionId =
+		{
+				0xb7, 0xe7, 0xa7, 0x01,
+				0xbc, 0x34, 0xd6, 0x86,
+				0xfa, 0x87, 0xdf, 0xae
+		};
+
+		private static readonly byte[] XorPort = { 0xa1, 0x47 };
+		private static readonly byte[] XorIPv4 = { 0xe1, 0x12, 0xa6, 0x43 };
+		private static readonly byte[] XorIPv6 =
+		{
+				0x01, 0x13, 0xa9, 0xfa,
+				0xa5, 0xd3, 0xf1, 0x79,
+				0xbc, 0x25, 0xf4, 0xb5,
+				0xbe, 0xd2, 0xb9, 0xd9
+		};
+
+		private const ushort Port = 32853;
+		private readonly IPAddress IPv4 = IPAddress.Parse(@"192.0.2.1");
+		private readonly IPAddress IPv6 = IPAddress.Parse(@"2001:db8:1234:5678:11:2233:4455:6677");
+
+		private readonly byte[] _ipv4Response = new byte[] { 0x00, (byte)IpFamily.IPv4 }.Concat(XorPort).Concat(XorIPv4).ToArray();
+		private readonly byte[] _ipv6Response = new byte[] { 0x00, (byte)IpFamily.IPv6 }.Concat(XorPort).Concat(XorIPv6).ToArray();
+
+		/// <summary>
+		/// https://tools.ietf.org/html/rfc5769.html
+		/// </summary>
+		[TestMethod]
+		public void TestXorMapped()
+		{
+			var t = new XorMappedAddressAttribute(_magicCookie, _transactionId)
+			{
+				Port = Port,
+				Family = IpFamily.IPv4,
+				Address = IPv4
+			};
+			Assert.IsTrue(_ipv4Response.SequenceEqual(t.Bytes));
+
+			t = new XorMappedAddressAttribute(_magicCookie, _transactionId);
+			Assert.IsTrue(t.TryParse(_ipv4Response));
+			Assert.AreEqual(t.Port, Port);
+			Assert.AreEqual(t.Family, IpFamily.IPv4);
+			Assert.AreEqual(t.Address, IPv4);
+
+			t = new XorMappedAddressAttribute(_magicCookie, _transactionId);
+			Assert.IsTrue(t.TryParse(_ipv6Response));
+			Assert.AreEqual(t.Port, Port);
+			Assert.AreEqual(t.Family, IpFamily.IPv6);
+			Assert.AreEqual(t.Address, IPv6);
+
+			Assert.IsTrue(_ipv6Response.SequenceEqual(t.Bytes));
+		}
+
+		[TestMethod]
+		[DataRow(@"1.2.3.4")]
+		[DataRow(@"1.2.256.5:80")]
+		[DataRow(@"2001:db8:1234:5678:11:2233:4455:6677:32853")]
+		[DataRow(@"2001:db8:1234:5678:11:2233:4455:6677")]
+		public void ParseEndpointTestFail(string ipStr)
+		{
+			Assert.IsNull(NetUtils.ParseEndpoint(ipStr));
+		}
+
+		[TestMethod]
+		[DataRow(@"0.0.0.0:123")]
+		[DataRow(@"192.168.1.1:2136")]
+		[DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]:32853")]
+		[DataRow(@"[2001:db8:1234:5678:11:2233:4455:6677]:0")]
+		[DataRow(@"[::1]:0")]
+		public void ParseEndpointTestSuccess(string ipStr)
+		{
+			Assert.AreEqual(NetUtils.ParseEndpoint(ipStr), IPEndPoint.Parse(ipStr));
+		}
+
+		[TestMethod]
+		public async Task BindingTest()
+		{
+			using var client = new StunClient5389UDP(@"stun.syncthing.net", 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()
+		{
+			using var client = new StunClient5389UDP(@"stun.syncthing.net", 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()
+		{
+			using var client = new StunClient5389UDP(@"stun.syncthing.net", 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()
+		{
+			using var client = new StunClient5389UDP(@"stun.syncthing.net", 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"));
+			using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0), proxy);
+			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);
+		}
+	}
 }

+ 7 - 11
UnitTest/UnitTest.csproj

@@ -1,21 +1,17 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
-
+    <TargetFramework>net5.0</TargetFramework>
+    <LangVersion>latest</LangVersion>
+    <Nullable>enable</Nullable>
     <IsPackable>false</IsPackable>
   </PropertyGroup>
 
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
-    <PlatformTarget>AnyCPU</PlatformTarget>
-    <Prefer32Bit>false</Prefer32Bit>
-  </PropertyGroup>
-
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
-    <PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
-    <PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
-    <PackageReference Include="coverlet.collector" Version="1.2.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+    <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
+    <PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
+    <PackageReference Include="coverlet.collector" Version="3.0.0" />
   </ItemGroup>
 
   <ItemGroup>