如何通过`HTTP2`实现TCP的内网穿透???

8次阅读
没有评论

可能有人很疑惑应用层 转发传输层?,为什么会有这样的需求啊???哈哈技术无所不用其极,由于一些场景下,对于一个服务器存在某一个内部网站中,但是对于这个服务器它没有访问外网的权限,虽然也可以申请端口访问外部指定的ip+端口,但是对于访问服务内部的TCP的时候我们就会发现忘记申请了!这个时候我们又要提交申请,又要等审批,然后开通端口,对于这个步骤不是一般的麻烦,所以我在想是否可以直接利用现有的Http网关的端口进行转发内部的TCP服务?这个时候我询问了我们的老九大佬,由于我之前也做过通过H2实现HTTP内网穿透,可以利用H2将内部网络中的服务映射出来,但是由于底层是基于yarp的一些方法实现,所以并没有考虑过TCP,然后于老九大佬交流深究,决定尝试验证可行性,然后我们的Taibai项目就诞生了,为什么叫Taibai?您仔细看看这个拼音,翻译过来就是太白,确实全称应该叫太白金星,寓意上天遁地无所不能!下面我们介绍一下具体实现逻辑,确实您仔细看会发现实现是真的超级简单的!

创建Core项目用于共用的核心类库

创建项目名Taibai.Core

下面几个方法都是用于操作Stream的类

DelegatingStream.cs

namespace Taibai.Core;/// <summary>/// 委托流/// </summary>public abstract class DelegatingStream : Stream{    /// <summary>
    /// 获取所包装的流对象
    /// </summary>
    protected readonly Stream Inner;    /// <summary>
    /// 委托流
    /// </summary>
    /// <param name="inner"></param>
    public DelegatingStream(Stream inner)
    {        this.Inner = inner;
    }    /// <inheritdoc/>
    public override bool CanRead => Inner.CanRead;    /// <inheritdoc/>
    public override bool CanSeek => Inner.CanSeek;    /// <inheritdoc/>
    public override bool CanWrite => Inner.CanWrite;    /// <inheritdoc/>
    public override long Length => Inner.Length;    /// <inheritdoc/>
    public override bool CanTimeout => Inner.CanTimeout;    /// <inheritdoc/>
    public override int ReadTimeout
    {        get => Inner.ReadTimeout;        set => Inner.ReadTimeout = value;
    }    /// <inheritdoc/>
    public override int WriteTimeout
    {        get => Inner.WriteTimeout;        set => Inner.WriteTimeout = value;
    }    /// <inheritdoc/>
    public override long Position
    {        get => Inner.Position;        set => Inner.Position = value;
    }    /// <inheritdoc/>
    public override void Flush()
    {
        Inner.Flush();
    }    /// <inheritdoc/>
    public override Task FlushAsync(CancellationToken cancellationToken)
    {        return Inner.FlushAsync(cancellationToken);
    }    /// <inheritdoc/>
    public override int Read(byte[] buffer, int offset, int count)
    {        return Inner.Read(buffer, offset, count);
    }    /// <inheritdoc/>
    public override int Read(Span<byte> destination)
    {        return Inner.Read(destination);
    }    /// <inheritdoc/>
    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {        return Inner.ReadAsync(buffer, offset, count, cancellationToken);
    }    /// <inheritdoc/>
    public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
    {        return Inner.ReadAsync(destination, cancellationToken);
    }    /// <inheritdoc/>
    public override long Seek(long offset, SeekOrigin origin)
    {        return Inner.Seek(offset, origin);
    }    /// <inheritdoc/>
    public override void SetLength(long value)
    {
        Inner.SetLength(value);
    }    /// <inheritdoc/>
    public override void Write(byte[] buffer, int offset, int count)
    {
        Inner.Write(buffer, offset, count);
    }    /// <inheritdoc/>
    public override void Write(ReadOnlySpan<byte> source)
    {
        Inner.Write(source);
    }    /// <inheritdoc/>
    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {        return Inner.WriteAsync(buffer, offset, count, cancellationToken);
    }    /// <inheritdoc/>
    public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
    {        return Inner.WriteAsync(source, cancellationToken);
    }    /// <inheritdoc/>
    public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
    {        return TaskToAsyncResult.Begin(ReadAsync(buffer, offset, count), callback, state);
    }    /// <inheritdoc/>
    public override int EndRead(IAsyncResult asyncResult)
    {        return TaskToAsyncResult.End<int>(asyncResult);
    }    /// <inheritdoc/>
    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback,        object? state)
    {        return TaskToAsyncResult.Begin(WriteAsync(buffer, offset, count), callback, state);
    }    /// <inheritdoc/>
    public override void EndWrite(IAsyncResult asyncResult)
    {
        TaskToAsyncResult.End(asyncResult);
    }    /// <inheritdoc/>
    public override int ReadByte()
    {        return Inner.ReadByte();
    }    /// <inheritdoc/>
    public override void WriteByte(byte value)
    {
        Inner.WriteByte(value);
    }    /// <inheritdoc/>
    public sealed override void Close()
    {        base.Close();
    }
}

SafeWriteStream.cs

public class SafeWriteStream(Stream inner) : DelegatingStream(inner){    private readonly SemaphoreSlim semaphoreSlim = new(1, 1);    public override async ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
    {        try
        {            await this.semaphoreSlim.WaitAsync(CancellationToken.None);            await base.WriteAsync(source, cancellationToken);            await this.FlushAsync(cancellationToken);
        }        finally
        {            this.semaphoreSlim.Release();
        }
    }    public override ValueTask DisposeAsync()
    {        this.semaphoreSlim.Dispose();        return this.Inner.DisposeAsync();
    }    protected override void Dispose(bool disposing)
    {        this.semaphoreSlim.Dispose();        this.Inner.Dispose();
    }
}

