Bruce Wayne 4 ani în urmă
părinte
comite
51565c67cb

+ 3 - 1
NatTypeTester-Console/Program.cs

@@ -30,7 +30,9 @@ namespace NatTypeTester
 			}
 
 			using var client = new StunClient5389UDP(server, port, local);
-			var res = await client.QueryAsync();
+			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}");

+ 1 - 0
NatTypeTester/FodyWeavers.xml

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

+ 1 - 1
NatTypeTester/MainWindow.xaml

@@ -122,7 +122,7 @@
 								x:Name="ProxyUsernameTextBox" Grid.Row="1"
 								Margin="0,5" IsReadOnly="False"
 								VerticalContentAlignment="Center" VerticalAlignment="Center"
-								ui:ControlHelper.Header="User ID" />
+								ui:ControlHelper.Header="Username" />
 							<TextBox 
 								x:Name="ProxyPasswordTextBox" Grid.Row="2"
 								Margin="0,5"

+ 31 - 27
NatTypeTester/MainWindow.xaml.cs

@@ -2,8 +2,8 @@ using ModernWpf;
 using NatTypeTester.ViewModels;
 using ReactiveUI;
 using STUN.Enums;
+using STUN.Utils;
 using System;
-using System.Reactive;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
 using System.Windows;
@@ -71,28 +71,29 @@ namespace NatTypeTester
 				#region RFC3489
 
 				this.OneWayBind(ViewModel,
-						vm => vm.ClassicNatType,
-						v => v.NatTypeTextBox.Text
+						vm => vm.Result3489.NatType,
+						v => v.NatTypeTextBox.Text,
+						type => type.ToString()
 				).DisposeWith(d);
 
 				this.Bind(ViewModel,
-						vm => vm.LocalEnd,
-						v => v.LocalEndTextBox.Text
+						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.PublicEnd,
-						v => v.PublicEndTextBox.Text
+						vm => vm.Result3489.PublicEndPoint,
+						v => v.PublicEndTextBox.Text,
+						ipe => ipe is null ? string.Empty : ipe.ToString()
 				).DisposeWith(d);
 
-				this.BindCommand(ViewModel,
-								viewModel => viewModel.TestClassicNatType,
-								view => view.TestButton)
-						.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); })
+						.Subscribe(async _ => await ViewModel.TestClassicNatType.Execute(default))
 						.DisposeWith(d);
 
 				#endregion
@@ -100,38 +101,41 @@ namespace NatTypeTester
 				#region RFC5780
 
 				this.OneWayBind(ViewModel,
-						vm => vm.BindingTest,
-						v => v.BindingTestTextBox.Text
+						vm => vm.Result5389.BindingTestResult,
+						v => v.BindingTestTextBox.Text,
+						res => res.ToString()
 				).DisposeWith(d);
 
 				this.OneWayBind(ViewModel,
-						vm => vm.MappingBehavior,
-						v => v.MappingBehaviorTextBox.Text
+						vm => vm.Result5389.MappingBehavior,
+						v => v.MappingBehaviorTextBox.Text,
+						res => res.ToString()
 				).DisposeWith(d);
 
 				this.OneWayBind(ViewModel,
-						vm => vm.FilteringBehavior,
-						v => v.FilteringBehaviorTextBox.Text
+						vm => vm.Result5389.FilteringBehavior,
+						v => v.FilteringBehaviorTextBox.Text,
+						res => res.ToString()
 				).DisposeWith(d);
 
 				this.Bind(ViewModel,
-						vm => vm.LocalAddress,
-						v => v.LocalAddressTextBox.Text
+						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.MappingAddress,
-						v => v.MappingAddressTextBox.Text
+						vm => vm.Result5389.PublicEndPoint,
+						v => v.MappingAddressTextBox.Text,
+						ipe => ipe is null ? string.Empty : ipe.ToString()
 				).DisposeWith(d);
 
