QUicOverUdp

This commit is contained in:
snltty
2024-05-30 16:21:11 +08:00
parent 839f6b38f0
commit 7d029ac1e2
18 changed files with 783 additions and 145 deletions

4
.editorconfig Normal file
View File

@@ -0,0 +1,4 @@
[*.cs]
# CA1416: 验证平台兼容性
dotnet_diagnostic.CA1416.severity = none

View File

@@ -2,10 +2,11 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>net7;net8</TargetFrameworks>
<TargetFrameworks>net8</TargetFrameworks>
<PublishAot>false</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release;ReleaseNetwork;ReleaseMonitor</Configurations>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
@@ -27,15 +28,6 @@
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseMonitor|net7.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseNetwork|net7.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
@@ -45,9 +37,7 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseNetwork|net8.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net7.0-windows;net8.0-windows</TargetFrameworks>
<TargetFrameworks>net8.0-windows</TargetFrameworks>
<Nullable>disable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net7.0-windows;net8.0-windows</TargetFrameworks>
<TargetFrameworks>net8.0-windows</TargetFrameworks>
<Nullable>disable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
@@ -11,26 +11,10 @@
<Configurations>Debug;Release;ReleaseNetwork;ReleaseMonitor</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseMonitor|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseNetwork|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net7.0-windows;net8.0-windows</TargetFrameworks>
<TargetFrameworks>net8.0-windows</TargetFrameworks>
<Nullable>disable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
@@ -11,26 +11,10 @@
<Configurations>Debug;Release;ReleaseNetwork;ReleaseMonitor</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseMonitor|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseNetwork|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

View File

@@ -29,6 +29,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cmonitor.tests", "cmonitor.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cmonitor.network.service", "cmonitor.network.service\cmonitor.network.service.csproj", "{E8AB5039-3A42-424F-AAEC-A102C8CAA305}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D2831E12-7497-4B7B-ADFD-7C327C5622D3}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net7.0-windows;net8.0-windows</TargetFrameworks>
<TargetFrameworks>net8.0-windows</TargetFrameworks>
<Nullable>disable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
@@ -11,25 +11,10 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseMonitor|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseNetwork|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net7.0-windows;net8.0-windows</TargetFrameworks>
<TargetFrameworks>net8.0-windows</TargetFrameworks>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<Nullable>disable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
@@ -12,25 +12,10 @@
<Configurations>Debug;Release;ReleaseNetwork;ReleaseMonitor</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseMonitor|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseNetwork|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net7.0-windows;net8.0-windows</TargetFrameworks>
<TargetFrameworks>net8.0-windows</TargetFrameworks>
<Nullable>disable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
@@ -11,25 +11,10 @@
<Configurations>Debug;Release;ReleaseNetwork;ReleaseMonitor</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseMonitor|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseNetwork|net7.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows|AnyCPU'">
<DebugType>embedded</DebugType>

View File

