由于之前借鉴 Kestrel 了非常多抽象和优化实现,对于后续的扩展非常便利,

实现 简单udp和sni 代理 两个功能比预期快了超多(当然也有偷懒因素)

(PS 大家有空的话,能否在 GitHub https://github.com/fs7744/NZOrz 点个 star 呢?毕竟借鉴代码也不易呀 哈哈哈哈哈)

简单udp代理

这里的udp 代理功能比较简单:代理程序收到任何 udp 包都会通过路由匹配找 upstream ,然后转发给upstream

udp proxy 使用配置

基本格式和之前 tcp proxy 一致,

只是Protocols得选择UDP, 然后多了UdpResponses 允许 upstream 返回多少个 udp 包给请求者, 默认为0,即不返回任何包

{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"ReverseProxy": {
"Routes": {
"udpTest": {
"Protocols": [ "UDP" ],
"Match": {
"Hosts": [ "*:5000" ]
},
"ClusterId": "udpTest",
"RetryCount": 1,
"UdpResponses": 1,
"Timeout": "00:00:11"
}
},
"Clusters": {
"udpTest": {
"LoadBalancingPolicy": "RoundRobin",
"HealthCheck": {
"Passive": {
"Enable": true
}
},
"Destinations": [
{
"Address": "127.0.0.1:11000"
}
]
}
}
}
}

实现

这里列举一下,表明有多简单

ps: 由于要实现的是非常简单udp代理,所以不基于IMultiplexedConnectionListener ,而基于 IConnectionListener 方式 (对,就是俺偷懒了)

1. 实现 UdpConnectionContext

偷懒就直接把udp 包数据放 context 上了,不放 Parameters 上,减少字典实例和内存使用

public sealed class UdpConnectionContext : TransportConnection
{
private readonly IMemoryOwner<byte> memory;
public Socket Socket { get; }
public int ReceivedBytesCount { get; } public Memory<byte> ReceivedBytes => memory.Memory.Slice(0, ReceivedBytesCount); public UdpConnectionContext(Socket socket, UdpReceiveFromResult result)
{
Socket = socket;
ReceivedBytesCount = result.ReceivedBytesCount;
this.memory = result.Buffer;
LocalEndPoint = socket.LocalEndPoint;
RemoteEndPoint = result.RemoteEndPoint;
} public UdpConnectionContext(Socket socket, EndPoint remoteEndPoint, int receivedBytes, IMemoryOwner<byte> memory)
{
Socket = socket;
ReceivedBytesCount = receivedBytes;
this.memory = memory;
LocalEndPoint = socket.LocalEndPoint;
RemoteEndPoint = remoteEndPoint;
} public override ValueTask DisposeAsync()
{
memory.Dispose();
return default;
}
}
2. 实现 IConnectionListener
internal sealed class UdpConnectionListener : IConnectionListener
{
private EndPoint? udpEndPoint;
private readonly GatewayProtocols protocols;
private OrzLogger _logger;
private readonly IUdpConnectionFactory connectionFactory;
private readonly Func<EndPoint, GatewayProtocols, Socket> createBoundListenSocket;
private Socket? _listenSocket; public UdpConnectionListener(EndPoint? udpEndPoint, GatewayProtocols protocols, IRouteContractor contractor, OrzLogger logger, IUdpConnectionFactory connectionFactory)
{
this.udpEndPoint = udpEndPoint;
this.protocols = protocols;
_logger = logger;
this.connectionFactory = connectionFactory;
createBoundListenSocket = contractor.GetSocketTransportOptions().CreateBoundListenSocket;
} public EndPoint EndPoint => udpEndPoint; internal void Bind()
{
if (_listenSocket != null)
{
throw new InvalidOperationException("Transport is already bound.");
} Socket listenSocket;
try
{
listenSocket = createBoundListenSocket(EndPoint, protocols);
}
catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
throw new AddressInUseException(e.Message, e);
} Debug.Assert(listenSocket.LocalEndPoint != null); _listenSocket = listenSocket;
} public async ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default)
{
while (true)
{
try
{
Debug.Assert(_listenSocket != null, "Bind must be called first.");
var r = await connectionFactory.ReceiveAsync(_listenSocket, cancellationToken);
return new UdpConnectionContext(_listenSocket, r);
}
catch (ObjectDisposedException)
{
// A call was made to UnbindAsync/DisposeAsync just return null which signals we're done
return null;
}
catch (SocketException e) when (e.SocketErrorCode == SocketError.OperationAborted)
{
// A call was made to UnbindAsync/DisposeAsync just return null which signals we're done
return null;
}
catch (SocketException)
{
// The connection got reset while it was in the backlog, so we try again.
_logger.ConnectionReset("(null)");
}
}
} public ValueTask DisposeAsync()
{
_listenSocket?.Dispose(); return default;
} public ValueTask UnbindAsync(CancellationToken cancellationToken = default)
{
_listenSocket?.Dispose();
return default;
}
}
3. 实现 IConnectionListenerFactory
public sealed class UdpTransportFactory : IConnectionListenerFactory, IConnectionListenerFactorySelector
{
private readonly IRouteContractor contractor;
private readonly OrzLogger logger;
private readonly IUdpConnectionFactory connectionFactory; public UdpTransportFactory(
IRouteContractor contractor,
OrzLogger logger,
IUdpConnectionFactory connectionFactory)
{
ArgumentNullException.ThrowIfNull(contractor);
ArgumentNullException.ThrowIfNull(logger); this.contractor = contractor;
this.logger = logger;
this.connectionFactory = connectionFactory;
} public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, GatewayProtocols protocols, CancellationToken cancellationToken = default)
{
var transport = new UdpConnectionListener(endpoint, GatewayProtocols.UDP, contractor, logger, connectionFactory);
transport.Bind();
return new ValueTask<IConnectionListener>(transport);
} public bool CanBind(EndPoint endpoint, GatewayProtocols protocols)
{
if (!protocols.HasFlag(GatewayProtocols.UDP)) return false;
return endpoint switch
{
IPEndPoint _ => true,
_ => false
};
}
}
4. 在 L4ProxyMiddleware 实现udp proxy 具体逻辑