-				this.BindCommand(ViewModel,
-								viewModel => viewModel.DiscoveryNatType,
-								view => view.DiscoveryButton)
-						.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); })
+						.Subscribe(async _ => await ViewModel.DiscoveryNatType.Execute(default))
 						.DisposeWith(d);
 
 				#endregion

+ 1 - 1
NatTypeTester/NatTypeTester.csproj

@@ -23,9 +23,9 @@
     <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.3" />
     <PackageReference Include="ReactiveUI.Events.WPF" Version="13.0.27" />
+    <PackageReference Include="ReactiveUI.Fody" Version="13.0.27" />
     <PackageReference Include="ReactiveUI.WPF" Version="13.0.27" />
   </ItemGroup>
 

+ 63 - 94
NatTypeTester/ViewModels/MainWindowViewModel.cs

@@ -6,13 +6,17 @@ using ReactiveUI.Fody.Helpers;
 using STUN.Client;
 using STUN.Enums;
 using STUN.Proxy;
+using STUN.StunResult;
 using STUN.Utils;
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Net;
 using System.Reactive;
 using System.Reactive.Linq;
+using System.Threading;
+using System.Threading.Tasks;
 using System.Windows;
 
 namespace NatTypeTester.ViewModels
@@ -22,13 +26,7 @@ namespace NatTypeTester.ViewModels
 		#region RFC3489
 
 		[Reactive]
-		public string? ClassicNatType { get; set; }
-
-		[Reactive]
-		public string LocalEnd { get; set; } = NetUtils.DefaultLocalEnd;
-
-		[Reactive]
-		public string? PublicEnd { get; set; }
+		public ClassicStunResult Result3489 { get; set; }
 
 		public ReactiveCommand<Unit, Unit> TestClassicNatType { get; }
 
@@ -37,19 +35,7 @@ namespace NatTypeTester.ViewModels
 		#region RFC5780
 
 		[Reactive]
-		public string? BindingTest { get; set; }
-
-		[Reactive]
-		public string? MappingBehavior { get; set; }
-
-		[Reactive]
-		public string? FilteringBehavior { get; set; }
-
-		[Reactive]
-		public string? LocalAddress { get; set; }
-
-		[Reactive]
-		public string? MappingAddress { get; set; }
+		public StunResult5389 Result5389 { get; set; }
 
 		public ReactiveCommand<Unit, Unit> DiscoveryNatType { get; }
 
@@ -92,14 +78,23 @@ namespace NatTypeTester.ViewModels
 
 		public MainWindowViewModel()
 		{
+			Result3489 = new ClassicStunResult
+			{
+				LocalEndPoint = new IPEndPoint(IPAddress.Any, 0)
+			};
+			Result5389 = new StunResult5389
+			{
+				LocalEndPoint = new IPEndPoint(IPAddress.Any, 0)
+			};
+
 			LoadStunServer();
 			List.Connect()
 				.DistinctValues(x => x)
 				.ObserveOnDispatcher()
 				.Bind(StunServers)
 				.Subscribe();
-			TestClassicNatType = ReactiveCommand.CreateFromObservable(TestClassicNatTypeImpl);
-			DiscoveryNatType = ReactiveCommand.CreateFromObservable(DiscoveryNatTypeImpl);
+			TestClassicNatType = ReactiveCommand.CreateFromTask(TestClassicNatTypeImpl);
+			DiscoveryNatType = ReactiveCommand.CreateFromTask(DiscoveryNatTypeImpl);
 		}
 
 		private async void LoadStunServer()
@@ -129,92 +124,66 @@ namespace NatTypeTester.ViewModels
 			}
 		}
 
