瀏覽代碼

feat: Add stun tcp client

Bruce Wayne 2 年之前
父節點
當前提交
2f08ec2547

+ 1 - 1
NatTypeTester.Models/Config.cs

@@ -8,7 +8,7 @@ namespace NatTypeTester.Models;
 [UsedImplicitly]
 public record Config : ReactiveRecord, ISingletonDependency
 {
-	private string _stunServer = @"stun.syncthing.net";
+	private string _stunServer = @"stunserver.stunprotocol.org";
 	public string StunServer
 	{
 		get => _stunServer;

+ 25 - 18
NatTypeTester.ViewModels/RFC3489ViewModel.cs

@@ -28,13 +28,18 @@ public class RFC3489ViewModel : ViewModelBase, IRoutableViewModel
 	private IDnsClient AAAADnsClient => LazyServiceProvider.LazyGetRequiredService<DefaultAAAAClient>();
 	private IDnsClient ADnsClient => LazyServiceProvider.LazyGetRequiredService<DefaultAClient>();
 
-	public ClassicStunResult Result3489 { get; set; }
+	private ClassicStunResult _result3489;
+	public ClassicStunResult Result3489
+	{
+		get => _result3489;
+		set => this.RaiseAndSetIfChanged(ref _result3489, value);
+	}
 
 	public ReactiveCommand<Unit, Unit> TestClassicNatType { get; }
 
 	public RFC3489ViewModel()
 	{
-		Result3489 = new ClassicStunResult();
+		_result3489 = new ClassicStunResult();
 		TestClassicNatType = ReactiveCommand.CreateFromTask(TestClassicNatTypeAsync);
 	}
 
@@ -80,25 +85,27 @@ public class RFC3489ViewModel : ViewModelBase, IRoutableViewModel
 
 		using StunClient3489 client = new(new IPEndPoint(serverIp, server.Port), Result3489.LocalEndPoint, proxy);
 
-		Result3489 = client.State;
-		using (Observable.Interval(TimeSpan.FromSeconds(0.1))
-				.ObserveOn(RxApp.MainThreadScheduler)
-				.Subscribe(_ => this.RaisePropertyChanged(nameof(Result3489))))
+		try
 		{
-			await client.ConnectProxyAsync(token);
-			try
-			{
-				await client.QueryAsync(token);
-			}
-			finally
+			using (Observable.Interval(TimeSpan.FromSeconds(0.1))
+					.ObserveOn(RxApp.MainThreadScheduler)
+					// ReSharper disable once AccessToDisposedClosure
+					.Subscribe(_ => Result3489 = client.State with { }))
 			{
-				await client.CloseProxyAsync(token);
+				await client.ConnectProxyAsync(token);
+				try
+				{
+					await client.QueryAsync(token);
+				}
+				finally
+				{
+					await client.CloseProxyAsync(token);
+				}
 			}
 		}
-
-		Result3489 = new ClassicStunResult();
-		Result3489.Clone(client.State);
-
-		this.RaisePropertyChanged(nameof(Result3489));
+		finally
+		{
+			Result3489 = client.State with { };
+		}
 	}
 }

+ 47 - 19
NatTypeTester.ViewModels/RFC5780ViewModel.cs

@@ -7,6 +7,7 @@ using ReactiveUI;
 using Socks5.Models;
 using STUN;
 using STUN.Client;
+using STUN.Enums;
 using STUN.Proxy;
 using STUN.StunResult;
 using System.Net;
@@ -28,13 +29,26 @@ public class RFC5780ViewModel : ViewModelBase, IRoutableViewModel
 	private IDnsClient AAAADnsClient => LazyServiceProvider.LazyGetRequiredService<DefaultAAAAClient>();
 	private IDnsClient ADnsClient => LazyServiceProvider.LazyGetRequiredService<DefaultAClient>();
 
-	public StunResult5389 Result5389 { get; set; }
+	private StunResult5389 _result5389;
+
+	public StunResult5389 Result5389
+	{
+		get => _result5389;
+		set => this.RaiseAndSetIfChanged(ref _result5389, value);
+	}
+
+	private TransportType _transportType;
+	public TransportType TransportType
+	{
+		get => _transportType;
+		set => this.RaiseAndSetIfChanged(ref _transportType, value);
+	}
 
 	public ReactiveCommand<Unit, Unit> DiscoveryNatType { get; }
 
 	public RFC5780ViewModel()
 	{
-		Result5389 = new StunResult5389();
+		_result5389 = new StunResult5389();
 		DiscoveryNatType = ReactiveCommand.CreateFromTask(DiscoveryNatTypeAsync);
 	}
 
@@ -76,29 +90,43 @@ public class RFC5780ViewModel : ViewModelBase, IRoutableViewModel
 			}
 		}
 
-		using IUdpProxy proxy = ProxyFactory.CreateProxy(Config.ProxyType, Result5389.LocalEndPoint, socks5Option);
-
-		using StunClient5389UDP client = new(new IPEndPoint(serverIp, server.Port), Result5389.LocalEndPoint, proxy);
+		using IStunClient5389 client = TransportType is TransportType.Udp ?
+				new StunClient5389UDP(new IPEndPoint(serverIp, server.Port), Result5389.LocalEndPoint, ProxyFactory.CreateProxy(Config.ProxyType, Result5389.LocalEndPoint, socks5Option))
+				: new StunClient5389TCP(new IPEndPoint(serverIp, server.Port), Result5389.LocalEndPoint, ProxyFactory.CreateProxy(Config.ProxyType, socks5Option));
 
-		Result5389 = client.State;
-		using (Observable.Interval(TimeSpan.FromSeconds(0.1))
-				.ObserveOn(RxApp.MainThreadScheduler)
-				.Subscribe(_ => this.RaisePropertyChanged(nameof(Result5389))))
+		try
 		{
-			await client.ConnectProxyAsync(token);
-			try
+			using (Observable.Interval(TimeSpan.FromSeconds(0.1))
+					.ObserveOn(RxApp.MainThreadScheduler)
+					// ReSharper disable once AccessToDisposedClosure
+					.Subscribe(_ => Result5389 = client.State with { }))
 			{
-				await client.QueryAsync(token);
-			}
-			finally
-			{
-				await client.CloseProxyAsync(token);
+				if (client is IUdpStunClient udpClient)
+				{
+					await udpClient.ConnectProxyAsync(token);
+					try
+					{
+						await client.QueryAsync(token);
+					}
+					finally
+					{
+						await udpClient.CloseProxyAsync(token);
+					}
+				}
+				else
+				{
+					await client.QueryAsync(token);
+				}
 			}
 		}
+		finally
+		{
+			Result5389 = client.State with { };
+		}
+	}
 
+	public void ResetResult()
+	{
 		Result5389 = new StunResult5389();
-		Result5389.Clone(client.State);
-
-		this.RaisePropertyChanged(nameof(Result5389));
 	}
 }

