基於Yarp實現內網http穿透

jiulang發表於2021-07-08

Yarp介紹

YARP是微軟開源的用來代理伺服器的反向代理元件,可實現的功能類似於nginx。
基於YARP,開發者可以非常快速的開發一個效能不錯的小nginx,用於代理http(s)請求到上游的http(s)服務。

http穿透原理

同網現象

在http反向代理裡,代理伺服器總是上游服務的http客戶端,為了網路效能,實際上上游服務總是和代理服務處在同一個區域網。試問一個問題:在公網的小nginx,如何把請求代理到區域網的http伺服器?你會發現,小nginx做不到,因為小nginx所在公網伺服器,無法直接與區域網的http伺服器進行通訊。

http穿透

在tcp裡,進行連線時總是由客戶端發起,但當連線之後客戶端與服務端是平等的,他們之間可以雙向收發資料。只要公網小nginx與區域網的http伺服器存在tcp連線,我們可以使用這個連線做為httpClient的連線層,然後小nginx使用這個httpClient請求到區域網http伺服器,而從達到http穿透效果。

完整流程

image

基於Yarp的http穿透

main連線

我們可以使用websocket協議,建立main連線,主要有以下好處:

  • 共享代理伺服器監聽的http(s)埠
  • 利用websocket的ping-pong實現連線檢測
  • 利用websocket連線請求頭進行身份認證

接收區域網建立的連線

我們可以為kestrel編寫中介軟體,用獲取獲取區域網主動建立的tcp連線,這些連線與代理伺服器與瀏覽器之間的連線共享同一個伺服器埠,以下的listen.Use(transportService.OnConnectedAsync);是一個kestrel中介軟體。

public static IWebHostBuilder UseKestrelTransportChannel(this IWebHostBuilder hostBuilder)
{
    return hostBuilder.UseKestrel(kestrel =>
    {
        var transportService = kestrel.ApplicationServices.GetRequiredService<TransportChannelService>();
        var options = kestrel.ApplicationServices.GetRequiredService<IOptions<HttpMouseOptions>>().Value;

        var http = options.Listen.Http;
        if (http != null)
        {
            kestrel.Listen(http.IPAddress, http.Port, listen =>
            {
                listen.Use(transportService.OnConnectedAsync);
            });
        }

        var https = options.Listen.Https;
        if (https != null && File.Exists(https.Certificate.Path))
        {
            kestrel.Listen(https.IPAddress, https.Port, listen =>
            {
                listen.Protocols = HttpProtocols.Http1AndHttp2;
                listen.UseHttps(https.Certificate.Path, https.Certificate.Password);
                listen.Use(transportService.OnConnectedAsync);
            });
        }
    });
}

繫結連線到HttpClient

Yarp進行代理時,需要指定HttpMessageInvoker,HttpMessageInvoker實際是SocketsHttpHandler的包裝。而SocketsHttpHandler可以設定ConnectCallback屬性,用於指定連線。

private static HttpMessageInvoker CreateHttpClient(TransportChannelService transportChannelService)
{
    return new HttpMessageInvoker(new SocketsHttpHandler()
    {
        UseProxy = false,
        UseCookies = false,
        AllowAutoRedirect = false,
        AutomaticDecompression = DecompressionMethods.None,
        ConnectCallback = transportChannelService.CreateChannelAsync,
        SslOptions = new SslClientAuthenticationOptions
        {
            RemoteCertificateValidationCallback = delegate { return true; }
        }
    });
}

Yarp直接轉發

使用直接轉發中介軟體

/// <summary>
/// 配置中介軟體
/// </summary>
/// <param name="app"></param>
/// <param name="connectionService"></param>
/// <param name="httpForwarderService"></param> 
public void Configure(IApplicationBuilder app, IHostEnvironment hostEnvironment, ConnectionService connectionService, HttpForwarderService httpForwarderService)
{ 
    app.UseWebSockets();
    app.Use(connectionService.OnConnectedAsync);
    app.Use(httpForwarderService.SendAsync);
}

通過請求的域名,找到區域網要轉發的最終伺服器地址,做為yarp的請求地址。

/// <summary>
/// 傳送http資料
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public async Task SendAsync(HttpContext httpContext, Func<Task> next)
{
    var clientDomain = httpContext.Request.Host.Host;
    if (this.connectionService.TryGetClientUpStream(clientDomain, out var clientUpstream))
    {
        var destPrefix = clientUpstream.ToString();
        if (this.options.CurrentValue.HttpRequest.TryGetValue(clientDomain, out var requestConfig) == false)
        {
            requestConfig = this.defaultRequestConfig;
        }
        await this.httpForwarder.SendAsync(httpContext, destPrefix, httpClient, requestConfig, this.transformer);
    } 
}

總結

基於kestrel和SocketsHttpHandler高度可定製化的擴充套件能力,結合Yarp元件,我們可以很方便的開發一個支援內網http穿透的公網http反向代理伺服器。如果把泛域名指向公網反向代理伺服器,最終實現一個二級域名繫結流量到一個區域網http伺服器的一對多功能。

相關文章