-		private IObservable<Unit> TestClassicNatTypeImpl()
+		private async Task TestClassicNatTypeImpl(CancellationToken token)
 		{
-			return Observable.FromAsync(async () =>
+			try
 			{
-				try
+				var server = new StunServer();
+				if (server.Parse(StunServer))
 				{
-					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!");
-					}
+					using var proxy = ProxyFactory.CreateProxy(
+						ProxyType,
+						Result3489.LocalEndPoint,
+						NetUtils.ParseEndpoint(ProxyServer),
+						ProxyUser, ProxyPassword
+						);
+
+					using var client = new StunClient3489(server.Hostname, server.Port, Result3489.LocalEndPoint, proxy);
+
+					Result3489 = client.Status;
+					await client.Query3489Async();
+					//TODO
 				}
-				catch (Exception ex)
+				else
 				{
-					MessageBox.Show(ex.Message, nameof(NatTypeTester), MessageBoxButton.OK, MessageBoxImage.Error);
+					throw new Exception(@"Wrong STUN Server!");
 				}
-			}).SubscribeOn(RxApp.TaskpoolScheduler);
+			}
+			catch (Exception ex)
+			{
+				MessageBox.Show(ex.Message, nameof(NatTypeTester), MessageBoxButton.OK, MessageBoxImage.Error);
+			}
 		}
 
-		private IObservable<Unit> DiscoveryNatTypeImpl()
+		private async Task DiscoveryNatTypeImpl(CancellationToken token)
 		{
-			return Observable.FromAsync(async () =>
+			try
 			{
-				try
+				var server = new StunServer();
+				if (server.Parse(StunServer))
 				{
-					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!");
-					}
+					using var proxy = ProxyFactory.CreateProxy(
+						ProxyType,
+						Result5389.LocalEndPoint,
+						NetUtils.ParseEndpoint(ProxyServer),
+						ProxyUser, ProxyPassword
+						);
+
+					using var client = new StunClient5389UDP(server.Hostname, server.Port, Result5389.LocalEndPoint, proxy);
+
+					Result5389 = client.Status;
+					await client.QueryAsync();
+					//TODO
 				}
-				catch (Exception ex)
+				else
 				{
-					MessageBox.Show(ex.Message, nameof(NatTypeTester), MessageBoxButton.OK, MessageBoxImage.Error);
+					throw new Exception(@"Wrong STUN Server!");
 				}
-			}).SubscribeOn(RxApp.TaskpoolScheduler);
+			}
+			catch (Exception ex)
+			{
+				MessageBox.Show(ex.Message, nameof(NatTypeTester), MessageBoxButton.OK, MessageBoxImage.Error);
+			}
 		}
 	}
 }

+ 43 - 52
STUN/Client/StunClient3489.cs

@@ -1,3 +1,4 @@
+using ReactiveUI.Fody.Helpers;
 using STUN.DnsClients;
 using STUN.Enums;
 using STUN.Message;
@@ -8,8 +9,6 @@ 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;
 
@@ -21,19 +20,6 @@ namespace STUN.Client
 	/// </summary>
 	public class StunClient3489 : IDisposable
 	{
-		#region Subject
-
-		private readonly Subject<NatType> _natTypeSubj = new();
-		public IObservable<NatType> NatTypeChanged => _natTypeSubj.AsObservable();
-
-		protected readonly Subject<IPEndPoint?> PubSubj = new();
-		public IObservable<IPEndPoint?> PubChanged => PubSubj.AsObservable();
-
-		protected readonly Subject<IPEndPoint?> LocalSubj = new();
-		public IObservable<IPEndPoint?> LocalChanged => LocalSubj.AsObservable();
-
-		#endregion
-
 		public IPEndPoint LocalEndPoint => Proxy.LocalEndPoint;
 
 		public TimeSpan Timeout
@@ -45,10 +31,13 @@ namespace STUN.Client
 		protected readonly IPAddress Server;
 		protected readonly ushort Port;
 
-		public IPEndPoint RemoteEndPoint => new(Server, 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);
@@ -71,17 +60,22 @@ namespace STUN.Client
 			Port = port;
 
 			Timeout = TimeSpan.FromSeconds(1.6);
+			Status.LocalEndPoint = local;
 		}
 
