Browse Source

Update UI

Bruce Wayne 5 years ago
parent
commit
fa8a5646e2

+ 5 - 0
NatTypeTester/App.cs

@@ -1,5 +1,8 @@
 using System;
+using System.Reflection;
 using System.Windows;
+using ReactiveUI;
+using Splat;
 
 namespace NatTypeTester
 {
@@ -11,6 +14,8 @@ namespace NatTypeTester
             var app = new Application();
             var win = new MainWindow();
 
+            Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetCallingAssembly());
+
             app.MainWindow = win;
             win.Show();
 

+ 67 - 33
NatTypeTester/MainWindow.xaml

@@ -1,9 +1,16 @@
-<Window x:Class="NatTypeTester.MainWindow"
-		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-		Title="NatTypeTester" Height="210" Width="385" WindowStartupLocation="CenterScreen" ResizeMode="CanMinimize"
-		DataContext="{Binding RelativeSource={RelativeSource Self}}"
-		KeyDown="MainWindow_OnKeyDown">
+<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"
+	Title="NatTypeTester"
+	Width="385"
+	WindowStartupLocation="CenterScreen"
+	SizeToContent="Height"
+	ResizeMode="CanMinimize"
+	Topmost="False">
 
 	<Window.Resources>
 		<SolidColorBrush x:Key="DisabledBackgroundBrush" Color="LightGray" />
@@ -17,34 +24,61 @@
 	</Window.Resources>
 
 	<Grid>
-		<Grid.RowDefinitions>
-			<RowDefinition />
-			<RowDefinition />
-			<RowDefinition />
-			<RowDefinition />
-			<RowDefinition />
-		</Grid.RowDefinitions>
-		<Grid.ColumnDefinitions>
-			<ColumnDefinition Width="Auto"/>
-			<ColumnDefinition />
-			<ColumnDefinition Width="65" />
-		</Grid.ColumnDefinitions>
+		<StackPanel>
+			<Grid Margin="0,5,5,5">
+				<Grid.ColumnDefinitions>
+					<ColumnDefinition Width="Auto"/>
+					<ColumnDefinition />
+				</Grid.ColumnDefinitions>
+				<TextBlock Grid.Row="0" Grid.Column="0" Margin="5,0" VerticalAlignment="Center" Text="STUN Server" />
+				<ComboBox Grid.Column="1" x:Name="ServersComboBox"
+						Height="23.24"  IsEditable="True"
+						SelectedIndex="0" VerticalContentAlignment="Center">
+					<ComboBox.ItemTemplate>
+						<DataTemplate>
+							<TextBlock Text="{Binding}" />
+						</DataTemplate>
+					</ComboBox.ItemTemplate>
+				</ComboBox>
+			</Grid>
+			<TabControl>
+				<TabItem Header="RFC 3489">
+					<Grid>
+						<Grid.RowDefinitions>
+							<RowDefinition />
+							<RowDefinition />
+							<RowDefinition />
+							<RowDefinition />
+							<RowDefinition />
+						</Grid.RowDefinitions>
+						<Grid.ColumnDefinitions>
+							<ColumnDefinition Width="Auto" />
+							<ColumnDefinition />
+							<ColumnDefinition Width="65" />
+						</Grid.ColumnDefinitions>
 
-		<TextBlock Grid.Row="0" Grid.Column="0" Margin="5,0" VerticalAlignment="Center" Text="STUN Server" />
-		<TextBlock Grid.Row="1" Grid.Column="0" Margin="5,0" VerticalAlignment="Center" Text="NAT type" />
-		<TextBlock Grid.Row="2" Grid.Column="0" Margin="5,0" VerticalAlignment="Center" Text="Local end" />
-		<TextBlock Grid.Row="3" Grid.Column="0" Margin="5,0" VerticalAlignment="Center" Text="Public end" />
+						<TextBlock Grid.Row="1" Grid.Column="0" Margin="5,0" VerticalAlignment="Center" Text="NAT type" />
+						<TextBlock Grid.Row="2" Grid.Column="0" Margin="5,0" VerticalAlignment="Center" Text="Local end" />
+						<TextBlock Grid.Row="3" Grid.Column="0" Margin="5,0" VerticalAlignment="Center" Text="Public end" />
 