+ 2 - 2
NatTypeTester/MainWindow.xaml

@@ -8,8 +8,8 @@
     xmlns:ui="http://schemas.modernwpf.com/2019"
     Title="NatTypeTester"
     WindowStartupLocation="CenterScreen"
-    Height="480" Width="500"
-    MinHeight="480" MinWidth="500"
+    Height="525" Width="500"
+    MinHeight="525" MinWidth="500"
     ui:WindowHelper.UseModernWindowStyle="True">
 
     <Grid>

+ 13 - 7
NatTypeTester/Views/RFC5780View.xaml

@@ -17,24 +17,31 @@
             <RowDefinition Height="Auto" />
             <RowDefinition Height="Auto" />
             <RowDefinition Height="Auto" />
+            <RowDefinition Height="Auto" />
             <RowDefinition />
         </Grid.RowDefinitions>
+        <Grid Margin="10,0" Grid.Row="0">
+            <ui:RadioButtons x:Name="TransportTypeRadioButtons" MaxColumns="4">
+                <RadioButton Content="UDP" />
+                <RadioButton Content="TCP" />
+            </ui:RadioButtons>
+        </Grid>
         <TextBox
-            x:Name="BindingTestTextBox" Grid.Row="0"
+            x:Name="BindingTestTextBox" Grid.Row="1"
             Margin="10,5" IsReadOnly="True"
             VerticalContentAlignment="Center" VerticalAlignment="Center"
             ui:ControlHelper.Header="Binding test" />
         <TextBox
-            x:Name="MappingBehaviorTextBox" Grid.Row="1"
+            x:Name="MappingBehaviorTextBox" Grid.Row="2"
             Margin="10,5" IsReadOnly="True"
             VerticalContentAlignment="Center" VerticalAlignment="Center"
             ui:ControlHelper.Header="Mapping behavior" />
         <TextBox
-            x:Name="FilteringBehaviorTextBox" Grid.Row="2"
+            x:Name="FilteringBehaviorTextBox" Grid.Row="3"
             Margin="10,5" IsReadOnly="True"
             VerticalContentAlignment="Center" VerticalAlignment="Center"
             ui:ControlHelper.Header="Filtering behavior" />
-        <ComboBox x:Name="LocalAddressComboBox" Grid.Row="3"
+        <ComboBox x:Name="LocalAddressComboBox" Grid.Row="4"
                   Margin="10,5"
                   IsEditable="True" HorizontalAlignment="Stretch"
                   VerticalContentAlignment="Center" VerticalAlignment="Center"
@@ -43,12 +50,11 @@
             <ComboBoxItem>[::]:0</ComboBoxItem>
         </ComboBox>
         <TextBox
-            x:Name="MappingAddressTextBox" Grid.Row="4"
+            x:Name="MappingAddressTextBox" Grid.Row="5"
             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" />
+        <Button x:Name="DiscoveryButton" Grid.Row="6" HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Test" Margin="0,10,10,10" />
     </Grid>
 </reactiveUi:ReactiveUserControl>

+ 9 - 1
NatTypeTester/Views/RFC5780View.xaml.cs

@@ -3,8 +3,10 @@ using NatTypeTester.Utils;
 using NatTypeTester.ViewModels;
 using ReactiveMarbles.ObservableEvents;
 using ReactiveUI;
+using STUN.Enums;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
+using System.Windows;
 using System.Windows.Input;
 using Volo.Abp.DependencyInjection;
 
@@ -21,6 +23,10 @@ public partial class RFC5780View : ITransientDependency
 
 		this.WhenActivated(d =>
 		{
+			this.Bind(ViewModel, vm => vm.TransportType, v => v.TransportTypeRadioButtons.SelectedIndex, type => (int)type, index => (TransportType)index).DisposeWith(d);
+			ViewModel.WhenAnyValue(vm => vm.TransportType).Subscribe(_ => ViewModel.ResetResult()).DisposeWith(d);
+			this.OneWayBind(ViewModel, vm => vm.TransportType, v => v.FilteringBehaviorTextBox.Visibility, type => type is TransportType.Udp ? Visibility.Visible : Visibility.Collapsed).DisposeWith(d);
+
 			this.OneWayBind(ViewModel, vm => vm.Result5389.BindingTestResult, v => v.BindingTestTextBox.Text).DisposeWith(d);
 
 			this.OneWayBind(ViewModel, vm => vm.Result5389.MappingBehavior, v => v.MappingBehaviorTextBox.Text).DisposeWith(d);
@@ -36,11 +42,13 @@ public partial class RFC5780View : ITransientDependency
 			this.BindCommand(ViewModel, vm => vm.DiscoveryNatType, v => v.DiscoveryButton).DisposeWith(d);
 
 			this.Events().KeyDown
-				.Where(x => x.Key == Key.Enter && DiscoveryButton.Command.CanExecute(default))
+				.Where(x => x.Key is Key.Enter && DiscoveryButton.Command.CanExecute(default))
 				.Subscribe(_ => DiscoveryButton.Command.Execute(default))
 				.DisposeWith(d);
 
 			ViewModel.DiscoveryNatType.ThrownExceptions.Subscribe(ex => _ = ex.HandleExceptionWithContentDialogAsync()).DisposeWith(d);
+
+			ViewModel.DiscoveryNatType.IsExecuting.Subscribe(b => TransportTypeRadioButtons.IsEnabled = !b).DisposeWith(d);
 		});
 	}
 }

+ 1 - 1
NatTypeTester/Views/SettingView.xaml

@@ -15,7 +15,7 @@
             <RowDefinition Height="Auto" />
             <RowDefinition Height="Auto" />
         </Grid.RowDefinitions>
-        <Grid Margin="10,0" Grid.Column="0">
+        <Grid Margin="10,0" Grid.Row="0">
             <ui:RadioButtons Header="Proxy" x:Name="ProxyRadioButtons">
                 <RadioButton Content="Don't use Proxy" />
                 <RadioButton Content="SOCKS5" />

+ 3 - 10
NatTypeTester/Views/SettingView.xaml.cs

@@ -23,16 +23,9 @@ public partial class SettingView : ITransientDependency
 
 			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 =>
-				{
-					ProxyType type = (ProxyType)index;
-					ProxyConfigGrid.IsEnabled = type is not ProxyType.Plain;
-					return type;
-				}).DisposeWith(d);
+			this.Bind(ViewModel, vm => vm.Config.ProxyType, v => v.ProxyRadioButtons.SelectedIndex, type => (int)type, index => (ProxyType)index).DisposeWith(d);
+
+			this.OneWayBind(ViewModel, vm => vm.Config.ProxyType, v => v.ProxyConfigGrid.IsEnabled, type => type is not ProxyType.Plain).DisposeWith(d);
 		});
 	}
 }

+ 0 - 2
STUN/Client/IStunClient.cs