-		public async Task<ClassicStunResult> Query3489Async()
+		private void Init()
 		{
-			var res = new ClassicStunResult();
-			_natTypeSubj.OnNext(res.NatType);
-			PubSubj.OnNext(res.PublicEndPoint);
+			Status.PublicEndPoint = default;
+			Status.LocalEndPoint = default;
+			Status.NatType = NatType.Unknown;
+		}
 
-			using var cts = new CancellationTokenSource(Timeout);
+		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 };
@@ -89,13 +83,13 @@ namespace STUN.Client
 				var (response1, remote1, local1) = await TestAsync(test1, RemoteEndPoint, RemoteEndPoint, cts.Token);
 				if (response1 is null || remote1 is null)
 				{
-					res.NatType = NatType.UdpBlocked;
-					return res;
+					Status.NatType = NatType.UdpBlocked;
+					return;
 				}
 
 				if (local1 is not null)
 				{
-					LocalSubj.OnNext(LocalEndPoint);
+					Status.LocalEndPoint = LocalEndPoint;
 				}
 
 				var mappedAddress1 = response1.GetMappedAddressAttribute();
@@ -107,11 +101,11 @@ namespace STUN.Client
 				|| Equals(changedAddress1.Address, remote1.Address)
 				|| changedAddress1.Port == remote1.Port)
 				{
-					res.NatType = NatType.UnsupportedServer;
-					return res;
+					Status.NatType = NatType.UnsupportedServer;
+					return;
 				}
 
-				PubSubj.OnNext(mappedAddress1); // 显示 test I 得到的映射地址
+				Status.PublicEndPoint = mappedAddress1; // 显示 test I 得到的映射地址
 
 				var test2 = new StunMessage5389
 				{
@@ -129,13 +123,15 @@ namespace STUN.Client
 					// No NAT
 					if (response2 is null)
 					{
-						res.NatType = NatType.SymmetricUdpFirewall;
-						res.PublicEndPoint = mappedAddress1;
-						return res;
+						Status.NatType = NatType.SymmetricUdpFirewall;
+						Status.PublicEndPoint = mappedAddress1;
 					}
-					res.NatType = NatType.OpenInternet;
-					res.PublicEndPoint = mappedAddress2;
-					return res;
+					else
+					{
+						Status.NatType = NatType.OpenInternet;
+						Status.PublicEndPoint = mappedAddress2;
+					}
+					return;
 				}
 
 				// NAT
@@ -143,9 +139,9 @@ namespace STUN.Client
 				{
 					// 有些单 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;
+					Status.NatType = type;
+					Status.PublicEndPoint = mappedAddress2;
+					return;
 				}
 
 				// Test I(#2)
@@ -155,15 +151,15 @@ namespace STUN.Client
 
 				if (mappedAddress12 is null)
 				{
-					res.NatType = NatType.Unknown;
-					return res;
+					Status.NatType = NatType.Unknown;
+					return;
 				}
 
 				if (!Equals(mappedAddress12, mappedAddress1))
 				{
-					res.NatType = NatType.Symmetric;
-					res.PublicEndPoint = mappedAddress12;
-					return res;
+					Status.NatType = NatType.Symmetric;
+					Status.PublicEndPoint = mappedAddress12;
+					return;
 				}
 
 				// Test III
@@ -177,19 +173,17 @@ namespace STUN.Client
 				var mappedAddress3 = response3.GetMappedAddressAttribute();
 				if (mappedAddress3 is not null)
 				{
-					res.NatType = NatType.RestrictedCone;
-					res.PublicEndPoint = mappedAddress3;
-					return res;
+					Status.NatType = NatType.RestrictedCone;
+					Status.PublicEndPoint = mappedAddress3;
+					return;
 				}
-				res.NatType = NatType.PortRestrictedCone;
-				res.PublicEndPoint = mappedAddress12;
-				return res;
+
+				Status.NatType = NatType.PortRestrictedCone;
+				Status.PublicEndPoint = mappedAddress12;
 			}
 			finally
 			{
 				await Proxy.DisconnectAsync();
-				_natTypeSubj.OnNext(res.NatType);
-				PubSubj.OnNext(res.PublicEndPoint);
 			}
 		}
 