-		<ComboBox x:Name="ServersComboBox" ItemsSource="{Binding StunServers}" 
-					Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Height="23.24"
-					Margin="0,5,5,0" IsEditable="True" SelectedIndex="0" VerticalContentAlignment="Center" />
-		<TextBox x:Name="NatTypeTextBox" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Height="23.24" Margin="0,5,5,0"
-		IsReadOnly="True" VerticalContentAlignment="Center" />
-		<TextBox x:Name="LocalEndTextBox" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Height="23.24" Margin="0,5,5,0" 
-		VerticalContentAlignment="Center" Text="0.0.0.0:0"/>
-		<TextBox x:Name="PublicEndTextBox" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Height="23.24" Margin="0,5,5,0" 
-		IsReadOnly="True" VerticalContentAlignment="Center" />
+						<TextBox x:Name="NatTypeTextBox" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
+						Height="23.24" Margin="5" IsReadOnly="True"
+						VerticalContentAlignment="Center" VerticalAlignment="Center"/>
+						<TextBox x:Name="LocalEndTextBox" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
+						Height="23.24" Margin="5"
+						VerticalContentAlignment="Center" VerticalAlignment="Center"
+						Text="0.0.0.0:0" />
+						<TextBox x:Name="PublicEndTextBox" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2"
+						Height="23.24" Margin="5" IsReadOnly="True"
+						VerticalContentAlignment="Center" VerticalAlignment="Center" />
 
-		<Button x:Name="TestButton" Grid.Row="4" Grid.Column="2" Content="Test" Margin="0,5,5,10" Click="TestButton_OnClick"/>
+						<Button x:Name="TestButton" Grid.Row="4" Grid.Column="2" Content="Test" Margin="5"/>
+					</Grid>
+				</TabItem>
+				<TabItem Header="RFC 5780">
+
+				</TabItem>
+			</TabControl>
+		</StackPanel>
 	</Grid>
-</Window>
+</reactiveUi:ReactiveWindow>

+ 32 - 66
NatTypeTester/MainWindow.xaml.cs