创建服务端

创建一个WebAPI的项目项目名Taibai.Server并且依赖Taibai.Core项目

创建ServerService.cs,这个类是用于管理内网的客户端的,这个一般是部署在内网服务器上,用于将内网的端口映射出来,但是我们的Demo只实现了简单的管理不做端口的管理。

using System.Collections.Concurrent;using Microsoft.AspNetCore.Http.Features;using Microsoft.AspNetCore.Http.Timeouts;using Taibai.Core;namespace Taibai.Server;public static class ServerService{    private static readonly ConcurrentDictionary<string, (CancellationToken, Stream)> ClusterConnections = new();    public static async Task StartAsync(HttpContext context)
    {        // 如果不是http2协议,我们不处理, 因为我们只支持http2
        if (context.Request.Protocol != HttpProtocol.Http2)
        {            return;
        }        // 获取query
        var query = context.Request.Query;        // 我们需要强制要求name参数
        var name = query["name"];        if (string.IsNullOrEmpty(name))
        {
            context.Response.StatusCode = 400;
            Console.WriteLine("Name is required");            return;
        }
        
        Console.WriteLine("Accepted connection from " + name);        // 获取http2特性
        var http2Feature = context.Features.Get<IHttpExtendedConnectFeature>();        
        // 禁用超时
        context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();        // 得到双工流
        var stream = new SafeWriteStream(await http2Feature.AcceptAsync());        // 将其添加到集合中,以便我们可以在其他地方使用
        CreateConnectionChannel(name, context.RequestAborted, stream);        // 注册取消连接
        context.RequestAborted.Register(() =>
        {            // 当取消时,我们需要从集合中删除
            ClusterConnections.TryRemove(name, out _);
        });        
        // 由于我们需要保持连接,所以我们需要等待,直到客户端主动断开连接。
        await Task.Delay(-1, context.RequestAborted);
    }    /// <summary>
    /// 通过名称获取连接
    /// </summary>
    /// <param name="host"></param>
    /// <returns></returns>
    public static (CancellationToken, Stream) GetConnectionChannel(string host)
    {        return ClusterConnections[host];
    }    /// <summary>
    /// 注册连接
    /// </summary>
    /// <param name="host"></param>
    /// <param name="cancellationToken"></param>
    /// <param name="stream"></param>
    public static void CreateConnectionChannel(string host, CancellationToken cancellationToken, Stream stream)
    {
        ClusterConnections.GetOrAdd(host,
            _ => (cancellationToken, stream));
    }
}

然后再创建ClientMiddleware.cs,并且继承IMiddleware,这个是我们本地使用的客户端链接的时候进入的中间件,再这个中间件会获取query中携带的name去找到指定的Stream,然后会将客户端的Stream和获取的server的Stream进行Copy,在这里他们会将读取的数据写入到对方的流中,这样就实现了双工通信

using Microsoft.AspNetCore.Http.Features;using Microsoft.AspNetCore.Http.Timeouts;using Taibai.Core;namespace Taibai.Server;public class ClientMiddleware : IMiddleware{    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {        
        // 如果不是http2协议,我们不处理, 因为我们只支持http2
        if (context.Request.Protocol != HttpProtocol.Http2)
        {            return;
        }        var name = context.Request.Query["name"];        if (string.IsNullOrEmpty(name))
        {
            context.Response.StatusCode = 400;
            Console.WriteLine("Name is required");            return;
        }
        
        Console.WriteLine("Accepted connection from " + name);        var http2Feature = context.Features.Get<IHttpExtendedConnectFeature>();
        context.Features.Get<IHttpRequestTimeoutFeature>()?.DisableTimeout();        // 得到双工流
        var stream = new SafeWriteStream(await http2Feature.AcceptAsync());        // 通过name找到指定的server链接,然后进行转发。
        var (cancellationToken, reader) = ServerService.GetConnectionChannel(name);        try
        {            // 注册取消连接
            cancellationToken.Register(() =>
            {
                Console.WriteLine("断开连接");
                stream.Close();
            });            // 得到客户端的流,然后给我们的SafeWriteStream,然后我们就可以进行转发了
            var socketStream = new SafeWriteStream(reader);            // 在这里他们会将读取的数据写入到对方的流中,这样就实现了双工通信,这个非常简单并且性能也不错。
            await Task.WhenAll(
                stream.CopyToAsync(socketStream, context.RequestAborted),
                socketStream.CopyToAsync(stream, context.RequestAborted)
            );
        }        catch (Exception e)
        {
            Console.WriteLine("断开连接" + e.Message);            throw;
        }
    }
}

打开Program.cs

using Taibai.Server;var builder = WebApplication.CreateBuilder(new WebApplicationOptions());

builder.Host.ConfigureHostOptions(host => { host.ShutdownTimeout = TimeSpan.FromSeconds(1d); });

builder.Services.AddSingleton<ClientMiddleware>();var app = builder.Build();

app.Map("/server", app =>
{
    app.Use(Middleware);    static async Task Middleware(HttpContext context, RequestDelegate _)
    {        await ServerService.StartAsync(context);
    }
});

app.Map("/client", app => { app.UseMiddleware<ClientMiddleware>(); });

app.Run();

在这里我们将server的所有路由都交过ServerService.StartAsync接管,再server会请求这个地址,

/client则给了ClientMiddleware中间件。

正文完
 0
评论(没有评论)