@@ -231,9 +225,6 @@ namespace STUN.Client
 		public virtual void Dispose()
 		{
 			Proxy.Dispose();
-			_natTypeSubj.OnCompleted();
-			PubSubj.OnCompleted();
-			LocalSubj.OnCompleted();
 		}
 	}
 }

+ 136 - 167
STUN/Client/StunClient5389UDP.cs

@@ -1,3 +1,4 @@
+using ReactiveUI.Fody.Helpers;
 using STUN.DnsClients;
 using STUN.Enums;
 using STUN.Message;
@@ -6,8 +7,6 @@ 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;
 
@@ -19,94 +18,74 @@ namespace STUN.Client
 	/// </summary>
 	public class StunClient5389UDP : StunClient3489
 	{
-		#region Subject
-
-		private readonly Subject<BindingTestResult> _bindingSubj = new();
-		public IObservable<BindingTestResult> BindingTestResultChanged => _bindingSubj.AsObservable();
-
-		private readonly Subject<MappingBehavior> _mappingBehaviorSubj = new();
-		public IObservable<MappingBehavior> MappingBehaviorChanged => _mappingBehaviorSubj.AsObservable();
-
-		private readonly Subject<FilteringBehavior> _filteringBehaviorSubj = new();
-		public IObservable<FilteringBehavior> FilteringBehaviorChanged => _filteringBehaviorSubj.AsObservable();
-
-		#endregion
+		[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;
+		}
+
+		private void Init()
+		{
+			Status.PublicEndPoint = default;
+			Status.LocalEndPoint = default;
+			Status.OtherEndPoint = default;
+			Status.BindingTestResult = BindingTestResult.Unknown;
+			Status.MappingBehavior = MappingBehavior.Unknown;
+			Status.FilteringBehavior = FilteringBehavior.Unknown;
 		}
 
-		public async Task<StunResult5389> QueryAsync()
+		public async Task QueryAsync()
 		{
-			var result = new StunResult5389();
 			try
 			{
-				_bindingSubj.OnNext(result.BindingTestResult);
-				_mappingBehaviorSubj.OnNext(result.MappingBehavior);
-				_filteringBehaviorSubj.OnNext(result.FilteringBehavior);
-				PubSubj.OnNext(result.PublicEndPoint);
+				Init();
 
 				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
+				await FilteringBehaviorTestBaseAsync(cts.Token);
+				if (Status.BindingTestResult != BindingTestResult.Success
+					|| Status.FilteringBehavior == FilteringBehavior.UnsupportedServer
 				)
 				{
-					return result;
+					return;
 				}
 
-				if (Equals(result.PublicEndPoint, result.LocalEndPoint))
+				if (Equals(Status.PublicEndPoint, Status.LocalEndPoint))
 				{
-					result.MappingBehavior = MappingBehavior.Direct;
-					return result;
+					Status.MappingBehavior = MappingBehavior.Direct;
+					return;
 				}
 
 				// 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))
+				var (success2, result2) = await MappingBehaviorTestBase2Async(cts.Token);
+				if (!success2)
 				{
-					result.MappingBehavior = MappingBehavior.EndpointIndependent;
-					return result;
+					return;
 				}
 
 				// 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;
+				await MappingBehaviorTestBase3Async(result2, cts.Token);
 			}
 			finally
 			{
-				_mappingBehaviorSubj.OnNext(result.MappingBehavior);
 				await Proxy.DisconnectAsync();
 			}
 		}
 
-		public async Task<StunResult5389> BindingTestAsync()
+		public async Task BindingTestAsync()
 		{
 			try
 			{
+				Init();
 				using var cts = new CancellationTokenSource(Timeout);
 				await Proxy.ConnectAsync(cts.Token);
-				var result = await BindingTestBaseAsync(RemoteEndPoint, true, cts.Token);
-				return result;
+				await BindingTestBaseAsync(RemoteEndPoint, Status, cts.Token);
 			}
 			finally
 			{
@@ -114,189 +93,179 @@ namespace STUN.Client
 			}
 		}
 
-		private async Task<StunResult5389> BindingTestBaseAsync(IPEndPoint remote, bool notifyChanged, CancellationToken token)
+		private async Task BindingTestBaseAsync(IPEndPoint remote, StunResult5389 result, 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 mappedAddress1 = response1.GetXorMappedAddressAttribute();
+			var otherAddress = response1.GetOtherAddressAttribute();
 			var local = local1 is null ? null : new IPEndPoint(local1, LocalEndPoint.Port);
 
 			if (response1 is null)
 			{
-				res = BindingTestResult.Fail;
+				result.BindingTestResult = BindingTestResult.Fail;
 			}
 			else if (mappedAddress1 is null)
 			{
-				res = BindingTestResult.UnsupportedServer;
+				result.BindingTestResult = BindingTestResult.UnsupportedServer;
 			}
 			else
 			{
-				res = BindingTestResult.Success;
-			}
-
-			if (notifyChanged)
-			{
-				_bindingSubj.OnNext(res);
-				PubSubj.OnNext(mappedAddress1);
+				result.BindingTestResult = BindingTestResult.Success;
 			}
-			LocalSubj.OnNext(LocalEndPoint);
 
-			return new StunResult5389
-			{
-				BindingTestResult = res,
-				LocalEndPoint = local,
-				PublicEndPoint = mappedAddress1,
-				OtherEndPoint = otherAddress
-			};
+			result.LocalEndPoint = local;
+			result.PublicEndPoint = mappedAddress1;
+			result.OtherEndPoint = otherAddress;
 		}
 
-		public async Task<StunResult5389> MappingBehaviorTestAsync()
+		public async Task MappingBehaviorTestAsync()
 		{
-			var result = new StunResult5389();
-			using var cts = new CancellationTokenSource(Timeout);
 			try
 			{
+				Init();
+				using var cts = new CancellationTokenSource(Timeout);
 				await Proxy.ConnectAsync(cts.Token);
+
 				// test I
-				result = await BindingTestBaseAsync(RemoteEndPoint, true, cts.Token);
-				if (result.BindingTestResult != BindingTestResult.Success)
+				await BindingTestBaseAsync(RemoteEndPoint, Status, cts.Token);
+				if (Status.BindingTestResult != BindingTestResult.Success)
 				{
-					return result;
+					return;
 				}
 
-				if (result.OtherEndPoint is null
-					|| Equals(result.OtherEndPoint.Address, RemoteEndPoint.Address)
-					|| result.OtherEndPoint.Port == RemoteEndPoint.Port)
+				if (Status.OtherEndPoint is null
+					|| Equals(Status.OtherEndPoint.Address, RemoteEndPoint.Address)
+					|| Status.OtherEndPoint.Port == RemoteEndPoint.Port)
 				{
-					result.MappingBehavior = MappingBehavior.UnsupportedServer;
-					return result;
+					Status.MappingBehavior = MappingBehavior.UnsupportedServer;
+					return;
 				}
 
-				if (Equals(result.PublicEndPoint, result.LocalEndPoint))
+				if (Equals(Status.PublicEndPoint, Status.LocalEndPoint))
 				{
-					result.MappingBehavior = MappingBehavior.Direct;
-					return result;
+					Status.MappingBehavior = MappingBehavior.Direct;
+					return;
 				}
 
 				// test II
-				var result2 = await BindingTestBaseAsync(new IPEndPoint(result.OtherEndPoint.Address, RemoteEndPoint.Port), false, cts.Token);
-				if (result2.BindingTestResult != BindingTestResult.Success)
+				var (success2, result2) = await MappingBehaviorTestBase2Async(cts.Token);
+				if (!success2)
 				{
-					result.MappingBehavior = MappingBehavior.Fail;
-					return result;
-				}
-
-				if (Equals(result2.PublicEndPoint, result.PublicEndPoint))
-				{
-					result.MappingBehavior = MappingBehavior.EndpointIndependent;
-					return result;
+					return;
 				}
 
 				// 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;
+				await MappingBehaviorTestBase3Async(result2, cts.Token);
 			}
 			finally
 			{
-				_mappingBehaviorSubj.OnNext(result.MappingBehavior);
 				await Proxy.DisconnectAsync();
 			}
 		}
 
-		private async Task<StunResult5389> FilteringBehaviorTestBaseAsync(CancellationToken token)
+		private async Task<(bool, StunResult5389)> MappingBehaviorTestBase2Async(CancellationToken token)
+		{
+			var result2 = new StunResult5389();
+			await BindingTestBaseAsync(new IPEndPoint(Status.OtherEndPoint!.Address, RemoteEndPoint.Port), result2, token);
+			Status.LocalEndPoint = result2.LocalEndPoint;
+			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 = new StunResult5389();
+			await BindingTestBaseAsync(Status.OtherEndPoint!, result3, token);
+			Status.LocalEndPoint = result3.LocalEndPoint;
+			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
-			var result1 = await BindingTestBaseAsync(RemoteEndPoint, true, token);
-			try
+			await BindingTestBaseAsync(RemoteEndPoint, Status, token);
+			if (Status.BindingTestResult != BindingTestResult.Success)
 			{
-				if (result1.BindingTestResult != BindingTestResult.Success)
-				{
-					return result1;
-				}
+				return;
+			}
 
-				if (result1.OtherEndPoint is null
-					|| Equals(result1.OtherEndPoint.Address, RemoteEndPoint.Address)
-					|| result1.OtherEndPoint.Port == RemoteEndPoint.Port)
-				{
-					result1.FilteringBehavior = FilteringBehavior.UnsupportedServer;
-					return result1;
-				}
+			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, result1.OtherEndPoint, token);
+			// 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)
-				{
-					result1.FilteringBehavior = FilteringBehavior.EndpointIndependent;
-					return result1;
-				}
+			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);
+			// 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)
-				{
-					result1.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
-					return result1;
-				}
+			if (response3 is null || remote3 is null)
+			{
+				Status.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
+				return;
+			}
 