@@ -1,10 +1,6 @@
-using NatTypeTester.Model;
-using STUN.Utils;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Input;
+using System.Reactive.Disposables;
+using NatTypeTester.ViewModels;
+using ReactiveUI;
 
 namespace NatTypeTester
 {
@@ -13,70 +9,40 @@ namespace NatTypeTester
         public MainWindow()
         {
             InitializeComponent();
-            LoadStunServer();
-        }
-
-        public static HashSet<string> StunServers { get; set; } = new HashSet<string>
-        {
-                @"stun.qq.com",
-                @"stun.miwifi.com",
-                @"stun.bige0.com",
-                @"stun.syncthing.net",
-                @"stun.stunprotocol.org"
-        };
+            ViewModel = new MainWindowViewModel();
 
-        private async void TestButton_OnClick(object sender, RoutedEventArgs e)
-        {
-            var stun = new StunServer();
-            if (stun.Parse(ServersComboBox.Text))
+            this.WhenActivated(disposableRegistration =>
             {
-                var server = stun.Hostname;
-                var port = stun.Port;
-                var local = LocalEndTextBox.Text;
-                TestButton.IsEnabled = false;
-                await Task.Run(() =>
-                {
-                    var (natType, localEnd, publicEnd) = NetUtils.NatTypeTestCore(local, server, port);
+                this.Bind(ViewModel,
+                        vm => vm.StunServer,
+                        v => v.ServersComboBox.Text
+                ).DisposeWith(disposableRegistration);
 
-                    Dispatcher?.InvokeAsync(() =>
-                    {
-                        NatTypeTextBox.Text = natType;
-                        LocalEndTextBox.Text = localEnd;
-                        PublicEndTextBox.Text = publicEnd;
-                        TestButton.IsEnabled = true;
-                    });
-                });
-            }
-            else
-            {
-                MessageBox.Show(@"Wrong Stun server!", @"NatTypeTester", MessageBoxButton.OK, MessageBoxImage.Error);
-            }
-        }
+                this.OneWayBind(ViewModel,
+                        vm => vm.StunServers,
+                        v => v.ServersComboBox.ItemsSource
+                ).DisposeWith(disposableRegistration);
 
-        private void MainWindow_OnKeyDown(object sender, KeyEventArgs e)
-        {
-            if (e.Key == Key.Enter)
-            {
-                TestButton_OnClick(this, new RoutedEventArgs());
-            }
-        }
+                this.OneWayBind(ViewModel,
+                        vm => vm.ClassicNatType,
+                        v => v.NatTypeTextBox.Text
+                ).DisposeWith(disposableRegistration);
 
-        private async void LoadStunServer()
-        {
-            const string path = @"stun.txt";
-            if (File.Exists(path))
-            {
-                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))
-                    {
-                        StunServers.Add(stun.ToString());
-                    }
-                }
-            }
+                this.Bind(ViewModel,
+                        vm => vm.LocalEnd,
+                        v => v.LocalEndTextBox.Text
+                ).DisposeWith(disposableRegistration);
+
+                this.OneWayBind(ViewModel,
+                        vm => vm.PublicEnd,
+                        v => v.PublicEndTextBox.Text
+                ).DisposeWith(disposableRegistration);
+
+                this.BindCommand(ViewModel,
+                                viewModel => viewModel.TestClassicNatType,
+                                view => view.TestButton)
+                        .DisposeWith(disposableRegistration);
+            });
         }
     }
 }

+ 12 - 3
NatTypeTester/Model/StunServer.cs

@@ -1,16 +1,17 @@
 using System;
 using System.Net;
+using System.Net.Sockets;
 
 namespace NatTypeTester.Model
 {
     public class StunServer
     {
-        public string Hostname;
-        public ushort Port;
+        public string Hostname { get; set; }
+        public ushort Port { get; set; }
 
         public StunServer()
         {
-            Hostname = @"stun.qq.com";
+            Hostname = @"stun.syncthing.net";
             Port = 3478;
         }
 
@@ -47,10 +48,18 @@ namespace NatTypeTester.Model
 
         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}";
         }
     }

+ 1 - 0
NatTypeTester/NatTypeTester.csproj

@@ -15,6 +15,7 @@
 
   <ItemGroup>
     <PackageReference Include="Costura.Fody" Version="4.1.0" />
+    <PackageReference Include="ReactiveUI.WPF" Version="11.5.17" />
   </ItemGroup>
 
   <ItemGroup>

+ 123 - 0
NatTypeTester/ViewModels/MainWindowViewModel.cs

@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using DynamicData;
+using DynamicData.Binding;
+using NatTypeTester.Model;
+using ReactiveUI;
+using STUN.Client;
+using STUN.Utils;
+
+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;
+        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 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
+
+        public MainWindowViewModel()
+        {
+            LoadStunServer();
+            List.Connect()
+                .DistinctValues(x => x)
+                .ObserveOnDispatcher()
+                .Bind(StunServers)
+                .Subscribe();
+            TestClassicNatType = ReactiveCommand.CreateFromObservable(TestClassicNatTypeImp);
+        }
+
+        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> TestClassicNatTypeImp()
+        {
+            return Observable.Start(() =>
+            {
+                var server = new StunServer();
+                if (server.Parse(StunServer))
+                {
+                    using var client = new StunClient3489(server.Hostname, server.Port, NetUtils.ParseEndpoint(LocalEnd));
+                    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}");
+                    client.Query();
+                }
+                else
+                {
+
+                }
+            });
+        }
+    }
+}