路由和之前tcp的公用,这里就不列举了

public class L4ProxyMiddleware : IOrderMiddleware
{
public async Task Invoke(ConnectionContext context, ConnectionDelegate next)
{
try
{
if (context.Protocols == GatewayProtocols.SNI)
{
await SNIProxyAsync(context);
}
else
{
var route = await router.MatchAsync(context);
if (route is null)
{
logger.NotFoundRouteL4(context.LocalEndPoint);
}
else
{
context.Route = route;
logger.ProxyBegin(route.RouteId);
if (context.Protocols == GatewayProtocols.TCP)
{
await TcpProxyAsync(context, route);
}
else
{
await UdpProxyAsync((UdpConnectionContext)context, route);
}
logger.ProxyEnd(route.RouteId);
}
}
}
catch (Exception ex)
{
logger.UnexpectedException(ex.Message, ex);
}
finally
{
await next(context);
}
} private async Task UdpProxyAsync(UdpConnectionContext context, RouteConfig route)
{
try
{
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
var cts = route.CreateTimeoutTokenSource(cancellationTokenSourcePool);
var token = cts.Token;
if (await DoUdpSendToAsync(socket, context, route, route.RetryCount, await reqUdp(context, context.ReceivedBytes, token), token))
{
var c = route.UdpResponses;
while (c > 0)
{
var r = await udp.ReceiveAsync(socket, token);
c--;
await udp.SendToAsync(context.Socket, context.RemoteEndPoint, await respUdp(context, r.GetReceivedBytes(), token), token);
}
}
else
{
logger.NotFoundAvailableUpstream(route.ClusterId);
}
}
catch (OperationCanceledException)
{
logger.ConnectUpstreamTimeout(route.RouteId);
}
catch (Exception ex)
{
logger.UnexpectedException(nameof(UdpProxyAsync), ex);
}
finally
{
context.SelectedDestination?.ConcurrencyCounter.Decrement();
}
}

所以是不是真的简单, 理论上基于 Kestrel 也是一个样子哦

优化

当然参考于 Kestrel 的 tcp socket 处理,也是有些简单优化的, 比如

