记录一下 简单udp和sni 代理 done
由于之前借鉴 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的更多相关文章
- grpc使用记录(三)简单异步服务实例
目录 grpc使用记录(三)简单异步服务实例 1.编写proto文件,定义服务 2.编译proto文件,生成代码 3.编写服务端代码 async_service.cpp async_service2. ...
- 建站第二步:简单配置Nginx反代理工具
简单配置Nginx反代理工具 你要用你的域名能和服务器绑定就要用一些反代理工具 Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,极其优异的服务器软件,底层为C 来自某些 ...
- (总结)Linux服务器上最简单的Nginx反向代理配置
Nginx不但是一款高性能的Web服务器,也是高性能的反向代理服务器.下面简单说说Nginx的反向代理功能. 反向代理是什么? 反向代理指以代理服务器来接受Internet上的连接请求,然后将请求转发 ...
- python scrapy简单爬虫记录(实现简单爬取知乎)
之前写了个scrapy的学习记录,只是简单的介绍了下scrapy的一些内容,并没有实际的例子,现在开始记录例子 使用的环境是python2.7, scrapy1.2.0 首先创建项目 在要建立项目的目 ...
- SQLServer存储过程和触发器学习记录及简单例子
一.存储过程 存储过程即为能完成特定功能的一组SQL语句集.如果需要对查出的多条数据进行操作的话,这里需要理解游标(CURSOR)的概念,对于oracle有for each row命令,可以不用游标 ...
- 踩坑记录:ubuntu下,http代理无法修改的问题
事情经过: 今天在ubuntu下使用http代理的时候,碰到一个奇怪的现象.就是在当前shell窗口下,输入“env | grep proxy”,显示的http_proxy一直都存在,即使我修改了本 ...
- grpc使用记录(二)简单同步服务实例
目录 1.编写proto文件,定义服务 2.编译proto文件,生成代码 3.编写服务端代码 server.cpp 代码 编译 4.编写客户端代码 client.cpp代码 5.简单测试一下 已经折腾 ...
- Spring学习记录2——简单了解Spring容器工作机制
简单的了解Spring容器内部工作机制 Spring的AbstractApplicationContext是ApplicationContext的抽象实现类,该抽象类的refresh()方法定义了Sp ...
- 实现一个简单的基于动态代理的 AOP
实现一个简单的基于动态代理的 AOP Intro 上次看基于动态代理的 AOP 框架实现,立了一个 Flag, 自己写一个简单的 AOP 实现示例,今天过来填坑了 目前的实现是基于 Emit 来做的, ...
- 简单配置nginx反向代理,实现跨域请求
简单配置nginx去做反向代理,实现跨域请求 简单介绍nginx的nginx.conf最核心的配置,去做反向代理,实现跨域请求. 更多详细配置,参考nginx官方文档 先介绍几个nginx命令 打开n ...
随机推荐
- Getting Started with JavaFX
https://openjfx.io/openjfx-docs/#maven Run HelloWorld using Maven If you want to develop JavaFX appl ...
- Netty 缓存buffer介绍及使用
每当你需要传输数据时,它必须包含一个缓冲区.Java NIO API 自带的缓冲区类是相当有限的,没有经过优化,使用 JDK 的ByteBuffer 操作更复杂.缓冲区是一个重要的组建,它是 API的 ...
- PMML讲解及使用
1. PMML概述 PMML全称预言模型标记语言(Predictive Model Markup Language),利用XML描述和存储数据挖掘模型,是一个已经被W3C所接受的标准.使用pmml储存 ...
- Qt编写视频监控系统74-悬浮工具栏(半透明/上下左右位置/自定义按钮)
一.前言 在监控系统中一般在视频实时预览的时候,希望提供一个悬浮工具条,可以显示一些提示信息比如分辨率.码率.帧率,提供一堆快捷操作按钮,可以录像.抓拍.云台控制.关闭等操作,参考了国内很多监控厂商客 ...
- JavaScript中find()和 filter()方法的区别小结
前言 JavaScript 在 ES6 上有很多数组方法,每种方法都有独特的用途和好处. 在开发应用程序时,大多使用数组方法来获取特定的值列表并获取单个或多个匹配项. 在列出这两种方法的区别之前,我们 ...
- 如何查看一个域名所对应的IP地址?
具体步骤如下: 1.点击电脑左下角开始菜单,打开"运行"选项. 2.然后输入"cmd"并打开. 3.在弹出的页面输入ping+你想要查看的域名,比如新浪网,pi ...
- 百度高效研发实战训练营-Step4
百度高效研发实战训练营-Step4 4.1 代码检测规则:Java案例详解 以Java的案例进行代码检查规则解释,代码检测规则可以分为以下十类: 4.1.1 源文件规范 该类规范,主要为从文件名.文件 ...
- 使用 SK Plugin 给 LLM 添加能力
前几篇我们介绍了如何使用 SK + ollama 跟 LLM 进行基本的对话.如果只是对话的话其实不用什么 SK 也是可以的.今天让我们给 LLM 整点活,让它真的给我们干点啥. What is Pl ...
- 【量化读书笔记】【打开量化投资的黑箱】CH.04.风险模型
风险管理不仅仅是规避风险和减少损失,是通过对敞口实施有目的的选择和规模控制来提高收益的质量和稳定性. (注:敞口,一般指金融活动中存在金融风险的部位以及受金融风险影响的程度) 本质上风险模型是为阿尔法 ...
- Redis实战-Redisson-分布式锁
1. 简介 随着技术的快速发展,业务系统规模的不断扩大,分布式系统越来越普及.一个应用往往会部署到多台机器上,在一些业务场景中,为了保证数据的一致性,要求在同一时刻,同一任务只在一个节点上运行,保证同 ...