+ 118 - 69
STUN/Client/StunClient3489.cs

@@ -8,7 +8,8 @@ using System.Diagnostics;
 using System.Linq;
 using System.Net;
 using System.Net.Sockets;
-using System.Threading.Tasks;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
 
 namespace STUN.Client
 {
@@ -16,8 +17,21 @@ namespace STUN.Client
     /// https://tools.ietf.org/html/rfc3489#section-10.1
     /// https://upload.wikimedia.org/wikipedia/commons/6/63/STUN_Algorithm3.svg
     /// </summary>
-    public class StunClient3489 : IStunClient
+    public class StunClient3489 : IDisposable
     {
+        #region Subject
+
+        private readonly Subject<NatType> _natTypeSubj = new Subject<NatType>();
+        public IObservable<NatType> NatTypeChanged => _natTypeSubj.AsObservable();
+
+        private readonly Subject<IPEndPoint> _pubSubj = new Subject<IPEndPoint>();
+        public IObservable<IPEndPoint> PubChanged => _pubSubj.AsObservable();
+
+        private readonly Subject<IPEndPoint> _localSubj = new Subject<IPEndPoint>();
+        public IObservable<IPEndPoint> LocalChanged => _localSubj.AsObservable();
+
+        #endregion
+
         public IPEndPoint LocalEndPoint => (IPEndPoint)UdpClient.Client.LocalEndPoint;
 
         public TimeSpan Timeout
@@ -67,88 +81,121 @@ namespace STUN.Client
             Timeout = TimeSpan.FromSeconds(1.6);
         }
 
-        public virtual IStunResult Query()
+        public ClassicStunResult Query()
         {
-            // test I
-            var test1 = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest, MagicCookie = 0 };
+            var res = new ClassicStunResult();
+            _natTypeSubj.OnNext(res.NatType);
+            _pubSubj.OnNext(res.PublicEndPoint);
 
-            var (response1, remote1, local1) = Test(test1, RemoteEndPoint, RemoteEndPoint);
-            if (response1 == null)
-            {
-                return new ClassicStunResult(NatType.UdpBlocked, null);
-            }
-            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)
+            try
             {
-                return new ClassicStunResult(NatType.UnsupportedServer, null);
-            }
+                // test I
+                var test1 = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest, MagicCookie = 0 };
 
-            var test2 = new StunMessage5389
-            {
-                StunMessageType = StunMessageType.BindingRequest,
-                MagicCookie = 0,
-                Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
-            };
+                var (response1, remote1, local1) = Test(test1, RemoteEndPoint, RemoteEndPoint);
+                if (response1 == null)
+                {
+                    res.NatType = NatType.UdpBlocked;
+                    return res;
+                }
+
+                if (local1 != null)
+                {
+                    _localSubj.OnNext(LocalEndPoint);
+                }
 
-            // test II
-            var (response2, remote2, _) = Test(test2, RemoteEndPoint, changedAddress1);
-            var mappedAddress2 = AttributeExtensions.GetMappedAddressAttribute(response2);
+                var mappedAddress1 = AttributeExtensions.GetMappedAddressAttribute(response1);
+                var changedAddress1 = AttributeExtensions.GetChangedAddressAttribute(response1);
 
-            if (Equals(mappedAddress1.Address, local1) && mappedAddress1.Port == LocalEndPoint.Port)
-            {
-                // No NAT
-                if (response2 == null)
+                // 某些单 IP 服务器的迷惑操作
+                if (mappedAddress1 == null
+                || changedAddress1 == null
+                || Equals(changedAddress1.Address, remote1.Address)
+                || changedAddress1.Port == remote1.Port)
                 {
-                    return new ClassicStunResult(NatType.SymmetricUdpFirewall, mappedAddress1);
+                    res.NatType = NatType.UnsupportedServer;
+                    return res;
                 }
-                return new ClassicStunResult(NatType.OpenInternet, mappedAddress2);
-            }
 