-				if (Equals(remote3.Address, RemoteEndPoint.Address) && remote3.Port != RemoteEndPoint.Port)
-				{
-					result1.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
-				}
-				else
-				{
-					result1.FilteringBehavior = FilteringBehavior.UnsupportedServer;
-				}
-				return result1;
+			if (Equals(remote3.Address, RemoteEndPoint.Address) && remote3.Port != RemoteEndPoint.Port)
+			{
+				Status.FilteringBehavior = FilteringBehavior.AddressAndPortDependent;
 			}
-			finally
+			else
 			{
-				_filteringBehaviorSubj.OnNext(result1.FilteringBehavior);
+				Status.FilteringBehavior = FilteringBehavior.UnsupportedServer;
 			}
 		}
 
-		public async Task<StunResult5389> FilteringBehaviorTestAsync()
+		public async Task FilteringBehaviorTestAsync()
 		{
 			try
 			{
+				Init();
 				using var cts = new CancellationTokenSource(Timeout);
 				await Proxy.ConnectAsync(cts.Token);
-				var result = await FilteringBehaviorTestBaseAsync(cts.Token);
-				return result;
+				await FilteringBehaviorTestBaseAsync(cts.Token);
 			}
 			finally
 			{
 				await Proxy.DisconnectAsync();
 			}
 		}