  • 不使用 UdpClient (ps 不是因为实现烂哈,而是其比较公用,没有机会让我们改变里面的内容)
  • 基于 SocketAsyncEventArgs, IValueTaskSource<SocketReceiveFromResult>SocketAsyncEventArgs, IValueTaskSource<int> 实现 将异步读写交予 PipeScheduler 的逻辑
  • 基于 ConcurrentQueue<UdpSender> 实现简单的 udp发送对象池,加强对象复用,稍稍稍微减少内存占用
  • 基于 ConcurrentQueue<PooledCancellationTokenSource> 实现简单的 CancellationTokenSource对象池,加强对象复用,稍稍稍微减少内存占用

sni代理

除了 tcp 和 udp 的基本代理, 也尝试实现了一个 对tcp的 sni 代理,(比如 http1 和 http2 的 https)

不过目前只实现了代理不做ssl加密解密,upstream自己处理的pass 模式,如果代理要实现ssl加密解密,理论上基于现成的 sslstream

sni proxy 使用配置

只需配置Listen 中 公用的 sni 监听端口

然后不同 sni 配置自己的路由和upstream就好

同时每个route 可以通过SupportSslProtocols限制 tls 版本

举个栗子

{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"ReverseProxy": {
"Listen": {
"snitest": {
"Protocols": "SNI",
"Address": [ "*:444" ]
}
},
"Routes": {
"snitestroute": {
"Protocols": "SNI",
"SupportSslProtocols": [ "Tls13", "Tls12" ],
"Match": {
"Hosts": [ "*google.com" ]
},
"ClusterId": "apidemo"
},
"snitestroute2": {
"Protocols": "Tcp",
"Match": {
"Hosts": [ "*:448" ]
},
"ClusterId": "apidemo"
}
},
"Clusters": {
"apidemo": {
"LoadBalancingPolicy": "RoundRobin",
"HealthCheck": {
"Active": {
"Enable": true,
"Policy": "Connect"
}
},
"Destinations": [
{
"Address": "https://www.google.com"
}
]
}
}
}
}

实现

核心实现其实只有 路由 处理 ,proxy 代理和 tcp 代理一模一样(在请求 和 upstream 间搬运 tcp数据而已)

路由处理

通过 ClientHello 找到要访问的 域名, 然后通过域名匹配路由找到 upstream, 最后搬运 tcp数据

ClientHello 解析就直接搬运自TlsFrameHelper

