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穿透效果。
完整流程
基於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服務器的一對多功能。