-
-		public override void Dispose()
-		{
-			base.Dispose();
-			_bindingSubj.OnCompleted();
-			_mappingBehaviorSubj.OnCompleted();
-			_filteringBehaviorSubj.OnCompleted();
-		}
 	}
 }

+ 1 - 6
STUN/StunResult/ClassicStunResult.cs

@@ -1,16 +1,11 @@
-using ReactiveUI;
 using ReactiveUI.Fody.Helpers;
 using STUN.Enums;
-using System.Net;
 
 namespace STUN.StunResult
 {
-	public class ClassicStunResult : ReactiveObject
+	public class ClassicStunResult : StunResult
 	{
 		[Reactive]
 		public NatType NatType { get; set; } = NatType.Unknown;
-
-		[Reactive]
-		public IPEndPoint? PublicEndPoint { get; set; }
 	}
 }

+ 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; }
+	}
+}

+ 1 - 8
STUN/StunResult/StunResult5389.cs

@@ -1,18 +1,11 @@
-using ReactiveUI;
 using ReactiveUI.Fody.Helpers;
 using STUN.Enums;
 using System.Net;
 
 namespace STUN.StunResult
 {
-	public class StunResult5389 : ReactiveObject
+	public class StunResult5389 : StunResult
 	{
-		[Reactive]
-		public IPEndPoint? PublicEndPoint { get; set; }
-
-		[Reactive]
-		public IPEndPoint? LocalEndPoint { get; set; }
-
 		[Reactive]
 		public IPEndPoint? OtherEndPoint { get; set; }
 

+ 0 - 2
STUN/Utils/NetUtils.cs

@@ -7,8 +7,6 @@ namespace STUN.Utils
 {
 	public static class NetUtils
 	{
-		public const string DefaultLocalEnd = @"0.0.0.0:0";
-
 		public static IPEndPoint? ParseEndpoint(string? str)
 		{
 			if (str is null)

+ 28 - 19
UnitTest/UnitTest.cs

@@ -86,7 +86,8 @@ namespace UnitTest
 		public async Task BindingTest()
 		{
 			using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-			var result = await client.BindingTestAsync();
+			await client.BindingTestAsync();
+			var result = client.Status;
 
 			Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
 			Assert.IsNotNull(result.LocalEndPoint);
@@ -101,17 +102,19 @@ namespace UnitTest
 		public async Task MappingBehaviorTest()
 		{
 			using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-			var result = await client.MappingBehaviorTestAsync();
+			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.IsTrue(result.MappingBehavior is
+				MappingBehavior.Direct or
+				MappingBehavior.EndpointIndependent or
+				MappingBehavior.AddressDependent or
+				MappingBehavior.AddressAndPortDependent
 			);
 			Assert.AreEqual(result.FilteringBehavior, FilteringBehavior.Unknown);
 		}
@@ -120,7 +123,8 @@ namespace UnitTest
 		public async Task FilteringBehaviorTest()
 		{
 			using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-			var result = await client.FilteringBehaviorTestAsync();
+			await client.FilteringBehaviorTestAsync();
+			var result = client.Status;
 
 			Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
 			Assert.IsNotNull(result.LocalEndPoint);
@@ -128,9 +132,10 @@ namespace UnitTest
 			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
+			Assert.IsTrue(result.FilteringBehavior is
+				FilteringBehavior.EndpointIndependent or
+				FilteringBehavior.AddressDependent or
+				FilteringBehavior.AddressAndPortDependent
 			);
 		}
 
@@ -138,21 +143,24 @@ namespace UnitTest
 		public async Task CombiningTest()
 		{
 			using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-			var result = await client.QueryAsync();
+			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.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
+			Assert.IsTrue(result.FilteringBehavior is
+				FilteringBehavior.EndpointIndependent or
+				FilteringBehavior.AddressDependent or
+				FilteringBehavior.AddressAndPortDependent
 			);
 		}
 
@@ -161,7 +169,8 @@ namespace UnitTest
 		{
 			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();
+			await client.QueryAsync();
+			var result = client.Status;
 
 			Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
 			Assert.IsNotNull(result.LocalEndPoint);