@@ -2,7 +2,5 @@ namespace STUN.Client;
 
 public interface IStunClient : IDisposable
 {
-	ValueTask ConnectProxyAsync(CancellationToken cancellationToken = default);
-	ValueTask CloseProxyAsync(CancellationToken cancellationToken = default);
 	ValueTask QueryAsync(CancellationToken cancellationToken = default);
 }

+ 10 - 0
STUN/Client/IStunClient5389.cs

@@ -0,0 +1,10 @@
+using STUN.StunResult;
+
+namespace STUN.Client;
+
+public interface IStunClient5389 : IStunClient
+{
+	StunResult5389 State { get; }
+	ValueTask<StunResult5389> BindingTestAsync(CancellationToken cancellationToken = default);
+	ValueTask MappingBehaviorTestAsync(CancellationToken cancellationToken = default);
+}

+ 8 - 0
STUN/Client/IUdpStunClient.cs

@@ -0,0 +1,8 @@
+namespace STUN.Client;
+
+public interface IUdpStunClient : IStunClient
+{
+	TimeSpan ReceiveTimeout { get; set; }
+	ValueTask ConnectProxyAsync(CancellationToken cancellationToken = default);
+	ValueTask CloseProxyAsync(CancellationToken cancellationToken = default);
+}

+ 9 - 7
STUN/Client/StunClient3489.cs

@@ -14,7 +14,7 @@ namespace STUN.Client;
 /// <summary>
 /// https://tools.ietf.org/html/rfc3489#section-10.1
 /// </summary>
-public class StunClient3489 : IStunClient
+public class StunClient3489 : IUdpStunClient
 {
 	public virtual IPEndPoint LocalEndPoint => (IPEndPoint)_proxy.Client.LocalEndPoint!;
 
@@ -24,7 +24,7 @@ public class StunClient3489 : IStunClient
 
 	private readonly IUdpProxy _proxy;
 
-	public ClassicStunResult State { get; } = new();
+	public ClassicStunResult State { get; private set; } = new();
 
 	public StunClient3489(IPEndPoint server, IPEndPoint local, IUdpProxy? proxy = null)
 	{
@@ -53,7 +53,7 @@ public class StunClient3489 : IStunClient
 
 	public async ValueTask QueryAsync(CancellationToken cancellationToken = default)
 	{
-		State.Reset();
+		State = new ClassicStunResult();
 
 		// test I
 		StunResponse? response1 = await Test1Async(cancellationToken);
@@ -63,7 +63,7 @@ public class StunClient3489 : IStunClient
 			return;
 		}
 
-		State.LocalEndPoint = new IPEndPoint(response1.LocalAddress, LocalEndPoint.Port);
+		State.LocalEndPoint = response1.Local;
 
 		IPEndPoint? mappedAddress1 = response1.Message.GetMappedAddressAttribute();
 		IPEndPoint? changedAddress = response1.Message.GetChangedAddressAttribute();
@@ -95,7 +95,7 @@ public class StunClient3489 : IStunClient
 		}
 
 		// is Public IP == link's IP?
-		if (Equals(mappedAddress1.Address, response1.LocalAddress) && mappedAddress1.Port == LocalEndPoint.Port)
+		if (Equals(mappedAddress1, response1.Local))
 		{
 			// No NAT
 			if (response2 is null)
@@ -172,10 +172,10 @@ public class StunClient3489 : IStunClient
 			StunMessage5389 message = new();
 			if (message.TryParse(buffer[..r.ReceivedBytes]) && message.IsSameTransaction(sendMessage))
 			{
-				return new StunResponse(message, (IPEndPoint)r.RemoteEndPoint, r.PacketInformation.Address);
+				return new StunResponse(message, (IPEndPoint)r.RemoteEndPoint, new IPEndPoint(r.PacketInformation.Address, ((IPEndPoint)_proxy.Client.LocalEndPoint!).Port));
 			}
 		}
-		catch (Exception ex)
+		catch (OperationCanceledException ex)
 		{
 			Debug.WriteLine(ex);
 		}
@@ -227,5 +227,7 @@ public class StunClient3489 : IStunClient
 	public void Dispose()
 	{
 		_proxy.Dispose();
+
+		GC.SuppressFinalize(this);
 	}
 }

+ 236 - 0
STUN/Client/StunClient5389TCP.cs