-            // NAT
-            if (response2 != null)
-            {
-                // 有些单 IP 服务器并不能测 NAT 类型,比如 Google 的
-                var type = Equals(remote1.Address, remote2.Address) || remote1.Port == remote2.Port ? NatType.UnsupportedServer : NatType.FullCone;
-                return new ClassicStunResult(type, mappedAddress2);
-            }
+                _pubSubj.OnNext(mappedAddress1); // 显示 test I 得到的映射地址
 
-            // Test I(#2)
-            var test12 = new StunMessage5389 { StunMessageType = StunMessageType.BindingRequest, MagicCookie = 0 };
-            var (response12, _, _) = Test(test12, changedAddress1, changedAddress1);
-            var mappedAddress12 = AttributeExtensions.GetMappedAddressAttribute(response12);
+                var test2 = new StunMessage5389
+                {
+                    StunMessageType = StunMessageType.BindingRequest,
+                    MagicCookie = 0,
+                    Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) }
+                };
 
-            if (mappedAddress12 == null) return new ClassicStunResult(NatType.Unknown, null);
+                // test II
+                var (response2, remote2, _) = Test(test2, RemoteEndPoint, changedAddress1);
+                var mappedAddress2 = AttributeExtensions.GetMappedAddressAttribute(response2);
 
-            if (!Equals(mappedAddress12, mappedAddress1))
-            {
-                return new ClassicStunResult(NatType.Symmetric, mappedAddress12);
-            }
+                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;
+                }
 
-            // Test III
-            var test3 = new StunMessage5389
-            {
-                StunMessageType = StunMessageType.BindingRequest,
-                MagicCookie = 0,
-                Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) }
-            };
-            var (response3, _, _) = Test(test3, changedAddress1, changedAddress1);
-            var mappedAddress3 = AttributeExtensions.GetMappedAddressAttribute(response3);
-            if (mappedAddress3 != null)
+                // 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, _, _) = Test(test12, changedAddress1, changedAddress1);
+                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, _, _) = Test(test3, changedAddress1, changedAddress1);
+                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
             {
-                return new ClassicStunResult(NatType.RestrictedCone, mappedAddress3);
+                _natTypeSubj.OnNext(res.NatType);
+                _pubSubj.OnNext(res.PublicEndPoint);
             }
-            return new ClassicStunResult(NatType.PortRestrictedCone, mappedAddress12);
-        }
-
-        public virtual Task<IStunResult> QueryAsync()
-        {
-            throw new NotImplementedException();
         }
 
         /// <returns>
@@ -192,6 +239,8 @@ namespace STUN.Client
         public void Dispose()
         {
             UdpClient?.Dispose();
+            _natTypeSubj.OnCompleted();
+            _pubSubj.OnCompleted();
         }
     }
 }

+ 1 - 6
STUN/Client/StunClient5389UDP.cs

@@ -23,12 +23,7 @@ namespace STUN.Client
             Timeout = TimeSpan.FromSeconds(3);
         }
 
