将错就错: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-ForX-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);
            }
        }
    }
}

(搞定)

posted @ 2023-01-19 19:35  dudu  阅读(332)  评论(0编辑  收藏  举报