@@ -0,0 +1,236 @@
+using Microsoft;
+using STUN.Enums;
+using STUN.Messages;
+using STUN.Proxy;
+using STUN.StunResult;
+using STUN.Utils;
+using System.Buffers;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO.Pipelines;
+using System.Net;
+
+namespace STUN.Client;
+
+public class StunClient5389TCP : IStunClient5389
+{
+	public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(3);
+
+	private readonly IPEndPoint _remoteEndPoint;
+	private readonly IPEndPoint _initLocalEndPoint;
+
+	private readonly ITcpProxy _proxy;
+
+	public StunResult5389 State { get; private set; } = new();
+
+	public StunClient5389TCP(IPEndPoint server, IPEndPoint local, ITcpProxy? proxy = default)
+	{
+		Requires.NotNull(server, nameof(server));
+		Requires.NotNull(local, nameof(local));
+
+		_proxy = proxy ?? new DirectTcpProxy();
+
+		_remoteEndPoint = server;
+
+		_initLocalEndPoint = local;
+		State.LocalEndPoint = local;
+	}
+
+	public async ValueTask QueryAsync(CancellationToken cancellationToken = default)
+	{
+		await MappingBehaviorTestAsync(cancellationToken);
+		State.FilteringBehavior = FilteringBehavior.None;
+	}
+
+	public async ValueTask MappingBehaviorTestAsync(CancellationToken cancellationToken = default)
+	{
+		State = new StunResult5389();
+
+		// test I
+		StunResult5389 bindingResult = await BindingTestAsync(cancellationToken);
+		State = bindingResult with { };
+		if (State.BindingTestResult is not BindingTestResult.Success)
+		{
+			return;
+		}
+
+		if (!HasValidOtherAddress(State.OtherEndPoint))
+		{
+			State.MappingBehavior = MappingBehavior.UnsupportedServer;
+			return;
+		}
+
+		if (Equals(State.PublicEndPoint, State.LocalEndPoint))
+		{
+			State.MappingBehavior = MappingBehavior.Direct; // or Endpoint-Independent
+			return;
+		}
+
+		// test II
+		StunResult5389 result2 = await MappingBehaviorTestBase2Async();
+		if (State.MappingBehavior is not MappingBehavior.Unknown)
+		{
+			return;
+		}
+
+		// test III
+		await MappingBehaviorTestBase3Async();
+
+		return;
+
+		bool HasValidOtherAddress([NotNullWhen(true)] IPEndPoint? other)
+		{
+			return other is not null && !Equals(other.Address, _remoteEndPoint.Address) && other.Port != _remoteEndPoint.Port;
+		}
+
+		async ValueTask<StunResult5389> MappingBehaviorTestBase2Async()
+		{
+			StunResult5389 result = await BindingTestBaseAsync(new IPEndPoint(State.OtherEndPoint.Address, _remoteEndPoint.Port), cancellationToken);
+
+			if (result.BindingTestResult is not BindingTestResult.Success)
+			{
+				State.MappingBehavior = MappingBehavior.Fail;
+			}
+			else if (Equals(result.PublicEndPoint, State.PublicEndPoint))
+			{
+				State.MappingBehavior = MappingBehavior.EndpointIndependent;
+			}
+			return result;
+		}
+
+		async ValueTask MappingBehaviorTestBase3Async()
+		{
+			StunResult5389 result3 = await BindingTestBaseAsync(State.OtherEndPoint, cancellationToken);
+			if (result3.BindingTestResult is not BindingTestResult.Success)
+			{
+				State.MappingBehavior = MappingBehavior.Fail;
+				return;
+			}
+
+			State.MappingBehavior = Equals(result3.PublicEndPoint, result2.PublicEndPoint) ? MappingBehavior.AddressDependent : MappingBehavior.AddressAndPortDependent;
+		}
+	}
+
+	public async ValueTask<StunResult5389> BindingTestAsync(CancellationToken cancellationToken = default)
+	{
+		return await BindingTestBaseAsync(_remoteEndPoint, cancellationToken);
+	}
+
+	protected virtual async ValueTask<StunResult5389> BindingTestBaseAsync(IPEndPoint remote, CancellationToken cancellationToken = default)
+	{
+		StunResult5389 result = new();
+		StunMessage5389 test = new()
+		{
+			StunMessageType = StunMessageType.BindingRequest
+		};
+		StunResponse? response1 = await RequestAsync(test, remote, cancellationToken);
+		IPEndPoint? mappedAddress1 = response1?.Message.GetXorMappedAddressAttribute();
+		IPEndPoint? otherAddress = response1?.Message.GetOtherAddressAttribute();
+
+		if (response1 is null)
+		{
+			result.BindingTestResult = BindingTestResult.Fail;
+		}
+		else if (mappedAddress1 is null)
+		{
+			result.BindingTestResult = BindingTestResult.UnsupportedServer;
+		}
+		else
+		{
+			result.BindingTestResult = BindingTestResult.Success;
+		}
+
+		IPEndPoint? local = response1?.Local;
+
+		result.LocalEndPoint = local;
+		result.PublicEndPoint = mappedAddress1;
+		result.OtherEndPoint = otherAddress;
+
+		return result;
+	}
+
+	private async ValueTask<StunResponse?> RequestAsync(StunMessage5389 sendMessage, IPEndPoint remote, CancellationToken cancellationToken)
+	{
+		try
+		{
+			using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+			cts.CancelAfter(ConnectTimeout);
+			IDuplexPipe pipe = await _proxy.ConnectAsync(_initLocalEndPoint, remote, cts.Token);
+			try
+			{
+				_initLocalEndPoint.Port = default;
+
+				using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(sendMessage.Length);
+				Memory<byte> buffer = memoryOwner.Memory;
+				int length = sendMessage.WriteTo(buffer.Span);
+
+				await pipe.Output.WriteAsync(buffer[..length], cancellationToken);
+
+				StunMessage5389 message = new();
+				bool success = await ReadPipeAsync(message, pipe.Input);
+
+				if (success && message.IsSameTransaction(sendMessage))
+				{
+					IPEndPoint? local = _proxy.CurrentLocalEndPoint;
+					if (local is not null)
+					{
+						return new StunResponse(message, remote, local);
+					}
+				}
+			}
+			finally
+			{
+				await _proxy.CloseAsync(cancellationToken);
+			}
+		}
+		catch (OperationCanceledException ex)
+		{
+			Debug.WriteLine(ex);
+		}
+
+		return default;
+
+		async ValueTask<bool> ReadPipeAsync(StunMessage5389 message, PipeReader reader)
+		{
+			try
+			{
+				while (true)
+				{
+					cancellationToken.ThrowIfCancellationRequested();
+
+					ReadResult result = await reader.ReadAsync(cancellationToken);
+					ReadOnlySequence<byte> buffer = result.Buffer;
+					try
+					{
+						if (message.TryParse(ref buffer))
+						{
+							return true;
+						}
+
+						if (result.IsCompleted)
+						{
+							break;
+						}
+					}
+					finally
+					{
+						reader.AdvanceTo(buffer.Start, buffer.End);
+					}
+				}
+
+				return false;
+			}
+			finally
+			{
+				await reader.CompleteAsync();
+			}
+		}
+	}
+
+	public void Dispose()
+	{
+		_proxy.Dispose();
+
+		GC.SuppressFinalize(this);
+	}
+}

+ 15 - 17
STUN/Client/StunClient5389UDP.cs

@@ -17,19 +17,17 @@ namespace STUN.Client;
 /// https://tools.ietf.org/html/rfc5389#section-7.2.1
 /// https://tools.ietf.org/html/rfc5780#section-4.2
 /// </summary>