    /// 路由匹配
public async ValueTask<(RouteConfig, ReadResult)> MatchSNIAsync(ConnectionContext context, CancellationToken token)
{
if (sniRoute is null) return (null, default);
var (hello, rr) = await TryGetClientHelloAsync(context, token);
if (hello.HasValue)
{
var h = hello.Value;
var r = await sniRoute.MatchAsync(h.TargetName.Reverse(), h, MatchSNI);
if (r is null)
{
logger.NotFoundRouteSni(h.TargetName);
}
return (r, rr);
}
else
{
logger.NotFoundRouteSni("client hello failed");
return (null, rr);
}
} /// 匹配 tls 版本
private bool MatchSNI(RouteConfig config, TlsFrameInfo info)
{
if (!config.SupportSslProtocols.HasValue) return true;
var v = config.SupportSslProtocols.Value;
if (v == SslProtocols.None) return true;
var t = info.SupportedVersions;
if (v.HasFlag(SslProtocols.Tls13) && t.HasFlag(SslProtocols.Tls13)) return true;
else if (v.HasFlag(SslProtocols.Tls12) && t.HasFlag(SslProtocols.Tls12)) return true;
else if (v.HasFlag(SslProtocols.Tls11) && t.HasFlag(SslProtocols.Tls11)) return true;
else if (v.HasFlag(SslProtocols.Tls) && t.HasFlag(SslProtocols.Tls)) return true;
else if (v.HasFlag(SslProtocols.Ssl3) && t.HasFlag(SslProtocols.Ssl3)) return true;
else if (v.HasFlag(SslProtocols.Ssl2) && t.HasFlag(SslProtocols.Ssl2)) return true;
else if (v.HasFlag(SslProtocols.Default) && t.HasFlag(SslProtocols.Default)) return true;
else return false;
} /// 解析 ClientHello
private static async ValueTask<(TlsFrameInfo?, ReadResult)> TryGetClientHelloAsync(ConnectionContext context, CancellationToken token)
{
var input = context.Transport.Input;
TlsFrameInfo info = default;
while (true)
{
var f = await input.ReadAsync(token).ConfigureAwait(false);
if (f.IsCompleted)
{
return (null, f);
}
var buffer = f.Buffer;
if (buffer.Length == 0)
{
continue;
} var data = buffer.IsSingleSegment ? buffer.First.Span : buffer.ToArray();
if (TlsFrameHelper.TryGetFrameInfo(data, ref info))
{
return (info, f);
}
else
{
input.AdvanceTo(buffer.Start, buffer.End);
continue;
}
}
}
搬运 tcp数据
private async Task SNIProxyAsync(ConnectionContext context)
{
var c = cancellationTokenSourcePool.Rent();
c.CancelAfter(options.ConnectionTimeout);
var (route, r) = await router.MatchSNIAsync(context, c.Token);
if (route is not null)
{
context.Route = route;
logger.ProxyBegin(route.RouteId);
ConnectionContext upstream = null;
try
{
upstream = await DoConnectionAsync(context, route, route.RetryCount);
if (upstream is null)
{
logger.NotFoundAvailableUpstream(route.ClusterId);
}
else
{
context.SelectedDestination?.ConcurrencyCounter.Increment();
var cts = route.CreateTimeoutTokenSource(cancellationTokenSourcePool);
var t = cts.Token;
await r.CopyToAsync(upstream.Transport.Output, t); // 和tcp 代理搬运数据唯一不同, 要先发送 ClientHello 数据,因为已经被我们读取了
context.Transport.Input.AdvanceTo(r.Buffer.End);
var task = hasMiddlewareTcp ?
await Task.WhenAny(
context.Transport.Input.CopyToAsync(new MiddlewarePipeWriter(upstream.Transport.Output, context, reqTcp), t)
, upstream.Transport.Input.CopyToAsync(new MiddlewarePipeWriter(context.Transport.Output, context, respTcp), t))
: await Task.WhenAny(
context.Transport.Input.CopyToAsync(upstream.Transport.Output, t)
, upstream.Transport.Input.CopyToAsync(context.Transport.Output, t));
if (task.IsCanceled)
{
logger.ProxyTimeout(route.RouteId, route.Timeout);
}
}
}
catch (OperationCanceledException)
{
logger.ConnectUpstreamTimeout(route.RouteId);
}
catch (Exception ex)
{
logger.UnexpectedException(nameof(TcpProxyAsync), ex);
}
finally
{
context.SelectedDestination?.ConcurrencyCounter.Decrement();
upstream?.Abort();
}
logger.ProxyEnd(route.RouteId);
}
}

组件各部分都是可替换或者可增加的

因为整体都是基于ioc的,所以组件各部分都是可替换或者可增加的, 客制化扩展还是很高的哦

目前暴露的列表可在 代码这里面查看

internal static HostApplicationBuilder UseOrzDefaults(this HostApplicationBuilder builder)
{
var services = builder.Services;
services.AddSingleton<IHostedService, HostedService>();
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IMeterFactory, DummyMeterFactory>();
services.AddSingleton<IServer, OrzServer>();
services.AddSingleton<OrzLogger>();
services.AddSingleton<OrzMetrics>();
services.AddSingleton<IConnectionListenerFactory, SocketTransportFactory>();
services.AddSingleton<IConnectionListenerFactory, UdpTransportFactory>();
services.AddSingleton<IUdpConnectionFactory, UdpConnectionFactory>();
services.AddSingleton<IConnectionFactory, SocketConnectionFactory>();
services.AddSingleton<IRouteContractorValidator, RouteContractorValidator>();
services.AddSingleton<IEndPointConvertor, CommonEndPointConvertor>();
services.AddSingleton<IL4Router, L4Router>();
services.AddSingleton<IOrderMiddleware, L4ProxyMiddleware>();
services.AddSingleton<ILoadBalancingPolicyFactory, LoadBalancingPolicy>();
services.AddSingleton<IClusterConfigValidator, ClusterConfigValidator>();
services.AddSingleton<IDestinationResolver, DnsDestinationResolver>(); services.AddSingleton<ILoadBalancingPolicy, RandomLoadBalancingPolicy>();
services.AddSingleton<ILoadBalancingPolicy, RoundRobinLoadBalancingPolicy>();
services.AddSingleton<ILoadBalancingPolicy, LeastRequestsLoadBalancingPolicy>();
services.AddSingleton<ILoadBalancingPolicy, PowerOfTwoChoicesLoadBalancingPolicy>(); services.AddSingleton<IHealthReporter, PassiveHealthReporter>();
services.AddSingleton<IHealthUpdater, HealthyAndUnknownDestinationsUpdater>();
services.AddSingleton<IActiveHealthCheckMonitor, ActiveHealthCheckMonitor>();
services.AddSingleton<IActiveHealthChecker, ConnectionActiveHealthChecker>(); return builder;
}

比如要添加 负载均衡策略,就可以实现

public interface ILoadBalancingPolicy
{
string Name { get; } DestinationState? PickDestination(ConnectionContext context, IReadOnlyList<DestinationState> availableDestinations);
}

如果对全部已有负载均衡策略都不满意,那就可以直接替换 ILoadBalancingPolicyFactory

public interface ILoadBalancingPolicyFactory
{
DestinationState? PickDestination(ConnectionContext context, RouteConfig route);
}

比如你就可以通过sni将开发环境(或者其他环境)无法访问的请求在一台有其他访问权限的机器进行转发

差不多就做了这些,造轮子还是挺好玩的,当然大家如果在 GitHub https://github.com/fs7744/NZOrz 点个 star, 就更好玩了

记录一下 简单udp和sni 代理 done的更多相关文章