@@ -3,7 +3,9 @@ using common.libs.extends;
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Net.Sockets;
using System.Text.Json.Serialization;
namespace cmonitor.client.tunnel
@@ -270,4 +272,217 @@ namespace cmonitor.client.tunnel
}
}
public sealed class TunnelConnectionQuic : ITunnelConnection
{
public TunnelConnectionQuic()
{
}
public string RemoteMachineName { get; init; }
public string TransactionId { get; init; }
public string TransportName { get; init; }
public string Label { get; init; }
public TunnelMode Mode { get; init; }
public TunnelProtocolType ProtocolType { get; init; }
public TunnelType Type { get; init; }
public TunnelDirection Direction { get; init; }
public IPEndPoint IPEndPoint { get; init; }
public bool Connected => Stream != null && Stream.CanWrite;
[JsonIgnore]
public QuicStream Stream { get; init; }
private ITunnelConnectionReceiveCallback callback;
private CancellationTokenSource cancellationTokenSource;
private object userToken;
private bool framing;
private Pipe pipe;
private ReceiveDataBuffer bufferCache = new ReceiveDataBuffer();
/// <summary>
/// 开始接收数据
/// </summary>
/// <param name="callback">数据回调</param>
/// <param name="userToken">自定义数据</param>
/// <param name="framing">是否处理粘包true时请在首部4字节标注数据长度</param>
public void BeginReceive(ITunnelConnectionReceiveCallback callback, object userToken, bool framing = true)
{
if (this.callback != null) return;
this.callback = callback;
this.userToken = userToken;
this.framing = framing;
cancellationTokenSource = new CancellationTokenSource();
pipe = new Pipe(new PipeOptions(pauseWriterThreshold: 1 * 1024 * 1024, resumeWriterThreshold: 128 * 1024));
_ = ProcessWrite();
_ = ProcessReader();
}
private async Task ProcessWrite()
{
PipeWriter writer = pipe.Writer;
try
{
while (cancellationTokenSource.IsCancellationRequested == false)
{
Memory<byte> buffer = writer.GetMemory(8 * 1024);
int length = await Stream.ReadAsync(buffer, cancellationTokenSource.Token);
if (length == 0)
{
break;
}
writer.Advance(length);
FlushResult result = await writer.FlushAsync();
if (result.IsCanceled || result.IsCompleted)
{
break;
}
}
}
catch (Exception ex)
{
if (Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
Logger.Instance.Error(ex);
}
}
finally
{
await writer.CompleteAsync();
Close();
}
}
private async Task ProcessReader()
{
PipeReader reader = pipe.Reader;
try
{
while (cancellationTokenSource.IsCancellationRequested == false)
{
ReadResult readResult = await reader.ReadAsync();
ReadOnlySequence<byte> buffer = readResult.Buffer;
if (buffer.IsEmpty && readResult.IsCompleted)
{
break;
}
if (buffer.Length > 0)
{
SequencePosition end = await ReadPacket(buffer).ConfigureAwait(false);
reader.AdvanceTo(end);
}
}
}
catch (Exception ex)
{
if (Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
Logger.Instance.Error(ex);
}
}
finally
{
await reader.CompleteAsync();
Close();
}
}
private unsafe int ReaderHead(ReadOnlySequence<byte> buffer)
{
Span<byte> span = stackalloc byte[4];
buffer.Slice(0, 4).CopyTo(span);
return span.ToInt32();
}
private async Task<SequencePosition> ReadPacket(ReadOnlySequence<byte> buffer)
{
//不分包
if (framing == false)
{
foreach (var memory in buffer)
{
try
{
await callback.Receive(this, memory, this.userToken).ConfigureAwait(false);
}
catch (Exception)
{
}
}
return buffer.End;
}
//分包
while (buffer.Length > 4)
{
//读取头
int length = ReaderHead(buffer);
if (buffer.Length < length + 4)
{
break;
}
//拼接数据
ReadOnlySequence<byte> cache = buffer.Slice(4, length);
foreach (var memory in cache)
{
bufferCache.AddRange(memory);
}
try
{
await callback.Receive(this, bufferCache.Data.Slice(0, bufferCache.Size), this.userToken).ConfigureAwait(false);
}
catch (Exception)
{
}
bufferCache.Clear();
//分割去掉已使用的数据
buffer = buffer.Slice(4 + length);
}
return buffer.Start;
}
private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);
public async Task SendAsync(ReadOnlyMemory<byte> data)
{
await semaphoreSlim.WaitAsync();
try
{
await Stream.WriteAsync(data, cancellationTokenSource.Token);
await Stream.FlushAsync();
}
catch (Exception ex)
{
if (Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
Logger.Instance.Error(ex);
}
}
finally
{
semaphoreSlim.Release();
}
}
public void Close()
{
callback = null;
userToken = null;
cancellationTokenSource?.Cancel();
pipe = null;
bufferCache.Clear(true);
Stream?.Close();
Stream?.Dispose();
}
public override string ToString()
{
return $"TransactionId:{TransactionId},TransportName:{TransportName},ProtocolType:{ProtocolType},Type:{Type},Direction:{Direction},IPEndPoint:{IPEndPoint},RemoteMachineName:{RemoteMachineName}";
}
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net7.0</TargetFrameworks>
<TargetFrameworks>net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -15,6 +15,7 @@
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>cmonitor.snk</AssemblyOriginatorKeyFile>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -107,6 +108,9 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\cmonitor.libs\cmonitor.libs.csproj" />
@@ -121,7 +125,7 @@
<TrimmerRootAssembly Include="NAudio" />
<TrimmerRootAssembly Include="SharpDX" />
</ItemGroup>
<ItemGroup >
<ItemGroup>
<PackageReference Include="MemoryPack" Version="1.10.0" />
</ItemGroup>