-public class StunClient5389UDP : IStunClient
+public class StunClient5389UDP : IStunClient5389, IUdpStunClient
 {
-	public virtual IPEndPoint LocalEndPoint => (IPEndPoint)_proxy.Client.LocalEndPoint!;
-
 	public TimeSpan ReceiveTimeout { get; set; } = TimeSpan.FromSeconds(3);
 
 	private readonly IPEndPoint _remoteEndPoint;
 
 	private readonly IUdpProxy _proxy;
 
-	public StunResult5389 State { get; } = new();
+	public StunResult5389 State { get; private set; } = new();
 
-	public StunClient5389UDP(IPEndPoint server, IPEndPoint local, IUdpProxy? proxy = null)
+	public StunClient5389UDP(IPEndPoint server, IPEndPoint local, IUdpProxy? proxy = default)
 	{
 		Requires.NotNull(server, nameof(server));
 		Requires.NotNull(local, nameof(local));
@@ -56,7 +54,7 @@ public class StunClient5389UDP : IStunClient
 
 	public async ValueTask QueryAsync(CancellationToken cancellationToken = default)
 	{
-		State.Reset();
+		State = new StunResult5389();
 
 		await FilteringBehaviorTestBaseAsync(cancellationToken);
 		if (State.BindingTestResult is not BindingTestResult.Success
@@ -112,7 +110,7 @@ public class StunClient5389UDP : IStunClient
 			result.BindingTestResult = BindingTestResult.Success;
 		}
 
-		IPEndPoint? local = response1 is null ? null : new IPEndPoint(response1.LocalAddress, LocalEndPoint.Port);
+		IPEndPoint? local = response1?.Local;
 
 		result.LocalEndPoint = local;
 		result.PublicEndPoint = mappedAddress1;
@@ -123,11 +121,11 @@ public class StunClient5389UDP : IStunClient
 
 	public async ValueTask MappingBehaviorTestAsync(CancellationToken cancellationToken = default)
 	{
-		State.Reset();
+		State = new StunResult5389();
 
 		// test I
 		StunResult5389 bindingResult = await BindingTestAsync(cancellationToken);
-		State.Clone(bindingResult);
+		State = bindingResult with { };
 		if (State.BindingTestResult is not BindingTestResult.Success)
 		{
 			return;
@@ -190,7 +188,7 @@ public class StunClient5389UDP : IStunClient
 
 	public async ValueTask FilteringBehaviorTestAsync(CancellationToken cancellationToken = default)
 	{
-		State.Reset();
+		State = new StunResult5389();
 		await FilteringBehaviorTestBaseAsync(cancellationToken);
 	}
 
@@ -198,7 +196,7 @@ public class StunClient5389UDP : IStunClient
 	{
 		// test I
 		StunResult5389 bindingResult = await BindingTestAsync(cancellationToken);
-		State.Clone(bindingResult);
+		State = bindingResult with { };
 		if (State.BindingTestResult is not BindingTestResult.Success)
 		{
 			return;
@@ -248,7 +246,7 @@ public class StunClient5389UDP : IStunClient
 		return await RequestAsync(message, _remoteEndPoint, State.OtherEndPoint, cancellationToken);
 	}
 
-	public virtual async ValueTask<StunResponse?> FilteringBehaviorTest3Async(CancellationToken cancellationToken = default)
+	protected virtual async ValueTask<StunResponse?> FilteringBehaviorTest3Async(CancellationToken cancellationToken = default)
 	{
 		Assumes.NotNull(State.OtherEndPoint);
 
@@ -263,9 +261,7 @@ public class StunClient5389UDP : IStunClient
 	[MethodImpl(MethodImplOptions.AggressiveInlining)]
 	private bool HasValidOtherAddress([NotNullWhen(true)] IPEndPoint? other)
 	{
-		return other is not null
-			   && !Equals(other.Address, _remoteEndPoint.Address)
-			   && other.Port != _remoteEndPoint.Port;
+		return other is not null && !Equals(other.Address, _remoteEndPoint.Address) && other.Port != _remoteEndPoint.Port;
 	}
 
 	private async ValueTask<StunResponse?> RequestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken cancellationToken)
@@ -285,10 +281,10 @@ public class StunClient5389UDP : IStunClient
 			StunMessage5389 message = new();
 			if (message.TryParse(buffer[..r.ReceivedBytes]) && message.IsSameTransaction(sendMessage))
 			{
-				return new StunResponse(message, (IPEndPoint)r.RemoteEndPoint, r.PacketInformation.Address);
+				return new StunResponse(message, (IPEndPoint)r.RemoteEndPoint, new IPEndPoint(r.PacketInformation.Address, ((IPEndPoint)_proxy.Client.LocalEndPoint!).Port));
 			}
 		}
-		catch (Exception ex)
+		catch (OperationCanceledException ex)
 		{
 			Debug.WriteLine(ex);
 		}
@@ -298,5 +294,7 @@ public class StunClient5389UDP : IStunClient
 	public void Dispose()
 	{
 		_proxy.Dispose();
+
+		GC.SuppressFinalize(this);
 	}
 }

+ 0 - 1
STUN/Enums/TransportType.cs

@@ -1,5 +1,4 @@
 namespace STUN.Enums;
-// Only UDP is supported
 
 public enum TransportType
 {

+ 6 - 3
STUN/Messages/StunMessage5389.cs

@@ -18,7 +18,7 @@ public class StunMessage5389
 	private const int SizeOfLength = sizeof(ushort);
 	private const int SizeOfMagicCookie = sizeof(uint);
 	private const int SizeOfTransactionId = 12;
-	private const int HeaderLength = SizeOfMessageType + SizeOfLength + SizeOfMagicCookie + SizeOfTransactionId;
+	public const int HeaderLength = SizeOfMessageType + SizeOfLength + SizeOfMagicCookie + SizeOfTransactionId;
 
 	public StunMessageType StunMessageType { get; set; }
 
@@ -30,6 +30,9 @@ public class StunMessage5389
 
 	public IEnumerable<StunAttribute> Attributes { get; set; }
 
+	public ushort MessageLength => (ushort)Attributes.Sum(x => x.RealLength);
+	public int Length => HeaderLength + MessageLength;
+
 	public StunMessage5389()
 	{
 		Attributes = Array.Empty<StunAttribute>();
@@ -41,8 +44,8 @@ public class StunMessage5389
 
 	public int WriteTo(Span<byte> buffer)
 	{
-		ushort messageLength = (ushort)Attributes.Sum(x => x.RealLength);
-		int length = HeaderLength + messageLength;
+		ushort messageLength = MessageLength;
+		int length = Length;
 		Requires.Range(buffer.Length >= length, nameof(buffer));
 
 		BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)StunMessageType);

+ 4 - 11
STUN/Messages/StunResponse.cs

@@ -2,16 +2,9 @@ using System.Net;
 
 namespace STUN.Messages;
 
-public class StunResponse
+public record StunResponse(StunMessage5389 Message, IPEndPoint Remote, IPEndPoint Local)
 {
-	public StunMessage5389 Message { get; set; }
-	public IPEndPoint Remote { get; set; }
-	public IPAddress LocalAddress { get; set; }
-
-	public StunResponse(StunMessage5389 message, IPEndPoint remote, IPAddress localAddress)
-	{
-		Message = message;
-		Remote = remote;
-		LocalAddress = localAddress;
-	}
+	public StunMessage5389 Message { get; set; } = Message;
+	public IPEndPoint Remote { get; set; } = Remote;
+	public IPEndPoint Local { get; set; } = Local;
 }

+ 72 - 0
STUN/Proxy/DirectTcpProxy.cs

@@ -0,0 +1,72 @@
+using Microsoft;
+using Pipelines.Extensions;
+using System.IO.Pipelines;
+using System.Net;
+using System.Net.Sockets;
+
+namespace STUN.Proxy;
+
+public class DirectTcpProxy : ITcpProxy, IDisposableObservable
+{
+	public IPEndPoint? CurrentLocalEndPoint
+	{
+		get
+		{
+			Verify.NotDisposed(this);
+			return _tcpClient?.Client.LocalEndPoint as IPEndPoint;
+		}
+	}
+
+	private TcpClient? _tcpClient;
+
+	public async ValueTask<IDuplexPipe> ConnectAsync(IPEndPoint local, IPEndPoint dst, CancellationToken cancellationToken = default)
+	{
+		Verify.NotDisposed(this);
+		Requires.NotNull(local, nameof(local));
+		Requires.NotNull(dst, nameof(dst));
+
+		await CloseAsync(cancellationToken);
+
+		_tcpClient = new TcpClient(local) { NoDelay = true };
+		await _tcpClient.ConnectAsync(dst, cancellationToken);
+
+		return _tcpClient.Client.AsDuplexPipe();
+	}
+
+	public ValueTask CloseAsync(CancellationToken cancellationToken = default)
+	{
+		Verify.NotDisposed(this);
+
+		if (_tcpClient is not null)
+		{
+			CloseClient();
+			_tcpClient = default;
+		}
+
+		return default;
+	}
+
+	private void CloseClient()
+	{
+		if (_tcpClient is null)
+		{
+			return;
+		}
+
+		_tcpClient.Client.Close(0);
+		_tcpClient.Dispose();
+	}
+
+	public bool IsDisposed { get; private set; }
+
+	public void Dispose()
+	{
+		Verify.NotDisposed(this);
+
+		IsDisposed = true;
+
+		CloseClient();
+
+		GC.SuppressFinalize(this);
+	}
+}

+ 12 - 0
STUN/Proxy/ITcpProxy.cs

@@ -0,0 +1,12 @@
+using System.IO.Pipelines;
+using System.Net;
+
+namespace STUN.Proxy;
+
+public interface ITcpProxy : IDisposable
+{
+	IPEndPoint? CurrentLocalEndPoint { get; }
+
+	ValueTask<IDuplexPipe> ConnectAsync(IPEndPoint local, IPEndPoint dst, CancellationToken cancellationToken = default);
+	ValueTask CloseAsync(CancellationToken cancellationToken = default);
+}

+ 21 - 0
STUN/Proxy/ProxyFactory.cs

@@ -27,4 +27,25 @@ public static class ProxyFactory
 			}
 		}
 	}
+
+	public static ITcpProxy CreateProxy(ProxyType type, Socks5CreateOption option)
+	{
+		switch (type)
+		{
+			case ProxyType.Plain:
+			{
+				return new DirectTcpProxy();
+			}
+			case ProxyType.Socks5:
+			{
+				Requires.NotNull(option, nameof(option));
+				Requires.Argument(option.Address is not null, nameof(option), @"Proxy server is null");
+				return new Socks5TcpProxy(option);
+			}
+			default:
+			{
+				throw Assumes.NotReachable();
+			}
+		}
+	}
 }

+ 90 - 0
STUN/Proxy/Socks5TcpProxy.cs

@@ -0,0 +1,90 @@
+using Microsoft;
+using Socks5.Clients;
+using Socks5.Models;
+using System.IO.Pipelines;
+using System.Net;
+using System.Net.Sockets;
+using System.Reflection;
+
+namespace STUN.Proxy;
+
+public class Socks5TcpProxy : ITcpProxy, IDisposableObservable
+{
+	public IPEndPoint? CurrentLocalEndPoint
+	{
+		get
+		{
+			Verify.NotDisposed(this);
+			return GetTcpClient()?.Client.LocalEndPoint as IPEndPoint;
+		}
+	}
+
+	private readonly Socks5CreateOption _socks5Options;
+
+	private Socks5Client? _socks5Client;
+
+	public Socks5TcpProxy(Socks5CreateOption socks5Options)
+	{
+		Requires.NotNull(socks5Options, nameof(socks5Options));
+		Requires.Argument(socks5Options.Address is not null, nameof(socks5Options), @"SOCKS5 address is null");
+
+		_socks5Options = socks5Options;
+	}
+
+	public async ValueTask<IDuplexPipe> ConnectAsync(IPEndPoint local, IPEndPoint dst, CancellationToken cancellationToken = default)
+	{
+		Verify.NotDisposed(this);
+		Requires.NotNull(local, nameof(local));
+		Requires.NotNull(dst, nameof(dst));
+
+		await CloseAsync(cancellationToken);
+
+		_socks5Client = new Socks5Client(_socks5Options);
+
+		GetTcpClient()?.Client.Bind(local);
+
+		await _socks5Client.ConnectAsync(dst.Address, (ushort)dst.Port, cancellationToken);
+
+		return _socks5Client.GetPipe();
+	}
+
+	public ValueTask CloseAsync(CancellationToken cancellationToken = default)
+	{
+		Verify.NotDisposed(this);
+
+		CloseClient();
+
+		return default;
+	}
+
+	private TcpClient? GetTcpClient()
+	{
+		// TODO
+		return _socks5Client?.GetType().GetField(@"_tcpClient", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(_socks5Client) as TcpClient;
+	}
+
+	private void CloseClient()
+	{
+		if (_socks5Client is null)
+		{
+			return;
+		}
+
+		GetTcpClient()?.Client.Close(0);
+		_socks5Client.Dispose();
+		_socks5Client = default;
+	}
+
+	public bool IsDisposed { get; private set; }
+
+	public void Dispose()
+	{
+		Verify.NotDisposed(this);
+
+		IsDisposed = true;
+
+		CloseClient();
+
+		GC.SuppressFinalize(this);
+	}
+}

+ 0 - 1
STUN/Proxy/Socks5UdpProxy.cs

@@ -30,7 +30,6 @@ public class Socks5UdpProxy : IUdpProxy
 
 	public Socks5UdpProxy(IPEndPoint localEndPoint, Socks5CreateOption socks5Options)
 	{
-		_socks5Options = socks5Options;
 		Requires.NotNull(localEndPoint, nameof(localEndPoint));
 		Requires.NotNull(socks5Options, nameof(socks5Options));
 		Requires.Argument(socks5Options.Address is not null, nameof(socks5Options), @"SOCKS5 address is null");

+ 1 - 14
STUN/StunResult/ClassicStunResult.cs

@@ -2,20 +2,7 @@ using STUN.Enums;
 
 namespace STUN.StunResult;
 
-public class ClassicStunResult : StunResult
+public record ClassicStunResult : StunResult
 {
 	public NatType NatType { get; set; } = NatType.Unknown;
-
-	public void Clone(ClassicStunResult result)
-	{
-		PublicEndPoint = result.PublicEndPoint;
-		LocalEndPoint = result.LocalEndPoint;
-		NatType = result.NatType;
-	}
-
-	public override void Reset()
-	{
-		base.Reset();
-		NatType = NatType.Unknown;
-	}
 }

+ 1 - 7
STUN/StunResult/StunResult.cs

@@ -2,14 +2,8 @@ using System.Net;
 
 namespace STUN.StunResult;
 
-public abstract class StunResult
+public abstract record StunResult
 {
 	public IPEndPoint? PublicEndPoint { get; set; }
 	public IPEndPoint? LocalEndPoint { get; set; }
-
-	public virtual void Reset()
-	{
-		PublicEndPoint = default;
-		LocalEndPoint = default;
-	}
 }

+ 1 - 20
STUN/StunResult/StunResult5389.cs

@@ -3,7 +3,7 @@ using System.Net;
 
 namespace STUN.StunResult;
 
-public class StunResult5389 : StunResult
+public record StunResult5389 : StunResult
 {
 	public IPEndPoint? OtherEndPoint { get; set; }
 
@@ -12,23 +12,4 @@ public class StunResult5389 : StunResult
 	public MappingBehavior MappingBehavior { get; set; } = MappingBehavior.Unknown;
 
 	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 override void Reset()
-	{
-		base.Reset();
-		OtherEndPoint = default;
-		BindingTestResult = BindingTestResult.Unknown;
-		MappingBehavior = MappingBehavior.Unknown;
-		FilteringBehavior = FilteringBehavior.Unknown;
-	}
 }

+ 11 - 10
UnitTest/StunClien5389UDPTest.cs

@@ -2,6 +2,7 @@ using Dns.Net.Abstractions;
 using Dns.Net.Clients;
 using Microsoft.VisualStudio.TestTools.UnitTesting;
 using Moq;
+using Moq.Protected;
 using STUN.Client;
 using STUN.Enums;
 using STUN.Messages;
@@ -408,7 +409,7 @@ public class StunClien5389UDPTest
 			LocalEndPoint = LocalAddress1,
 			OtherEndPoint = ChangedAddress1
 		};
-		StunResponse r2 = new(DefaultStunMessage, ChangedAddress1, LocalAddress1.Address);
+		StunResponse r2 = new(DefaultStunMessage, ChangedAddress1, LocalAddress1);
 		mock.Setup(x => x.BindingTestBaseAsync(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(r1);
 		mock.Setup(x => x.FilteringBehaviorTest2Async(It.IsAny<CancellationToken>())).ReturnsAsync(r2);
 
@@ -435,7 +436,7 @@ public class StunClien5389UDPTest
 			LocalEndPoint = LocalAddress1,
 			OtherEndPoint = ChangedAddress1
 		};
-		StunResponse r2 = new(DefaultStunMessage, ServerAddress, LocalAddress1.Address);
+		StunResponse r2 = new(DefaultStunMessage, ServerAddress, LocalAddress1);
 		mock.Setup(x => x.BindingTestBaseAsync(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(r1);
 		mock.Setup(x => x.FilteringBehaviorTest2Async(It.IsAny<CancellationToken>())).ReturnsAsync(r2);
 
@@ -464,7 +465,7 @@ public class StunClien5389UDPTest
 		};
 		mock.Setup(x => x.BindingTestBaseAsync(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(r1);
 		mock.Setup(x => x.FilteringBehaviorTest2Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
-		mock.Setup(x => x.FilteringBehaviorTest3Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
+		mock.Protected().Setup<ValueTask<StunResponse?>>(@"FilteringBehaviorTest3Async", It.IsAny<CancellationToken>()).ReturnsAsync(default(StunResponse?));
 
 		await client.FilteringBehaviorTestAsync();
 
@@ -489,10 +490,10 @@ public class StunClien5389UDPTest
 			LocalEndPoint = LocalAddress1,
 			OtherEndPoint = ChangedAddress1
 		};
-		StunResponse r3 = new(DefaultStunMessage, ChangedAddress2, LocalAddress1.Address);
+		StunResponse r3 = new(DefaultStunMessage, ChangedAddress2, LocalAddress1);
 		mock.Setup(x => x.BindingTestBaseAsync(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(r1);
 		mock.Setup(x => x.FilteringBehaviorTest2Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
-		mock.Setup(x => x.FilteringBehaviorTest3Async(It.IsAny<CancellationToken>())).ReturnsAsync(r3);
+		mock.Protected().Setup<ValueTask<StunResponse?>>(@"FilteringBehaviorTest3Async", It.IsAny<CancellationToken>()).ReturnsAsync(r3);
 
 		await client.FilteringBehaviorTestAsync();
 
@@ -517,10 +518,10 @@ public class StunClien5389UDPTest
 			LocalEndPoint = LocalAddress1,
 			OtherEndPoint = ChangedAddress1
 		};
-		StunResponse r3 = new(DefaultStunMessage, ServerAddress, LocalAddress1.Address);
+		StunResponse r3 = new(DefaultStunMessage, ServerAddress, LocalAddress1);
 		mock.Setup(x => x.BindingTestBaseAsync(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(r1);
 		mock.Setup(x => x.FilteringBehaviorTest2Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
-		mock.Setup(x => x.FilteringBehaviorTest3Async(It.IsAny<CancellationToken>())).ReturnsAsync(r3);
+		mock.Protected().Setup<ValueTask<StunResponse?>>(@"FilteringBehaviorTest3Async", It.IsAny<CancellationToken>()).ReturnsAsync(r3);
 
 		await client.FilteringBehaviorTestAsync();
 
@@ -591,7 +592,7 @@ public class StunClien5389UDPTest
 		};
 		mock.Setup(x => x.BindingTestBaseAsync(It.Is<IPEndPoint>(p => Equals(p, ServerAddress)), It.IsAny<CancellationToken>())).ReturnsAsync(r1);
 		mock.Setup(x => x.FilteringBehaviorTest2Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
-		mock.Setup(x => x.FilteringBehaviorTest3Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
+		mock.Protected().Setup<ValueTask<StunResponse?>>(@"FilteringBehaviorTest3Async", It.IsAny<CancellationToken>()).ReturnsAsync(default(StunResponse?));
 
 		await client.QueryAsync();
 
@@ -618,7 +619,7 @@ public class StunClien5389UDPTest
 		};
 		mock.Setup(x => x.BindingTestBaseAsync(It.IsAny<IPEndPoint>(), It.IsAny<CancellationToken>())).ReturnsAsync(r1);
 		mock.Setup(x => x.FilteringBehaviorTest2Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
-		mock.Setup(x => x.FilteringBehaviorTest3Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
+		mock.Protected().Setup<ValueTask<StunResponse?>>(@"FilteringBehaviorTest3Async", It.IsAny<CancellationToken>()).ReturnsAsync(default(StunResponse?));
 
 		await client.QueryAsync();
 
@@ -661,7 +662,7 @@ public class StunClien5389UDPTest
 		mock.Setup(x => x.BindingTestBaseAsync(It.Is<IPEndPoint>(p => Equals(p, ChangedAddress3)), It.IsAny<CancellationToken>())).ReturnsAsync(r2);
 		mock.Setup(x => x.BindingTestBaseAsync(It.Is<IPEndPoint>(p => Equals(p, ChangedAddress1)), It.IsAny<CancellationToken>())).ReturnsAsync(r3);
 		mock.Setup(x => x.FilteringBehaviorTest2Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
-		mock.Setup(x => x.FilteringBehaviorTest3Async(It.IsAny<CancellationToken>())).ReturnsAsync(default(StunResponse?));
+		mock.Protected().Setup<ValueTask<StunResponse?>>(@"FilteringBehaviorTest3Async", It.IsAny<CancellationToken>()).ReturnsAsync(default(StunResponse?));
 
 		await client.QueryAsync();
 

+ 17 - 17
UnitTest/StunClient3489Test.cs

@@ -50,7 +50,7 @@ public class StunClient3489Test
 		StunClient3489? client = mock.Object;
 
 		mock.Setup(x => x.LocalEndPoint).Returns(LocalAddress1);
-		StunResponse unknownResponse = new(DefaultStunMessage, Any, LocalAddress1.Address);
+		StunResponse unknownResponse = new(DefaultStunMessage, Any, LocalAddress1);
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(unknownResponse);
 		await TestAsync();
 
@@ -60,7 +60,7 @@ public class StunClient3489Test
 			{
 				BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port)
 			}
-		}, ServerAddress, LocalAddress1.Address);
+		}, ServerAddress, LocalAddress1);
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r1);
 		await TestAsync();
 
@@ -70,7 +70,7 @@ public class StunClient3489Test
 			{
 				BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ChangedAddress1.Port)
 			}
-		}, ServerAddress, LocalAddress1.Address);
+		}, ServerAddress, LocalAddress1);
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r2);
 		await TestAsync();
 
@@ -81,7 +81,7 @@ public class StunClient3489Test
 				BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
 				BuildChangeAddress(IpFamily.IPv4, ServerAddress.Address, (ushort)ChangedAddress1.Port)
 			}
-		}, ServerAddress, LocalAddress1.Address);
+		}, ServerAddress, LocalAddress1);
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r3);
 		await TestAsync();
 
