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.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; namespace STUN.Client; /// /// https://tools.ietf.org/html/rfc5389#section-7.2.1 /// https://tools.ietf.org/html/rfc5780#section-4.2 /// public class StunClient5389UDP : IStunClient { 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 StunClient5389UDP(IPEndPoint server, IPEndPoint local, IUdpProxy? proxy = null) { Requires.NotNull(server, nameof(server)); Requires.NotNull(local, nameof(local)); _proxy = proxy ?? new NoneUdpProxy(local); _remoteEndPoint = server; State.LocalEndPoint = local; } public async ValueTask ConnectProxyAsync(CancellationToken cancellationToken = default) { using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(ReceiveTimeout); await _proxy.ConnectAsync(cts.Token); } public async ValueTask CloseProxyAsync(CancellationToken cancellationToken = default) { await _proxy.CloseAsync(cancellationToken); } public async ValueTask QueryAsync(CancellationToken cancellationToken = default) { State.Reset(); await FilteringBehaviorTestBaseAsync(cancellationToken); if (State.BindingTestResult is not BindingTestResult.Success || State.FilteringBehavior is FilteringBehavior.UnsupportedServer ) { return; } if (Equals(State.PublicEndPoint, State.LocalEndPoint)) { State.MappingBehavior = MappingBehavior.Direct; return; } // MappingBehaviorTest test II StunResult5389 result2 = await MappingBehaviorTestBase2Async(cancellationToken); if (State.MappingBehavior is not MappingBehavior.Unknown) { return; } // MappingBehaviorTest test III await MappingBehaviorTestBase3Async(result2, cancellationToken); } public async ValueTask BindingTestAsync(CancellationToken cancellationToken = default) { return await BindingTestBaseAsync(_remoteEndPoint, cancellationToken); } public virtual async ValueTask BindingTestBaseAsync(IPEndPoint remote, CancellationToken cancellationToken = default) { StunResult5389 result = new(); StunMessage5389 test = new() { StunMessageType = StunMessageType.BindingRequest }; StunResponse? response1 = await RequestAsync(test, remote, 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 is null ? null : new IPEndPoint(response1.LocalAddress, LocalEndPoint.Port); result.LocalEndPoint = local; result.PublicEndPoint = mappedAddress1; result.OtherEndPoint = otherAddress; return result; } public async ValueTask MappingBehaviorTestAsync(CancellationToken cancellationToken = default) { State.Reset(); // test I StunResult5389 bindingResult = await BindingTestAsync(cancellationToken); State.Clone(bindingResult); 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(cancellationToken); if (State.MappingBehavior is not MappingBehavior.Unknown) { return; } // test III await MappingBehaviorTestBase3Async(result2, cancellationToken); } private async ValueTask MappingBehaviorTestBase2Async(CancellationToken cancellationToken) { Verify.Operation(State.OtherEndPoint is not null, @"OTHER-ADDRESS is not returned"); StunResult5389 result2 = await BindingTestBaseAsync(new IPEndPoint(State.OtherEndPoint.Address, _remoteEndPoint.Port), cancellationToken); if (result2.BindingTestResult is not BindingTestResult.Success) { State.MappingBehavior = MappingBehavior.Fail; } else if (Equals(result2.PublicEndPoint, State.PublicEndPoint)) { State.MappingBehavior = MappingBehavior.EndpointIndependent; } return result2; } private async ValueTask MappingBehaviorTestBase3Async(StunResult5389 result2, CancellationToken cancellationToken) { Verify.Operation(State.OtherEndPoint is not null, @"OTHER-ADDRESS is not returned"); 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 FilteringBehaviorTestAsync(CancellationToken cancellationToken = default) { State.Reset(); await FilteringBehaviorTestBaseAsync(cancellationToken); } private async ValueTask FilteringBehaviorTestBaseAsync(CancellationToken cancellationToken) { // test I StunResult5389 bindingResult = await BindingTestAsync(cancellationToken); State.Clone(bindingResult); if (State.BindingTestResult is not BindingTestResult.Success) { return; } if (!HasValidOtherAddress(State.OtherEndPoint)) { State.FilteringBehavior = FilteringBehavior.UnsupportedServer; return; } // test II StunResponse? response2 = await FilteringBehaviorTest2Async(cancellationToken); if (response2 is not null) { State.FilteringBehavior = Equals(response2.Remote, State.OtherEndPoint) ? FilteringBehavior.EndpointIndependent : FilteringBehavior.UnsupportedServer; return; } // test III StunResponse? response3 = await FilteringBehaviorTest3Async(cancellationToken); if (response3 is null) { State.FilteringBehavior = FilteringBehavior.AddressAndPortDependent; return; } if (Equals(response3.Remote.Address, _remoteEndPoint.Address) && response3.Remote.Port != _remoteEndPoint.Port) { State.FilteringBehavior = FilteringBehavior.AddressDependent; } else { State.FilteringBehavior = FilteringBehavior.UnsupportedServer; } } public virtual async ValueTask FilteringBehaviorTest2Async(CancellationToken cancellationToken = default) { Assumes.NotNull(State.OtherEndPoint); StunMessage5389 message = new() { StunMessageType = StunMessageType.BindingRequest, Attributes = new[] { AttributeExtensions.BuildChangeRequest(true, true) } }; return await RequestAsync(message, _remoteEndPoint, State.OtherEndPoint, cancellationToken); } public virtual async ValueTask FilteringBehaviorTest3Async(CancellationToken cancellationToken = default) { Assumes.NotNull(State.OtherEndPoint); StunMessage5389 message = new() { StunMessageType = StunMessageType.BindingRequest, Attributes = new[] { AttributeExtensions.BuildChangeRequest(false, true) } }; return await RequestAsync(message, _remoteEndPoint, _remoteEndPoint, cancellationToken); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool HasValidOtherAddress([NotNullWhen(true)] IPEndPoint? other) { return other is not null && !Equals(other.Address, _remoteEndPoint.Address) && other.Port != _remoteEndPoint.Port; } private async ValueTask RequestAsync(StunMessage5389 sendMessage, IPEndPoint remote, IPEndPoint receive, CancellationToken cancellationToken) { try { using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(0x10000); Memory buffer = memoryOwner.Memory; int length = sendMessage.WriteTo(buffer.Span); await _proxy.SendToAsync(buffer[..length], SocketFlags.None, remote, cancellationToken); using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(ReceiveTimeout); SocketReceiveMessageFromResult r = await _proxy.ReceiveMessageFromAsync(buffer, SocketFlags.None, receive, cts.Token); StunMessage5389 message = new(); if (message.TryParse(buffer.Span[..r.ReceivedBytes]) && message.IsSameTransaction(sendMessage)) { return new StunResponse(message, (IPEndPoint)r.RemoteEndPoint, r.PacketInformation.Address); } } catch (Exception ex) { Debug.WriteLine(ex); } return default; } public void Dispose() { _proxy.Dispose(); } }