将错就错:DNS 错乱解析造成错误请求,借助 YARP 转发给正确的应用
最近园子在部署 IPv6 时遇到了一个非常奇怪的 dns 解析问题,当给非 www 二级域名(比如 q.cnblogs.com)添加 AAAA(IPv6) 记录后,部分用户访问 q.cnblogs.com 时会被错误地解析为 www.cnblogs.com 对应的 IPv4 地址,去掉 AAAA 解析就恢复正常。
为了对付这个不可控的奇怪问题(换了2家知名 dns 解析服务商都有同样的问题),我们采用了一个将错就错的变通方法,在 www 应用中借助反向代理将错误解析的请求转发给正确的应用处理,反向代理库用的是 YARP。
由于我们只转发请求,所以只需使用 YARP 的 Direct Forwarding 功能。
安装 nuget 包 Yarp.ReverseProxy,然后在 Startup 中注册 HttpForwarder
services.AddHttpForwarder();
专门实现一个中间件 ForwardRequestsMiddleware
用于转发请求:
查看代码 ForwardRequestsMiddleware
public class ForwardRequestsMiddleware : IDisposable
{
private static readonly string[] _forwardedSubdomains = new[] { "q", "news" };
private readonly RequestDelegate _next;
private readonly IHttpForwarder _forwarder;
private readonly HttpMessageInvoker _httpClient;
private readonly ForwarderRequestConfig _requestOptions;
private bool _disposed;
public static string[] Subdomains => _forwardedSubdomains;
public ForwardRequestsMiddleware(
RequestDelegate next,
IHttpForwarder forwarder)
{
_next = next;
_forwarder = forwarder;
_httpClient = new HttpMessageInvoker(new SocketsHttpHandler()
{
UseProxy = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
UseCookies = false,
ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),
ConnectTimeout = TimeSpan.FromSeconds(15),
});
_requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) };
}
public async Task InvokeAsync(HttpContext context)
{
var host = context.Request.Host.Host;
if (host.Equals(ConstStrings.DefaultHost, StringComparison.OrdinalIgnoreCase) ||
!host.Contains(".") ||
!_forwardedSubdomains.Any(d => host.Equals($"{d}.{ConstStrings.RootDomain}", StringComparison.OrdinalIgnoreCase)))
{
await _next(context);
return;
}
var serviceName = host.Substring(0, host.IndexOf(".")) + "_web";
await _forwarder.SendAsync(context, $"http://{serviceName}", _httpClient, _requestOptions);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) _httpClient?.Dispose();
_disposed = true;
}
}
实现后测试时发现转发时少了原始请求的 Host 请求头,而我们的应用需要这个请求头,进一步了解后得知默认使用的 StructuredTransformer 会自动去除原始 Host 请求头。
于是自己实现一个 CustomTransformer:
private class CustomTransformer : HttpTransformer
{
public override ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix)
{
return base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);
}
}
通过 IHttpForwarder.SendAsync
方法第5个参数传入
await _forwarder.SendAsync(context, $"http://{serviceName}", _httpClient, _requestOptions, _transformer);
继续测试时发现转发时少了 X-Forwarded-For
与 X-Forwarded-Proto
请求头,StructuredTransformer 转发时会加上这些请求头,但 StructuredTransformer 被标记为 internal,无法重用也无法定制,只能自己想方法在 CustomTransformer 中实现。
参考 YARP 的源码经过一番折腾,终于实现了!
查看代码 CustomTransformer
private class CustomTransformer : HttpTransformer
{
private readonly RequestTransform[] _requestTransforms;
public CustomTransformer()
{
_requestTransforms = new RequestTransform[]
{
new RequestHeaderXForwardedForTransform("X-Forwarded-For", ForwardedTransformActions.Set),
new RequestHeaderXForwardedProtoTransform("X-Forwarded-Proto", ForwardedTransformActions.Set)
};
}
public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix)
{
await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);
if (_requestTransforms.Length == 0)
{
return;
}
var transformContext = new RequestTransformContext()
{
DestinationPrefix = destinationPrefix,
HttpContext = httpContext,
ProxyRequest = proxyRequest,
Path = httpContext.Request.Path,
Query = new QueryTransformContext(httpContext.Request),
HeadersCopied = true
};
foreach (var requestTransform in _requestTransforms)
{
await requestTransform.ApplyAsync(transformContext);
}
}
}
进一步改进中间件,改用 IForwarderHttpClientFactory
创建 HttpMessageInvoker
,之前的实现是手动 new,因此需要实现 IDisposable 接口。
使用 IForwarderHttpClientFactory
需要在 Startup 中注册 ForwarderHttpClientFactory
services.TryAddSingleton<IForwarderHttpClientFactory, ForwarderHttpClientFactory>();
中间件的最终实现如下:
查看代码 ForwardRequestsMiddleware
public class ForwardRequestsMiddleware
{
private static readonly string[] _forwardedSubdomains = new[] { "q", "news" };
private readonly RequestDelegate _next;
private readonly IHttpForwarder _forwarder;
private readonly IForwarderHttpClientFactory _httpClientFactory;
private readonly ForwarderRequestConfig _requestOptions;
private readonly HttpTransformer _transformer;
public ForwardRequestsMiddleware(
RequestDelegate next,
IHttpForwarder forwarder,
IForwarderHttpClientFactory httpClientFactory)
{
_next = next;
_forwarder = forwarder;
_httpClientFactory = httpClientFactory;
_requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(300) };
_transformer = new CustomTransformer();
}
public async Task InvokeAsync(HttpContext context)
{
var host = context.Request.Host.Host;
if (host.Equals(ConstStrings.DefaultHost, StringComparison.OrdinalIgnoreCase) ||
!host.Contains(".") ||
!_forwardedSubdomains.Any(d => host.Equals($"{d}.{ConstStrings.RootDomain}", StringComparison.OrdinalIgnoreCase)))
{
await _next(context);
return;
}
var serviceName = host.Substring(0, host.IndexOf(".")) + "_web";
var httpClient = _httpClientFactory.CreateClient(new ForwarderHttpClientContext
{
NewConfig = HttpClientConfig.Empty
});
await _forwarder.SendAsync(context, $"http://{serviceName}", httpClient, _requestOptions, _transformer);
}
private class CustomTransformer : HttpTransformer
{
private readonly RequestTransform[] _requestTransforms;
public CustomTransformer()
{
_requestTransforms = new RequestTransform[]
{
new RequestHeaderXForwardedForTransform("X-Forwarded-For", ForwardedTransformActions.Set),
new RequestHeaderXForwardedProtoTransform("X-Forwarded-Proto", ForwardedTransformActions.Set)
};
}
public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix)
{
await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix);
if (_requestTransforms.Length == 0)
{
return;
}
var transformContext = new RequestTransformContext()
{
DestinationPrefix = destinationPrefix,
HttpContext = httpContext,
ProxyRequest = proxyRequest,
Path = httpContext.Request.Path,
Query = new QueryTransformContext(httpContext.Request),
HeadersCopied = true
};
foreach (var requestTransform in _requestTransforms)
{
await requestTransform.ApplyAsync(transformContext);
}
}
}
}
(搞定)