@@ -92,7 +92,7 @@ public class StunClient3489Test
 				BuildMapping(IpFamily.IPv4, MappedAddress1.Address, (ushort)MappedAddress1.Port),
 				BuildChangeAddress(IpFamily.IPv4, ChangedAddress1.Address, (ushort)ServerAddress.Port)
 			}
-		}, ServerAddress, LocalAddress1.Address);
+		}, ServerAddress, LocalAddress1);
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(r4);
 		await TestAsync();
 
@@ -119,7 +119,7 @@ public class StunClient3489Test
 				}
 			},
 			ServerAddress,
-			MappedAddress1.Address
+			MappedAddress1
 		);
 		StunResponse test2Response = new(
 			new StunMessage5389
@@ -130,7 +130,7 @@ public class StunClient3489Test
 				}
 			},
 			ChangedAddress1,
-			MappedAddress1.Address
+			MappedAddress1
 		);
 
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(openInternetTest1Response);
@@ -163,7 +163,7 @@ public class StunClient3489Test
 				}
 			},
 			ServerAddress,
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		StunResponse fullConeResponse = new(
 			new StunMessage5389
@@ -174,7 +174,7 @@ public class StunClient3489Test
 				}
 			},
 			ChangedAddress1,
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		StunResponse unsupportedResponse1 = new(
 			new StunMessage5389
@@ -185,7 +185,7 @@ public class StunClient3489Test
 				}
 			},
 			ServerAddress,
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		StunResponse unsupportedResponse2 = new(
 			new StunMessage5389
@@ -196,7 +196,7 @@ public class StunClient3489Test
 				}
 			},
 			new IPEndPoint(ServerAddress.Address, ChangedAddress1.Port),
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		StunResponse unsupportedResponse3 = new(
 			new StunMessage5389
@@ -207,7 +207,7 @@ public class StunClient3489Test
 				}
 			},
 			new IPEndPoint(ChangedAddress1.Address, ServerAddress.Port),