  1. grpc使用记录(三)简单异步服务实例

    目录 grpc使用记录(三)简单异步服务实例 1.编写proto文件,定义服务 2.编译proto文件,生成代码 3.编写服务端代码 async_service.cpp async_service2. ...

  2. 建站第二步:简单配置Nginx反代理工具

    简单配置Nginx反代理工具 你要用你的域名能和服务器绑定就要用一些反代理工具 Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,极其优异的服务器软件,底层为C 来自某些 ...

  3. (总结)Linux服务器上最简单的Nginx反向代理配置

    Nginx不但是一款高性能的Web服务器,也是高性能的反向代理服务器.下面简单说说Nginx的反向代理功能. 反向代理是什么? 反向代理指以代理服务器来接受Internet上的连接请求,然后将请求转发 ...

  4. python scrapy简单爬虫记录(实现简单爬取知乎)

    之前写了个scrapy的学习记录,只是简单的介绍了下scrapy的一些内容,并没有实际的例子,现在开始记录例子 使用的环境是python2.7, scrapy1.2.0 首先创建项目 在要建立项目的目 ...

  5. SQLServer存储过程和触发器学习记录及简单例子

     一.存储过程 存储过程即为能完成特定功能的一组SQL语句集.如果需要对查出的多条数据进行操作的话,这里需要理解游标(CURSOR)的概念,对于oracle有for each row命令,可以不用游标 ...

  6. 踩坑记录:ubuntu下,http代理无法修改的问题

    事情经过: 今天在ubuntu下使用http代理的时候,碰到一个奇怪的现象.就是在当前shell窗口下,输入“env  | grep proxy”,显示的http_proxy一直都存在,即使我修改了本 ...