-        public override IStunResult Query()
-        {
-            throw new NotImplementedException();
-        }
-
-        public override async Task<IStunResult> QueryAsync()
+        public async Task<StunResult5389> QueryAsync()
         {
             var result = await FilteringBehaviorTestAsync();
             if (result.BindingTestResult != BindingTestResult.Success

+ 0 - 11
STUN/Interfaces/IStunClient.cs

@@ -1,11 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace STUN.Interfaces
-{
-    public interface IStunClient : IDisposable
-    {
-        public IStunResult Query();
-        public Task<IStunResult> QueryAsync();
-    }
-}

+ 0 - 9
STUN/Interfaces/IStunResult.cs

@@ -1,9 +0,0 @@
-using System.Net;
-
-namespace STUN.Interfaces
-{
-    public interface IStunResult
-    {
-        public IPEndPoint PublicEndPoint { get; }
-    }
-}

+ 4 - 0
STUN/STUN.csproj

@@ -5,4 +5,8 @@
     <LangVersion>latest</LangVersion>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="ReactiveUI" Version="11.5.17" />
+  </ItemGroup>
+
 </Project>

+ 13 - 24
STUN/StunResult/ClassicStunResult.cs

@@ -1,35 +1,24 @@
 using System.Net;
+using ReactiveUI;
 using STUN.Enums;
-using STUN.Interfaces;
 
 namespace STUN.StunResult
 {
-    public class ClassicStunResult : IStunResult
+    public class ClassicStunResult : ReactiveObject
     {
-        /// <summary>
-        /// Default constructor.
-        /// </summary>
-        /// <param name="natType">Specifies UDP network type.</param>
-        /// <param name="publicEndPoint">Public IP end point.</param>
-        public ClassicStunResult(NatType natType, IPEndPoint publicEndPoint)
+        private NatType _natType = NatType.Unknown;
+        private IPEndPoint _publicEndPoint;
+
+        public NatType NatType
         {
-            NatType = natType;
-            PublicEndPoint = publicEndPoint;
+            get => _natType;
+            set => this.RaiseAndSetIfChanged(ref _natType, value);
         }
 
-        #region Properties Implementation
-
-        /// <summary>
-        /// Gets UDP network type.
-        /// </summary>
-        public NatType NatType { get; }
-
-        /// <summary>
-        /// Gets public IP end point. This value is null if failed to get network type.
-        /// </summary>
-        public IPEndPoint PublicEndPoint { get; }
-
-        #endregion
-
+        public IPEndPoint PublicEndPoint
+        {
+            get => _publicEndPoint;
+            set => this.RaiseAndSetIfChanged(ref _publicEndPoint, value);
+        }
     }
 }

+ 2 - 2
STUN/StunResult/StunResult5389.cs

@@ -1,10 +1,10 @@
 using STUN.Enums;
-using STUN.Interfaces;
 using System.Net;
+using ReactiveUI;
 
 namespace STUN.StunResult
 {
-    public class StunResult5389 : IStunResult
+    public class StunResult5389 : ReactiveObject
     {
         public IPEndPoint PublicEndPoint { get; set; }
         public IPEndPoint LocalEndPoint { get; set; }

+ 2 - 2
STUN/Utils/NetUtils.cs

@@ -54,7 +54,7 @@ namespace STUN.Utils
 
                 using var client = new StunClient3489(server, port, ParseEndpoint(local));
 
-                var result = (ClassicStunResult)client.Query();
+                var result = client.Query();
 
                 return (
                         result.NatType.ToString(),
@@ -72,7 +72,7 @@ namespace STUN.Utils
         public static async Task<StunResult5389> NatBehaviorDiscovery(string server, ushort port, IPEndPoint local)
         {
             using var client = new StunClient5389UDP(server, port, local);
-            return (StunResult5389)await client.QueryAsync();
+            return await client.QueryAsync();
         }
 
         public static (byte[], IPEndPoint, IPAddress) UdpReceive(this UdpClient client, byte[] bytes, IPEndPoint remote, EndPoint receive)

+ 2 - 4
UnitTest/UnitTest.cs

@@ -1,12 +1,10 @@
-using System;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
 using STUN.Client;
 using STUN.Enums;
 using STUN.Message.Attributes;
 using System.Linq;
 using System.Net;
 using System.Threading.Tasks;
-using STUN.StunResult;
 using STUN.Utils;
 
 namespace UnitTest
@@ -138,7 +136,7 @@ namespace UnitTest
         public async Task CombiningTest()
         {
             using var client = new StunClient5389UDP(@"stun.syncthing.net", 3478, new IPEndPoint(IPAddress.Any, 0));
-            var result = (StunResult5389)await client.QueryAsync();
+            var result = await client.QueryAsync();
 
             Assert.AreEqual(result.BindingTestResult, BindingTestResult.Success);
             Assert.IsNotNull(result.LocalEndPoint);