-			LocalAddress1.Address
+			LocalAddress1
 		);
 
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(test1Response);
@@ -250,7 +250,7 @@ public class StunClient3489Test
 				}
 			},
 			ServerAddress,
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		StunResponse test12Response = new(
 			new StunMessage5389
@@ -262,7 +262,7 @@ public class StunClient3489Test
 				}
 			},
 			ServerAddress,
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(test1Response);
 		mock.Setup(x => x.LocalEndPoint).Returns(LocalAddress1);
@@ -295,7 +295,7 @@ public class StunClient3489Test
 				}
 			},
 			ServerAddress,
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		StunResponse test3Response = new(
 			new StunMessage5389
@@ -307,7 +307,7 @@ public class StunClient3489Test
 				}
 			},
 			ChangedAddress2,
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		StunResponse test3ErrorResponse = new(
 			new StunMessage5389
@@ -319,7 +319,7 @@ public class StunClient3489Test
 				}
 			},
 			ServerAddress,
-			LocalAddress1.Address
+			LocalAddress1
 		);
 		mock.Setup(x => x.Test1Async(It.IsAny<CancellationToken>())).ReturnsAsync(test1Response);
 		mock.Setup(x => x.LocalEndPoint).Returns(LocalAddress1);

+ 52 - 0
UnitTest/StunClient5389TCPTest.cs