  7. grpc使用记录(二)简单同步服务实例

    目录 1.编写proto文件,定义服务 2.编译proto文件,生成代码 3.编写服务端代码 server.cpp 代码 编译 4.编写客户端代码 client.cpp代码 5.简单测试一下 已经折腾 ...

  8. Spring学习记录2——简单了解Spring容器工作机制

    简单的了解Spring容器内部工作机制 Spring的AbstractApplicationContext是ApplicationContext的抽象实现类,该抽象类的refresh()方法定义了Sp ...

  9. 实现一个简单的基于动态代理的 AOP

    实现一个简单的基于动态代理的 AOP Intro 上次看基于动态代理的 AOP 框架实现,立了一个 Flag, 自己写一个简单的 AOP 实现示例,今天过来填坑了 目前的实现是基于 Emit 来做的, ...

  10. 简单配置nginx反向代理,实现跨域请求

    简单配置nginx去做反向代理,实现跨域请求 简单介绍nginx的nginx.conf最核心的配置,去做反向代理,实现跨域请求. 更多详细配置,参考nginx官方文档 先介绍几个nginx命令 打开n ...

随机推荐

  1. Python 在Excel单元格中应用数据条

    在Excel中添加数据条是一种数据可视化技巧,它通过条形图的形式在单元格内直观展示数值的大小,尤其适合比较同一列或行中各个单元格的数值.这种表示方式可以让大量的数字信息一目了然.本文将介绍如何使用Py ...

  2. Qt编写跨平台视频监控系统(64通道占用7%CPU/支持win_linux_mac等)

    一.前言 视频监控组件经历过数十年的迭代,从最初的只简单播放个rtsp视频流,到现在支持各种音频视频文件格式(mp3.wav.mp4.asf.rm.rmvb.mkv等).支持各种视频流格式(rtp.r ...

  3. Qt音视频开发9-ffmpeg录像存储

    一.前言 上一篇文章写道直接将视频流保存裸流到文件,尽管裸流文件有一定的好处,但是 毕竟大部分用户需要的不是裸流而是MP4视频文件,所以需要将视频流保存成MP4文件,毕竟电脑上的播放器包括默认的播放器 ...

  4. Idea中@Autowired 黄色波浪线问题以及注入类显示红色波浪线问题解决

    解决办法: 点开路径:File--->Settings--->Editor--->Inspections--->Spring--->Spring core--->C ...

  5. IM开发者的零基础通信技术入门(十一):为什么WiFi信号差?一文即懂!

    一.本文内容概述 WiFi对于现在的家庭来说,属于司空见惯的上网方式,但很多情况下,家里房间多.空间大.杂物乱的情况下,WiFi的信号就受影响.为什么WiFi信号会受影响?什么情况下该使用何种方式组网 ...

  6. 安装Rational rose软件简单教程

    安装Rational rose软件简单教程 一.下载文件 百度云盘下载链接:https://pan.baidu.com/s/1QWmTgxKlQtE5flP7dXIF5w 提取码:wjnq 二.装虚拟 ...

  7. 控制反转(Inversion of Control,IoC)

    依赖注入(Dependency Injection,DI)和控制反转(Inversion of Control,IoC)是软件工程中两个相关但不同的概念.它们都旨在提高代码的模块化.可维护性和可测试性 ...

  8. runoob-pandas(python)

    https://www.runoob.com/pandas/pandas-tutorial.html Pandas 教程 Pandas 是 Python 语言的一个扩展程序库,用于数据分析. Pand ...

  9. 学Shiro完结版-3

    第八章 拦截器机制--<跟我学Shiro> 8.1 拦截器介绍 Shiro使用了与Servlet一样的Filter接口进行扩展:所以如果对Filter不熟悉可以参考<Servlet3 ...

  10. C语言实现高阶阶乘(1000的阶乘C语言实现)

    由于C语言的变量的大小的限制,使用已有变量无法保存阶乘结果,所以使用数组保存结果,从而使得无法保存的结果得以保存. #include <stdio.h> void Print_Factor ...