서론
최근 Kestrel과 Yarp를 참조하여 장기적으로 개발할 프로젝트를 구상했으나, 다양한 외부 요인으로 인해 여유롭게 개발할 시간이 부족해졌다. 또한 Kestrel을 직접 L4 레벨에서 활용할 수 있는지 궁금해하는 개발자들이 있어, Kestrel의 기본 기능을 확장하여 L4/L7 프록시 기능을 제공하는 VKProxy를 간단한 SOCKS5 구현과 함께 소개하고자 한다.
참고: 본 문서에서 다루지 않는 특정 프로토콜 관련 문의는 답변 불가능하므로 양해 부탁한다.
Kestrel의 잠재력 끌어내기
Kestrel은 ASP.NET Core의 크로스 플랫폼 웹 서버로, HTTP/1/2/3의 L7 레벨 기능만 제공하는 것으로 인식되기 쉽다. 그러나 소스 코드를 분석해보면 실제로는 L4 레벨의 소켓에서 HTTP 프로토콜 처리를 구현하고 있다. 문제는 OnBind 메서드가 HTTP 관련 구현만 제공하며 공개 확장 API가 부족하다는 점이다.
다행히도 코드가 오픈소스로 공개되어 있고, .NET의 리플렉션을 통한 비공개 API 접근이 가능하므로 이 제약을 극복할 수 있다.
유의사항:
1. Native AOT 관련 시나리오에서 리플렉션 기반 우회 방법 문제가 발생할 수 있음
2. Kestrel 버전별로 API 차이가 있을 수 있어 현재는 .NET 9.0을 기준으로 작성되었으며 .NET 10 출시 후 마이그레이션予定
구현 예시
TCP/UDP와 HTTP/1/2/3를 동시에监听하고 처리하는 전체 구조를 살펴보자.
VKProxy.Core 패키지는 Kestrel의 핵심 기능을 캡슐화하고 UDP 처리를 기본 제공하므로, Kestrel 기반 네트워크 처리가 필요한 경우에만 이 패키지를 사용하면 된다.
패키지 설치
dotnet add package VKProxy.Core --version 0.0.0.1
애플리케이션 진입점
using CoreDemo;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using VKProxy.Core.Hosting;
var app = Host.CreateDefaultBuilder(args).UseVKProxyCore()
.ConfigureServices(i =>
{
i.AddSingleton<IListenHandler, TcpListenHandler>();
i.AddSingleton<IListenHandler, UdpListenHandler>();
i.AddSingleton<IListenHandler, HttpListenHandler>();
})
.Build();
await app.RunAsync();
TCP 처리 구현
internal class TcpListenHandler : ListenHandlerBase
{
private readonly List<EndPointOptions> endPointOptions = new List<EndPointOptions>();
private readonly ILogger<TcpListenHandler> logger;
private readonly IConnectionFactory connectionFactory;
public TcpListenHandler(ILogger<TcpListenHandler> logger, IConnectionFactory connectionFactory)
{
this.logger = logger;
this.connectionFactory = connectionFactory;
}
public override Task InitAsync(CancellationToken cancellationToken)
{
endPointOptions.Add(new EndPointOptions()
{
EndPoint = IPEndPoint.Parse("127.0.0.1:5000"),
Key = "tcpXXX"
});
return Task.CompletedTask;
}
public override async Task BindAsync(ITransportManager transportManager, CancellationToken cancellationToken)
{
foreach (var item in endPointOptions)
{
try
{
await transportManager.BindAsync(item, Proxy, cancellationToken);
logger.LogInformation($"listen {item.EndPoint}");
}
catch (Exception ex)
{
logger.LogError(ex.Message, ex);
}
}
}
private async Task Proxy(ConnectionContext connection)
{
logger.LogInformation($"begin tcp {DateTime.Now} {connection.LocalEndPoint.ToString()} ");
var upstream = await connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Parse("14.215.177.38"), 80));
var task1 = connection.Transport.Input.CopyToAsync(upstream.Transport.Output);
var task2 = upstream.Transport.Input.CopyToAsync(connection.Transport.Output);
await Task.WhenAny(task1, task2);
upstream.Abort();
connection.Abort();
logger.LogInformation($"end tcp {DateTime.Now} {connection.LocalEndPoint.ToString()} ");
}
}
UDP 처리 구현
internal class UdpListenHandler : ListenHandlerBase
{
private readonly ILogger<UdpListenHandler> logger;
private readonly IUdpConnectionFactory udp;
private readonly IPEndPoint proxyServer = new(IPAddress.Parse("127.0.0.1"), 11000);
public UdpListenHandler(ILogger<UdpListenHandler> logger, IUdpConnectionFactory udp)
{
this.logger = logger;
this.udp = udp;
}
public override async Task BindAsync(ITransportManager transportManager, CancellationToken cancellationToken)
{
var ip = new EndPointOptions()
{
EndPoint = UdpEndPoint.Parse("127.0.0.1:5000"),
Key = "udpXXX"
};
await transportManager.BindAsync(ip, Proxy, cancellationToken);
logger.LogInformation($"listen {ip.EndPoint}");
}
private async Task Proxy(ConnectionContext connection)
{
if (connection is UdpConnectionContext context)
{
Console.WriteLine($"{context.LocalEndPoint} received {context.ReceivedBytesCount} from {context.RemoteEndPoint}");
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
await udp.SendToAsync(socket, proxyServer, context.ReceivedBytes, CancellationToken.None);
var r = await udp.ReceiveAsync(socket, CancellationToken.None);
await udp.SendToAsync(context.Socket, context.RemoteEndPoint, r.GetReceivedBytes(), CancellationToken.None);
}
}
}
HTTP 처리 구현
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Logging;
using System.Net;
using VKProxy.Core.Adapters;
using VKProxy.Core.Config;
using VKProxy.Core.Hosting;
namespace CoreDemo;
public class HttpListenHandler : ListenHandlerBase
{
private readonly ILogger<HttpListenHandler> logger;
private readonly ICertificateLoader certificateLoader;
public HttpListenHandler(ILogger<HttpListenHandler> logger, ICertificateLoader certificateLoader)
{
this.logger = logger;
this.certificateLoader = certificateLoader;
}
private async Task Proxy(HttpContext context)
{
var resp = context.Response;
resp.StatusCode = 404;
await resp.WriteAsJsonAsync(new { context.Request.Protocol });
await resp.CompleteAsync().ConfigureAwait(false);
}
public override async Task BindAsync(ITransportManager transportManager, CancellationToken cancellationToken)
{
try
{
var ip = new EndPointOptions()
{
EndPoint = IPEndPoint.Parse("127.0.0.1:4000"),
Key = "http"
};
await transportManager.BindHttpAsync(ip, Proxy, cancellationToken);
logger.LogInformation($"listen {ip.EndPoint}");
ip = new EndPointOptions()
{
EndPoint = IPEndPoint.Parse("127.0.0.1:4001"),
Key = "https"
};
var (c, f) = certificateLoader.LoadCertificate(new CertificateConfig() { Path = "testCert.pfx", Password = "testPassword" });
await transportManager.BindHttpAsync(ip, Proxy, cancellationToken, HttpProtocols.Http1AndHttp2AndHttp3, callbackOptions: new HttpsConnectionAdapterOptions()
{
ServerCertificate = c,
CheckCertificateRevocation = false,
ClientCertificateMode = ClientCertificateMode.AllowCertificate
});
logger.LogInformation($"listen {ip.EndPoint}");
}
catch (Exception ex)
{
logger.LogError(ex.Message, ex);
}
}
}
Kestrel 커스터마이징 핵심 포인트
핵심은 TransportManager API를 노출하는 것이다. 이를 통해 L4 레벨의 처리 능력을 확보할 수 있다.
TransportManagerAdapter 구현
public class TransportManagerAdapter : ITransportManager, IHeartbeat
{
private static MethodInfo StopAsyncMethod;
private static MethodInfo StopEndpointsAsyncMethod;
private static MethodInfo MultiplexedBindAsyncMethod;
private static MethodInfo BindAsyncMethod;
private static MethodInfo StartHeartbeatMethod;
private object transportManager;
private object heartbeat;
private object serviceContext;
private object metrics;
private int multiplexedTransportCount;
private int transportCount;
internal readonly IServiceProvider serviceProvider;
IServiceProvider ITransportManager.ServiceProvider => serviceProvider;
public TransportManagerAdapter(IServiceProvider serviceProvider, IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedConnectionListenerFactories)
{
(transportManager, heartbeat, serviceContext, metrics) = CreateTransportManager(serviceProvider);
multiplexedTransportCount = multiplexedConnectionListenerFactories.Count();
transportCount = transportFactories.Count();
this.serviceProvider = serviceProvider;
}
private static (object, object, object, object) CreateTransportManager(IServiceProvider serviceProvider)
{
foreach (var item in KestrelExtensions.TransportManagerType.GetTypeInfo().DeclaredMethods)
{
if (item.Name == "StopAsync")
{
StopAsyncMethod = item;
}
else if (item.Name == "StopEndpointsAsync")
{
StopEndpointsAsyncMethod = item;
}
else if (item.Name == "BindAsync")
{
if (item.GetParameters().Any(i => i.ParameterType == typeof(ConnectionDelegate)))
{
BindAsyncMethod = item;
}
else
{
MultiplexedBindAsyncMethod = item;
}
}
}
var s = CreateServiceContext(serviceProvider);
var r = Activator.CreateInstance(KestrelExtensions.TransportManagerType,
Enumerable.Reverse(serviceProvider.GetServices<IConnectionListenerFactory>()).ToList(),
Enumerable.Reverse(serviceProvider.GetServices<IMultiplexedConnectionListenerFactory>()).ToList(),
CreateHttpsConfigurationService(serviceProvider),
s.context
);
return (r, s.heartbeat, s.context, s.metrics);
static object CreateHttpsConfigurationService(IServiceProvider serviceProvider)
{
var CreateLogger = typeof(LoggerFactoryExtensions).GetTypeInfo().DeclaredMethods.First(i => i.Name == "CreateLogger" && i.ContainsGenericParameters);
var r = Activator.CreateInstance(KestrelExtensions.HttpsConfigurationServiceType);
var m = KestrelExtensions.HttpsConfigurationServiceType.GetMethod("Initialize");
var log = serviceProvider.GetRequiredService<ILoggerFactory>();
var l = CreateLogger.MakeGenericMethod(KestrelExtensions.HttpsConnectionMiddlewareType).Invoke(null, new object[] { log });
m.Invoke(r, new object[] { serviceProvider.GetRequiredService<IHostEnvironment>(), log.CreateLogger<KestrelServer>(), l });
return r;
}
static (object context, object heartbeat, object metrics) CreateServiceContext(IServiceProvider serviceProvider)
{
var m = CreateKestrelMetrics();
var KestrelCreateServiceContext = KestrelExtensions.KestrelServerImplType.GetMethod("CreateServiceContext", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
var r = KestrelCreateServiceContext.Invoke(null, new object[]
{
serviceProvider.GetRequiredService<IOptions<KestrelServerOptions>>(),
serviceProvider.GetRequiredService<ILoggerFactory>(),
null,
m
});
var h = KestrelExtensions.ServiceContextType.GetTypeInfo().DeclaredProperties.First(i => i.Name == "Heartbeat");
StartHeartbeatMethod = KestrelExtensions.HeartbeatType.GetTypeInfo().DeclaredMethods.First(i => i.Name == "Start");
return (r, h.GetGetMethod().Invoke(r, null), m);
}
static object CreateKestrelMetrics()
{
return Activator.CreateInstance(KestrelExtensions.KestrelMetricsType, Activator.CreateInstance(KestrelExtensions.DummyMeterFactoryType));
}
}
public Task<EndPoint> BindAsync(EndPointOptions endpointConfig, ConnectionDelegate connectionDelegate, CancellationToken cancellationToken)
{
return BindAsyncMethod.Invoke(transportManager, new object[] { endpointConfig.EndPoint, connectionDelegate, endpointConfig.Init(), cancellationToken }) as Task<EndPoint>;
}
public Task<EndPoint> BindAsync(EndPointOptions endpointConfig, MultiplexedConnectionDelegate multiplexedConnectionDelegate, CancellationToken cancellationToken)
{
return MultiplexedBindAsyncMethod.Invoke(transportManager, new object[] { endpointConfig.EndPoint, multiplexedConnectionDelegate, endpointConfig.GetListenOptions(), cancellationToken }) as Task<EndPoint>;
}
public Task StopEndpointsAsync(List<EndPointOptions> endpointsToStop, CancellationToken cancellationToken)
{
return StopEndpointsAsyncMethod.Invoke(transportManager, new object[] { EndPointOptions.Init(endpointsToStop), cancellationToken }) as Task;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return StopAsyncMethod.Invoke(transportManager, new object[] { cancellationToken }) as Task;
}
public void StartHeartbeat()
{
if (heartbeat != null)
{
StartHeartbeatMethod.Invoke(heartbeat, null);
}
}
public void StopHeartbeat()
{
if (heartbeat is IDisposable disposable)
{
disposable.Dispose();
}
}
public IConnectionBuilder UseHttpServer(IConnectionBuilder builder, IHttpApplication<HttpApplication.Context> application, HttpProtocols protocols, bool addAltSvcHeader)
{
KestrelExtensions.UseHttpServerMethod.Invoke(null, new object[] { builder, serviceContext, application, protocols, addAltSvcHeader });
return builder;
}
public IMultiplexedConnectionBuilder UseHttp3Server(IMultiplexedConnectionBuilder builder, IHttpApplication<HttpApplication.Context> application, HttpProtocols protocols, bool addAltSvcHeader)
{
KestrelExtensions.UseHttp3ServerMethod.Invoke(null, new object[] { builder, serviceContext, application, protocols, addAltSvcHeader });
return builder;
}
public ConnectionDelegate UseHttps(ConnectionDelegate next, HttpsConnectionAdapterOptions tlsCallbackOptions, HttpProtocols protocols)
{
if (tlsCallbackOptions == null)
return next;
var o = KestrelExtensions.HttpsConnectionMiddlewareInitMethod.Invoke(new object[] { next, tlsCallbackOptions, protocols, serviceProvider.GetRequiredService<ILoggerFactory>(), metrics });
return KestrelExtensions.HttpsConnectionMiddlewareOnConnectionAsyncMethod.CreateDelegate<ConnectionDelegate>(o);
}
public async Task BindHttpApplicationAsync(EndPointOptions options, IHttpApplication<HttpApplication.Context> application, CancellationToken cancellationToken, HttpProtocols protocols = HttpProtocols.Http1AndHttp2AndHttp3, bool addAltSvcHeader = true, Action<IConnectionBuilder> config = null
, Action<IMultiplexedConnectionBuilder> configMultiplexed = null, HttpsConnectionAdapterOptions callbackOptions = null)
{
var hasHttp1 = protocols.HasFlag(HttpProtocols.Http1);
var hasHttp2 = protocols.HasFlag(HttpProtocols.Http2);
var hasHttp3 = protocols.HasFlag(HttpProtocols.Http3);
var hasTls = callbackOptions is not null;
if (hasTls)
{
if (hasHttp3)
{
options.GetListenOptions().Protocols = protocols;
options.SetHttpsOptions(callbackOptions);
}
}
else
{
if (hasHttp1)
{
hasHttp2 = false;
hasHttp3 = false;
}
else if (hasHttp3)
{
throw new InvalidOperationException("HTTP/3 requires HTTPS.");
}
}
if (hasHttp3 && multiplexedTransportCount == 0 && !(hasHttp1 || hasHttp2))
{
throw new InvalidOperationException("Unable to bind an HTTP/3 endpoint. This could be because QUIC has not been configured using UseQuic, or the platform doesn't support QUIC or HTTP/3.");
}
addAltSvcHeader = addAltSvcHeader && multiplexedTransportCount > 0;
if (hasHttp1 || hasHttp2
|| protocols == HttpProtocols.None)
{
if (transportCount == 0)
{
throw new InvalidOperationException($"Cannot start HTTP/1.x or HTTP/2 server if no {nameof(IConnectionListenerFactory)} is registered.");
}
var builder = new ConnectionBuilder(serviceProvider);
config?.Invoke(builder);
UseHttpServer(builder, application, protocols, addAltSvcHeader);
var connectionDelegate = UseHttps(builder.Build(), callbackOptions, protocols);
options.EndPoint = await BindAsync(options, connectionDelegate, cancellationToken).ConfigureAwait(false);
}
if (hasHttp3 && multiplexedTransportCount > 0)
{
var builder = new MultiplexedConnectionBuilder(serviceProvider);
configMultiplexed?.Invoke(builder);
UseHttp3Server(builder, application, protocols, addAltSvcHeader);
var multiplexedConnectionDelegate = builder.Build();
options.EndPoint = await BindAsync(options, multiplexedConnectionDelegate, cancellationToken).ConfigureAwait(false);
}
}
}
VKServer를 재정의하여 OnBind 메서드의 영향을 제거함으로써 ITransportManager를 활용하여 다양한 L4/L7 처리가 가능해진다.
public class VKServer : IServer
{
private readonly ITransportManager transportManager;
private readonly IHeartbeat heartbeat;
private readonly IListenHandler listenHandler;
private readonly GeneralLogger logger;
private bool _hasStarted;
private int _stopping;
private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1);
private readonly CancellationTokenSource _stopCts = new CancellationTokenSource();
private readonly TaskCompletionSource _stoppedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
private IDisposable? _configChangedRegistration;
public VKServer(ITransportManager transportManager, IHeartbeat heartbeat, IListenHandler listenHandler, GeneralLogger logger)
{
this.transportManager = transportManager;
this.heartbeat = heartbeat;
this.listenHandler = listenHandler;
this.logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
if (_hasStarted)
{
throw new InvalidOperationException("Server already started");
}
_hasStarted = true;
await listenHandler.InitAsync(cancellationToken);
heartbeat.StartHeartbeat();
await BindAsync(cancellationToken).ConfigureAwait(false);
}
catch
{
Dispose();
throw;
}
}
private async Task BindAsync(CancellationToken cancellationToken)
{
await _bindSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (_stopping == 1)
{
throw new InvalidOperationException("Server has already been stopped.");
}
IChangeToken? reloadToken = listenHandler.GetReloadToken();
await listenHandler.BindAsync(transportManager, _stopCts.Token).ConfigureAwait(false);
_configChangedRegistration = reloadToken?.RegisterChangeCallback(TriggerRebind, this);
}
finally
{
_bindSemaphore.Release();
}
}
private void TriggerRebind(object? state)
{
if (state is VKServer server)
{
_ = server.RebindAsync();
}
}
private async Task RebindAsync()
{
await _bindSemaphore.WaitAsync();
IChangeToken? reloadToken = null;
try
{
if (_stopping == 1)
{
return;
}
reloadToken = listenHandler.GetReloadToken();
await listenHandler.RebindAsync(transportManager, _stopCts.Token).ConfigureAwait(false);
}
catch (Exception ex)
{
logger.UnexpectedException("Unable to reload configuration", ex);
}
finally
{
_configChangedRegistration = reloadToken?.RegisterChangeCallback(TriggerRebind, this);
_bindSemaphore.Release();
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
if (Interlocked.Exchange(ref _stopping, 1) == 1)
{
await _stoppedTcs.Task.ConfigureAwait(false);
return;
}
heartbeat.StopHeartbeat();
_stopCts.Cancel();
await _bindSemaphore.WaitAsync().ConfigureAwait(false);
try
{
await listenHandler.StopAsync(transportManager, cancellationToken).ConfigureAwait(false);
await transportManager.StopAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_stoppedTcs.TrySetException(ex);
throw;
}
finally
{
_configChangedRegistration?.Dispose();
_stopCts.Dispose();
_bindSemaphore.Release();
}
_stoppedTcs.TrySetResult();
}
public void Dispose()
{
StopAsync(new CancellationToken(canceled: true)).GetAwaiter().GetResult();
}
}
SOCKS5 프록시 구현
SOCKS5 프로토콜에 대한 상세한 설명은 생략하며, 핵심 구현 부분만 다룬다. 프로토콜 명세는 관련 자료를 참고하시기 바랍니다.
internal class Socks5Middleware : ITcpProxyMiddleware
{
private readonly IDictionary<byte, ISocks5Auth> auths;
private readonly IConnectionFactory tcp;
private readonly IHostResolver hostResolver;
private readonly ITransportManager transport;
private readonly IUdpConnectionFactory udp;
public Socks5Middleware(IEnumerable<ISocks5Auth> socks5Auths, IConnectionFactory tcp, IHostResolver hostResolver, ITransportManager transport, IUdpConnectionFactory udp)
{
this.auths = socks5Auths.ToFrozenDictionary(i => i.AuthType);
this.tcp = tcp;
this.hostResolver = hostResolver;
this.transport = transport;
this.udp = udp;
}
public Task InitAsync(ConnectionContext context, CancellationToken token, TcpDelegate next)
{
var feature = context.Features.Get<IL4ReverseProxyFeature>();
if (feature is not null)
{
var route = feature.Route;
if (route is not null && route.Metadata is not null
&& route.Metadata.TryGetValue("socks5", out var b) && bool.TryParse(b, out var isSocks5) && isSocks5)
{
feature.IsDone = true;
return Proxy(context, feature, token);
}
}
return next(context, token);
}
public Task<ReadOnlyMemory<byte>> OnRequestAsync(ConnectionContext context, ReadOnlyMemory<byte> source, CancellationToken token, TcpProxyDelegate next)
{
return next(context, source, token);
}
public Task<ReadOnlyMemory<byte>> OnResponseAsync(ConnectionContext context, ReadOnlyMemory<byte> source, CancellationToken token, TcpProxyDelegate next)
{
return next(context, source, token);
}
private async Task Proxy(ConnectionContext context, IL4ReverseProxyFeature feature, CancellationToken token)
{
var input = context.Transport.Input;
var output = context.Transport.Output;
// 1. SOCKS5 인증
if (!await Socks5Parser.AuthAsync(input, auths, context, token))
{
context.Abort();
}
// 2. SOCKS5 명령 요청 획득
var cmd = await Socks5Parser.GetCmdRequestAsync(input, token);
IPEndPoint ip = await ResolveIpAsync(context, cmd, token);
switch (cmd.Cmd)
{
case Socks5Cmd.Connect:
case Socks5Cmd.Bind:
// 3. TCP 프록시 처리
ConnectionContext upstream;
try
{
upstream = await tcp.ConnectAsync(ip, token);
}
catch
{
await Socks5Parser.ResponeAsync(output, Socks5CmdResponseType.ConnectFail, token);
throw;
}
// 4. TCP 연결 성공 알림
await Socks5Parser.ResponeAsync(output, Socks5CmdResponseType.Success, token);
var task = await Task.WhenAny(
context.Transport.Input.CopyToAsync(upstream.Transport.Output, token)
, upstream.Transport.Input.CopyToAsync(context.Transport.Output, token));
if (task.IsCanceled)
{
context.Abort();
}
break;
case Socks5Cmd.UdpAssociate:
// 3. UDP 프록시 처리
var local = context.LocalEndPoint as IPEndPoint;
var op = new EndPointOptions()
{
EndPoint = new UdpEndPoint(local.Address, 0),
Key = Guid.NewGuid().ToString(),
};
try
{
var remote = context.RemoteEndPoint;
var timeout = feature.Route.Timeout;
op.EndPoint = await transport.BindAsync(op, c => ProxyUdp(c as UdpConnectionContext, remote, timeout), token);
// 5. TCP 종료 시 임시 UDP 서비스 종료
context.ConnectionClosed.Register(state => transport.StopEndpointsAsync(new List<EndPointOptions>() { state as EndPointOptions }, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(), op);
}
catch
{
await Socks5Parser.ResponeAsync(output, Socks5CmdResponseType.ConnectFail, token);
throw;
}
// 4. UDP 연결 성공 알림
await Socks5Parser.ResponeAsync(output, op.EndPoint as IPEndPoint, Socks5CmdResponseType.Success, token);
break;
}
}
private async Task ProxyUdp(UdpConnectionContext context, EndPoint remote, TimeSpan timeout)
{
using var cts = CancellationTokenSourcePool.Default.Rent(timeout);
var token = cts.Token;
if (context.RemoteEndPoint.GetHashCode() == remote.GetHashCode())
{
var req = Socks5Parser.GetUdpRequest(context.ReceivedBytes);
IPEndPoint ip = await ResolveIpAsync(req, token);
await udp.SendToAsync(context.Socket, ip, req.Data, token);
}
else
{
await Socks5Parser.UdpResponeAsync(udp, context, remote as IPEndPoint, token);
}
}
private async Task<IPEndPoint> ResolveIpAsync(ConnectionContext context, Socks5Common cmd, CancellationToken token)
{
IPEndPoint ip = await ResolveIpAsync(cmd, token);
if (ip is null)
{
await Socks5Parser.ResponeAsync(context.Transport.Output, Socks5CmdResponseType.AddressNotAllow, token);
context.Abort();
}
return ip;
}
private async Task<IPEndPoint> ResolveIpAsync(Socks5Common cmd, CancellationToken token)
{
IPEndPoint ip;
if (cmd.Domain is not null)
{
var ips = await hostResolver.HostResolveAsync(cmd.Domain, token);
if (ips.Length > 0)
{
ip = new IPEndPoint(ips.First(), cmd.Port);
}
else
ip = null;
}
else if (cmd.Ip is not null)
{
ip = new IPEndPoint(cmd.Ip, cmd.Port);
}
else
{
ip = null;
}
return ip;
}
}
위 구현을 통해 복잡한 while 루프 없이도 SOCKS5 프록시 기능이 구현 가능하다.