@@ -0,0 +1,52 @@
+using Dns.Net.Abstractions;
+using Dns.Net.Clients;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using STUN.Client;
+using STUN.Enums;
+using STUN.StunResult;
+using System.Net;
+
+namespace UnitTest;
+
+[TestClass]
+public class StunClient5389TCPTest
+{
+	private readonly IDnsClient _dnsClient = new DefaultDnsClient();
+
+	private const string Server = @"stunserver.stunprotocol.org";
+	private const ushort Port = 3478;
+
+	private static readonly IPEndPoint Any = new(IPAddress.Any, 0);
+
+	[TestMethod]
+	public async Task BindingTestSuccessAsync()
+	{
+		IPAddress ip = await _dnsClient.QueryAsync(Server);
+		using IStunClient5389 client = new StunClient5389TCP(new IPEndPoint(ip, Port), Any);
+
+		StunResult5389 response = await client.BindingTestAsync();
+
+		Assert.AreEqual(BindingTestResult.Success, response.BindingTestResult);
+		Assert.AreEqual(MappingBehavior.Unknown, response.MappingBehavior);
+		Assert.AreEqual(FilteringBehavior.Unknown, response.FilteringBehavior);
+		Assert.IsNotNull(response.PublicEndPoint);
+		Assert.IsNotNull(response.LocalEndPoint);
+		Assert.IsNotNull(response.OtherEndPoint);
+	}
+
+	[TestMethod]
+	public async Task BindingTestFailAsync()
+	{
+		IPAddress ip = IPAddress.Parse(@"1.1.1.1");
+		using IStunClient5389 client = new StunClient5389TCP(new IPEndPoint(ip, Port), Any);
+
+		StunResult5389 response = await client.BindingTestAsync();
+
+		Assert.AreEqual(BindingTestResult.Fail, response.BindingTestResult);
+		Assert.AreEqual(MappingBehavior.Unknown, response.MappingBehavior);
+		Assert.AreEqual(FilteringBehavior.Unknown, response.FilteringBehavior);
+		Assert.IsNull(response.PublicEndPoint);
+		Assert.IsNull(response.LocalEndPoint);
+		Assert.IsNull(response.OtherEndPoint);
+	}
+}