View File

@@ -3,13 +3,14 @@ using common.libs.extends;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace cmonitor.plugins.tunnel.server
{
public sealed class TunnelBindServer
{
public Func<object, Socket,Task> OnTcpConnected { get; set; } = async (state, socket) => { await Task.CompletedTask; };
public Func<object, UdpClient,Task> OnUdpConnected { get; set; } = async (state, udpClient) => { await Task.CompletedTask; };
public Func<object, Socket, Task> OnTcpConnected { get; set; } = async (state, socket) => { await Task.CompletedTask; };
public Func<object, UdpClient, Task> OnUdpConnected { get; set; } = async (state, udpClient) => { await Task.CompletedTask; };
private ConcurrentDictionary<int, AsyncUserToken> acceptBinds = new ConcurrentDictionary<int, AsyncUserToken>();
@@ -107,9 +108,21 @@ namespace cmonitor.plugins.tunnel.server
try
{
IPEndPoint ep = new IPEndPoint(IPAddress.Any, IPEndPoint.MinPort);
byte[] _ = token.UdpClient.EndReceive(result, ref ep);
byte[] bytes = token.UdpClient.EndReceive(result, ref ep);
string command = Encoding.UTF8.GetString(bytes);
if (command == "snltty.end")
{
OnUdpConnected(token.State, token.UdpClient);
return;
}
else if (command == "snltty.test")
{
token.UdpClient.Send(bytes);
}
result = token.UdpClient.BeginReceive(ReceiveCallbackUdp, token);
}
catch (Exception)
{

View File

@@ -68,6 +68,7 @@ namespace cmonitor.plugins.tunnel.transport
BindAndTTL(tunnelTransportInfo1);
if (await OnSendConnectBegin(tunnelTransportInfo1) == false)
{
tunnelBindServer.RemoveBind(tunnelTransportInfo1.Local.Local.Port, true);
return null;
}
ITunnelConnection connection = await WaitReverse(tunnelTransportInfo1);
@@ -76,8 +77,10 @@ namespace cmonitor.plugins.tunnel.transport
await OnSendConnectSuccess(tunnelTransportInfo);
return connection;
}
tunnelBindServer.RemoveBind(tunnelTransportInfo1.Local.Local.Port, true);
}
await OnSendConnectFail(tunnelTransportInfo);
return null;
}
@@ -159,35 +162,18 @@ namespace cmonitor.plugins.tunnel.transport
foreach (IPEndPoint ep in eps.Where(c => NotIPv6Support(c.Address) == false))
{
Socket targetSocket = new(ep.AddressFamily, SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp);
try
{
targetSocket.IPv6Only(ep.Address.AddressFamily, false);
targetSocket.KeepAlive();
targetSocket.ReuseBind(new IPEndPoint(ep.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any, tunnelTransportInfo.Local.Local.Port));
IAsyncResult result = targetSocket.BeginConnect(ep, null, null);
if (Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
Logger.Instance.Warning($"{Name} connect to {tunnelTransportInfo.Remote.MachineName} {ep}");
}
int times = ep.Address.Equals(tunnelTransportInfo.Remote.Remote.Address) ? 10 : 5;
for (int i = 0; i < times; i++)
{
if (result.IsCompleted)
{
break;
}
await Task.Delay(20);
}
try
{
if (result.IsCompleted == false)
{
targetSocket.SafeClose();
continue;
}
targetSocket.EndConnect(result);
await targetSocket.ConnectAsync(ep).WaitAsync(TimeSpan.FromMilliseconds(ep.Address.Equals(tunnelTransportInfo.Remote.Remote.Address) ? 500 : 100));
SslStream sslStream = new SslStream(new NetworkStream(targetSocket), true, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions { EnabledSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13 });
@@ -309,7 +295,7 @@ namespace cmonitor.plugins.tunnel.transport
}
catch (Exception ex)
{
if(Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
if (Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
Logger.Instance.Error(ex);
}

View File

@@ -0,0 +1,502 @@
using cmonitor.client.tunnel;
using cmonitor.config;
using common.libs;
using common.libs.extends;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace cmonitor.plugins.tunnel.transport
{
public sealed class TransportUdpQuic : ITunnelTransport
{
public string Name => "quic";
public string Label => "基于UDP的Quic";
public TunnelProtocolType ProtocolType => TunnelProtocolType.Quic;
public Func<TunnelTransportInfo, Task<bool>> OnSendConnectBegin { get; set; } = async (info) => { return await Task.FromResult<bool>(false); };
public Func<TunnelTransportInfo, Task> OnSendConnectFail { get; set; } = async (info) => { await Task.CompletedTask; };
public Func<TunnelTransportInfo, Task> OnSendConnectSuccess { get; set; } = async (info) => { await Task.CompletedTask; };
public Action<ITunnelConnection> OnConnected { get; set; } = (state) => { };
private byte[] UdpTtlBytes = Encoding.UTF8.GetBytes("snltty.ttl");
private byte[] UdpEndBytes = Encoding.UTF8.GetBytes("snltty.end");
private X509Certificate serverCertificate;
private IPEndPoint quicEP = new IPEndPoint(IPAddress.Any, 0);
private ConcurrentDictionary<int, AsyncUserToken> udpListeners = new ConcurrentDictionary<int, AsyncUserToken>();
private readonly Config config;
public TransportUdpQuic(Config config)
{
this.config = config;
_ = QuicStart();
}
private async Task QuicStart()
{
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
string path = Path.GetFullPath(config.Data.Client.Tunnel.Certificate);
if (File.Exists(path))
{
serverCertificate = new X509Certificate(path, config.Data.Client.Tunnel.Password);
}
else
{
Logger.Instance.Error($"file {path} not found");
Environment.Exit(0);
}
QuicListener listener = await QuicListener.ListenAsync(new QuicListenerOptions
{
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
ListenBacklog = int.MaxValue,
ListenEndPoint = new IPEndPoint(IPAddress.Any, 0),
ConnectionOptionsCallback = (connection, hello, token) =>
{
return ValueTask.FromResult(new QuicServerConnectionOptions
{
MaxInboundBidirectionalStreams = 65535,
MaxInboundUnidirectionalStreams = 65535,
DefaultCloseErrorCode = 0x0a,
DefaultStreamErrorCode = 0x0b,
ServerAuthenticationOptions = new SslServerAuthenticationOptions
{
ServerCertificate = serverCertificate,
EnabledSslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13,
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 }
}
});
}
});
quicEP = new IPEndPoint(IPAddress.Loopback, listener.LocalEndPoint.Port);
while (true)
{
QuicConnection quicConnection = await listener.AcceptConnectionAsync();
QuicStream quicStream = await quicConnection.AcceptInboundStreamAsync();
if (udpListeners.TryGetValue(quicConnection.RemoteEndPoint.Port, out AsyncUserToken token))
{
await OnUdpConnected(token.State, quicStream, token.TargetEP);
}
}
}
}
public async Task<ITunnelConnection> ConnectAsync(TunnelTransportInfo tunnelTransportInfo)
{
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
if (QuicListener.IsSupported == false)
{
await OnSendConnectFail(tunnelTransportInfo);
return null;
}
}
if (tunnelTransportInfo.Direction == TunnelDirection.Forward)
{
//正向连接
if (await OnSendConnectBegin(tunnelTransportInfo) == false)
{
return null;
}
await Task.Delay(500);
ITunnelConnection connection = await ConnectForward(tunnelTransportInfo);
if (connection != null)
{
await OnSendConnectSuccess(tunnelTransportInfo);
return connection;
}
}
else if (tunnelTransportInfo.Direction == TunnelDirection.Reverse)
{
//反向连接
TunnelTransportInfo tunnelTransportInfo1 = tunnelTransportInfo.ToJsonFormat().DeJson<TunnelTransportInfo>();
BindListen(tunnelTransportInfo1.Local.Local, tunnelTransportInfo1, quicEP);
BindAndTTL(tunnelTransportInfo1);
if (await OnSendConnectBegin(tunnelTransportInfo1) == false)
{
RemoveBind(tunnelTransportInfo1.Local.Local.Port, true);
return null;
}
ITunnelConnection connection = await WaitReverse(tunnelTransportInfo1);
if (connection != null)
{
await OnSendConnectSuccess(tunnelTransportInfo);
return connection;
}
RemoveBind(tunnelTransportInfo1.Local.Local.Port, true);
}
await OnSendConnectFail(tunnelTransportInfo);
return null;
}
public void OnBegin(TunnelTransportInfo tunnelTransportInfo)
{
if (tunnelTransportInfo.Direction == TunnelDirection.Forward)
{
BindListen(tunnelTransportInfo.Local.Local, tunnelTransportInfo, quicEP);
}
Task.Run(async () =>
{
if (tunnelTransportInfo.Direction == TunnelDirection.Forward)
{
BindAndTTL(tunnelTransportInfo);
}
else
{
ITunnelConnection connection = await ConnectForward(tunnelTransportInfo);
if (connection != null)
{
OnConnected(connection);
await OnSendConnectSuccess(tunnelTransportInfo);
}
else
{
await OnSendConnectFail(tunnelTransportInfo);
}
}
});
}
private async Task<ITunnelConnection> ConnectForward(TunnelTransportInfo tunnelTransportInfo)
{
//要连接哪些IP
IPAddress[] localIps = tunnelTransportInfo.Remote.LocalIps.Where(c => c.Equals(tunnelTransportInfo.Remote.Local.Address) == false).ToArray();
List<IPEndPoint> eps = new List<IPEndPoint>();
//先尝试内网ipv4
foreach (IPAddress item in localIps.Where(c => c.AddressFamily == AddressFamily.InterNetwork))
{
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Local.Port));
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Remote.Port));
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Remote.Port + 1));
}
//在尝试外网
eps.AddRange(new List<IPEndPoint>{
new IPEndPoint(tunnelTransportInfo.Remote.Remote.Address,tunnelTransportInfo.Remote.Remote.Port),
new IPEndPoint(tunnelTransportInfo.Remote.Remote.Address,tunnelTransportInfo.Remote.Remote.Port+1),
});
//再尝试IPV6
foreach (IPAddress item in localIps.Where(c => c.AddressFamily == AddressFamily.InterNetworkV6))
{
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Local.Port));
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Remote.Port));
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Remote.Port + 1));
}
if (Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
Logger.Instance.Warning($"{Name} connect to {tunnelTransportInfo.Remote.MachineName} {string.Join("\r\n", eps.Select(c => c.ToString()))}");
}
foreach (IPEndPoint ep in eps.Where(c => NotIPv6Support(c.Address) == false))
{
IPEndPoint local = new IPEndPoint(ep.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any, tunnelTransportInfo.Local.Local.Port);
UdpClient udpClient = new UdpClient(local.AddressFamily);
udpClient.Client.ReuseBind(local);
udpClient.Client.WindowsUdpBug();
try
{
udpClient.Send(UdpTtlBytes, ep);
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync().WaitAsync(TimeSpan.FromMilliseconds(ep.Address.Equals(tunnelTransportInfo.Remote.Remote.Address) ? 500 : 100));
udpClient.Send(UdpEndBytes, ep);
int port = BindForward(udpClient, ep);
QuicConnection connection = await QuicConnection.ConnectAsync(new QuicClientConnectionOptions
{
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, port),
LocalEndPoint = new IPEndPoint(IPAddress.Any, 0),
DefaultCloseErrorCode = 0x0a,
DefaultStreamErrorCode = 0x0b,
ClientAuthenticationOptions = new SslClientAuthenticationOptions
{
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
EnabledSslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13,
RemoteCertificateValidationCallback = (sender, certificate, chain, errors) =>
{
return true;
}
}
}).AsTask().WaitAsync(TimeSpan.FromMilliseconds(ep.Address.Equals(tunnelTransportInfo.Remote.Remote.Address) ? 500 : 100));
QuicStream quicStream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);
return new TunnelConnectionQuic
{
Stream = quicStream,
IPEndPoint = ep,
TransactionId = tunnelTransportInfo.TransactionId,
RemoteMachineName = tunnelTransportInfo.Remote.MachineName,
TransportName = Name,
Direction = tunnelTransportInfo.Direction,
ProtocolType = ProtocolType,
Type = TunnelType.P2P,
Mode = TunnelMode.Client,
Label = string.Empty,
};
}
catch (Exception)
{
udpClient.Close();
udpClient.Dispose();
}
}
return null;
}
private int BindForward(UdpClient serverUdpClient, IPEndPoint server)
{
IPAddress localIP = NetworkHelper.IPv6Support ? IPAddress.IPv6Any : IPAddress.Any;
UdpClient udpClient = new UdpClient(localIP.AddressFamily);
udpClient.Client.WindowsUdpBug();
AsyncUserTokenForward token = new AsyncUserTokenForward { SourceUdpClient = udpClient, TargetUdpClient = serverUdpClient, TargetEP = server };
IAsyncResult result = udpClient.BeginReceive(ReceiveCallbackUdpForward, token);
return (udpClient.Client.LocalEndPoint as IPEndPoint).Port;
}
private void ReceiveCallbackUdpForward(IAsyncResult result)
{
AsyncUserTokenForward token = result.AsyncState as AsyncUserTokenForward;
try
{
byte[] bytes = token.SourceUdpClient.EndReceive(result, ref token.tempEP);
token.TargetUdpClient.Send(bytes, token.TargetEP);
if (token.Received == false)
{
token.Received = true;
AsyncUserTokenForward token1 = new AsyncUserTokenForward { SourceUdpClient = token.TargetUdpClient, TargetUdpClient = token.SourceUdpClient, TargetEP = token.tempEP, Received = true };
result = token.TargetUdpClient.BeginReceive(ReceiveCallbackUdpForward, token1);
}
result = token.SourceUdpClient.BeginReceive(ReceiveCallbackUdpForward, token);
}
catch (Exception)
{
token.Clear();
}
}
private void RemoveBind(int port, bool close)
{
if (udpListeners.TryRemove(port, out AsyncUserToken token))
{
if (close)
{
token.Clear();
}
}
}
private void BindListen(IPEndPoint local, TunnelTransportInfo state, IPEndPoint targetEP)
{
IPAddress localIP = NetworkHelper.IPv6Support ? IPAddress.IPv6Any : IPAddress.Any;
UdpClient udpClient = new UdpClient(localIP.AddressFamily);
udpClient.Client.ReuseBind(new IPEndPoint(localIP, local.Port));
udpClient.Client.WindowsUdpBug();
AsyncUserToken token = new AsyncUserToken { SourceUdpClient = udpClient, State = state, TargetEP = targetEP, Port = local.Port };
IAsyncResult result = udpClient.BeginReceive(ReceiveCallbackUdp, token);
udpListeners.AddOrUpdate(local.Port, token, (a, b) => token);
}
private void ReceiveCallbackUdp(IAsyncResult result)
{
AsyncUserToken token = result.AsyncState as AsyncUserToken;
try
{
byte[] bytes = token.SourceUdpClient.EndReceive(result, ref token.tempEP);
if (token.Received == false)
{
if (bytes.AsSpan().SequenceEqual(UdpEndBytes))
{
token.Received = true;
}
else
{
token.SourceUdpClient.Send(bytes, token.tempEP);
}
}
else
{
if (token.TargetUdpClient == null)
{
token.TargetUdpClient = new UdpClient();
token.TargetUdpClient.Client.WindowsUdpBug();
token.TargetUdpClient.Send(bytes, token.TargetEP);
AsyncUserToken token1 = new AsyncUserToken { SourceUdpClient = token.TargetUdpClient, TargetUdpClient = token.SourceUdpClient, Received = true, TargetEP = token.tempEP, Port = token.Port };
result = token1.SourceUdpClient.BeginReceive(ReceiveCallbackUdp, token1);
}
else
{
token.TargetUdpClient.Send(bytes, token.TargetEP);
}
}
result = token.SourceUdpClient.BeginReceive(ReceiveCallbackUdp, token);
}
catch (Exception)
{
RemoveBind(token.Port, true);
}
}
private void BindAndTTL(TunnelTransportInfo tunnelTransportInfo)
{
//给对方发送TTL消息
IPAddress[] localIps = tunnelTransportInfo.Remote.LocalIps.Where(c => c.Equals(tunnelTransportInfo.Remote.Local.Address) == false).ToArray();
List<IPEndPoint> eps = new List<IPEndPoint>();
foreach (IPAddress item in localIps)
{
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Local.Port));
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Remote.Port));
eps.Add(new IPEndPoint(item, tunnelTransportInfo.Remote.Remote.Port + 1));
}
eps.AddRange(new List<IPEndPoint>{
new IPEndPoint(tunnelTransportInfo.Remote.Remote.Address,tunnelTransportInfo.Remote.Remote.Port),
new IPEndPoint(tunnelTransportInfo.Remote.Remote.Address,tunnelTransportInfo.Remote.Remote.Port+1),
});
foreach (var ip in eps.Where(c => NotIPv6Support(c.Address) == false))
{
try
{
IPEndPoint ep = new IPEndPoint(ip.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any, tunnelTransportInfo.Local.Local.Port);
using UdpClient udpClient = new UdpClient(ep.AddressFamily);
udpClient.Client.ReuseBind(ep);
udpClient.Send(new byte[] { 0 });
}
catch (Exception ex)
{
if (Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
Logger.Instance.Error(ex);
}
}
}
}
private ConcurrentDictionary<string, TaskCompletionSource<ITunnelConnection>> reverseDic = new ConcurrentDictionary<string, TaskCompletionSource<ITunnelConnection>>();
private async Task<ITunnelConnection> WaitReverse(TunnelTransportInfo tunnelTransportInfo)
{
TaskCompletionSource<ITunnelConnection> tcs = new TaskCompletionSource<ITunnelConnection>();
reverseDic.TryAdd(tunnelTransportInfo.Remote.MachineName, tcs);
try
{
ITunnelConnection connection = await tcs.Task.WaitAsync(TimeSpan.FromMilliseconds(5000));
return connection;
}
catch (Exception)
{
}
finally
{
reverseDic.TryRemove(tunnelTransportInfo.Remote.MachineName, out _);
}
return null;
}
public void OnFail(TunnelTransportInfo tunnelTransportInfo)
{
RemoveBind(tunnelTransportInfo.Local.Local.Port, true);
if (reverseDic.TryRemove(tunnelTransportInfo.Remote.MachineName, out TaskCompletionSource<ITunnelConnection> tcs))
{
tcs.SetResult(null);
}
}
public void OnSuccess(TunnelTransportInfo tunnelTransportInfo)
{
RemoveBind(tunnelTransportInfo.Local.Local.Port, false);
if (reverseDic.TryRemove(tunnelTransportInfo.Remote.MachineName, out TaskCompletionSource<ITunnelConnection> tcs))
{
tcs.SetResult(null);
}
}
private async Task OnUdpConnected(TunnelTransportInfo state, QuicStream stream, IPEndPoint remoteEndpoint)
{
if (state.TransportName == Name)
{
try
{
TunnelConnectionQuic result = new TunnelConnectionQuic
{
RemoteMachineName = state.Remote.MachineName,
Direction = state.Direction,
ProtocolType = TunnelProtocolType.Quic,
Stream = stream,
Type = TunnelType.P2P,
Mode = TunnelMode.Server,
TransactionId = state.TransactionId,
TransportName = state.TransportName,
IPEndPoint = remoteEndpoint,
Label = string.Empty,
};
if (reverseDic.TryRemove(state.Remote.MachineName, out TaskCompletionSource<ITunnelConnection> tcs))
{
tcs.SetResult(result);
return;
}
OnConnected(result);
}
catch (Exception ex)
{
if (Logger.Instance.LoggerLevel <= LoggerTypes.DEBUG)
{
Logger.Instance.Error(ex);
}
}
}
await Task.CompletedTask;
}
private bool NotIPv6Support(IPAddress ip)
{
return ip.AddressFamily == AddressFamily.InterNetworkV6 && (NetworkHelper.IPv6Support == false);
}
public sealed class AsyncUserToken : AsyncUserTokenForward
{
public TunnelTransportInfo State { get; set; }
public int Port { get; set; }
}
public class AsyncUserTokenForward
{
public UdpClient SourceUdpClient { get; set; }
public UdpClient TargetUdpClient { get; set; }
public IPEndPoint TargetEP { get; set; }
public IPEndPoint tempEP = new IPEndPoint(IPAddress.Any, 0);
public bool Received { get; set; }
public void Clear()
{
try
{
SourceUdpClient?.Close();
TargetUdpClient?.Close();
}
catch (Exception)
{
}
}
}
}
}

View File

@@ -2,10 +2,11 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net8.0</TargetFrameworks>
<PublishAot>false</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release;ReleaseNetwork;ReleaseMonitor</Configurations>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
@@ -27,21 +28,10 @@
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseMonitor|net7.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='ReleaseNetwork|net7.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>

View File

@@ -50,9 +50,15 @@ namespace common.libs.extends
{
}
finally
{
try
{
socket.Close();
}
catch (Exception)
{
}
}
}
}