在 C# 中为 gRPC 配置 HTTP 代理(无需环境变量)

发布: (2025年12月13日 GMT+8 20:50)
5 min read
原文: Dev.to

Source: Dev.to

配置 C# 中 gRPC 的 HTTP 代理(无需环境变量)

挑战

你有一个使用 Grpc.Core 的 C# gRPC 客户端,需要通过 HTTP 代理转发流量。听起来很简单,对吧?

其实并非如此。

常见的做法可能是:

  • 设置 http_proxy 环境变量 ✅ 有效,但会影响 所有 HTTP 流量
  • 使用带 HttpClientHandler.ProxyGrpc.Net.Client ✅ 干净,但需要迁移库
  • 设置 grpc.http_proxy 通道选项 ❌ 在 Grpc.Core 中无效

我需要 每个通道单独的代理配置,既不影响其他流量,也不想迁移库。于是我深入 gRPC C‑core 源码,弄清 http_proxy 实际是如何工作的。

源代码引用(gRPC v1.10.0)

文件用途
http_proxy.cc读取 http_proxy 环境变量,设置通道参数
http_connect_handshaker.cc向代理发送 HTTP CONNECT 请求
secure_channel_create.ccSSL/TLS 目标名称覆盖处理
channel.cc默认 authority 头部逻辑
ChannelOptions.csC# 通道选项常量

我的发现

当 gRPC 读取 http_proxy 环境变量时,它仅仅:

  1. 解析代理 URL
  2. 设置内部通道参数
  3. 在建立连接时使用这些参数

关键点:这些通道参数可以通过 C# 的 ChannelOption 访问!

解决方案

理解 HTTP CONNECT 隧道

HTTP 代理使用 CONNECT 方法创建 TCP 隧道:

┌────────┐         ┌───────┐         ┌────────────┐
│ Client │   TCP   │ Proxy │   TCP   │ gRPC Server│
└───┬────┘         └───┬───┘         └─────┬──────┘
    │                  │                   │
    │ TCP Connect      │                   │
    │─────────────────►│                   │
    │                  │                   │
    │ CONNECT host:port HTTP/1.1          │
    │─────────────────►│                   │
    │                  │                   │
    │                  │ TCP Connect       │
    │                  │──────────────────►│
    │                  │                   │
    │ HTTP/1.1 200 Connection established │
    │◄─────────────────│                   │
    │                  │                   │
    │◄──────────── TCP Tunnel ───────────►│
    │         (TLS + gRPC flows here)      │
    │                  │                   │

隧道建立后,TLS 握手和 gRPC 通信会透明地通过它。

三个关键通道选项

private Channel CreateChannel(string targetEndpoint, SslCredentials credentials, string proxyEndpoint)
{
    string proxy = proxyEndpoint?.Trim();

    if (!string.IsNullOrEmpty(proxy))
    {
        // 提取不带端口的主机名用于 SSL 验证
        string targetHost = targetEndpoint.Contains(":")
            ? targetEndpoint.Substring(0, targetEndpoint.LastIndexOf(':'))
            : targetEndpoint;

        var options = new[]
        {
            // 1. HTTP CONNECT 隧道目标 (host:port)
            new ChannelOption("grpc.http_connect_server", targetEndpoint),

            // 2. SSL SNI + 证书验证 (仅主机名)
            new ChannelOption(ChannelOptions.SslTargetNameOverride, targetHost),

            // 3. HTTP/2 :authority 头部 (host:port)
            new ChannelOption(ChannelOptions.DefaultAuthority, targetEndpoint)
        };

        // 通道连接到 PROXY,再隧道到 TARGET
        return new Channel(proxy, credentials, options);
    }

    // 直接连接
    return new Channel(targetEndpoint, credentials);
}

每个选项的作用

1. grpc.http_connect_server

目的:告诉 gRPC 要隧道到哪里。
效果:当 gRPC 连接到通道目标(代理)时,会发送:

CONNECT api.example.com:443 HTTP/1.1
Host: api.example.com:443

格式host:port(端口必填)。

2. SslTargetNameOverride

目的:为 SNI 与证书验证提供正确的主机名。
若不使用:gRPC 会为代理主机名发送 SNI 并验证代理的证书——这显然错误。
效果:TLS ClientHello 中的 SNI 为 api.example.com,证书校验也针对 api.example.com
格式hostname(不带端口)。

3. DefaultAuthority

目的:设置 HTTP/2 的 :authority 伪头。
若不使用:authority 会是代理地址,导致服务器路由错误。
效果

:method: POST
:scheme: https
:authority: api.example.com:443   ← 正确!
:path: /mypackage.MyService/MyMethod

格式host:port(端口通常用于服务器路由)。

完整流程图

┌─────────────────────────────────────────────────────────────────┐
│                        YOUR APPLICATION                          │
│  Channel target: proxy-server:8080                              │
│  Options:                                                        │
│    grpc.http_connect_server  = "api.example.com:443"            │
│    SslTargetNameOverride     = "api.example.com"                │
│    DefaultAuthority          = "api.example.com:443"            │
└──────────────────────────────┬──────────────────────────────────┘

          Step 1: TCP Connect  │

┌─────────────────────────────────────────────────────────────────┐
│                        HTTP PROXY                                │
│  Receives: CONNECT api.example.com:443 HTTP/1.1                 │
│  Action:   Opens TCP connection to api.example.com:443          │
│  Returns:  HTTP/1.1 200 Connection established                  │
└──────────────────────────────┬──────────────────────────────────┘

          Step 2: TCP Tunnel   │ (transparent passthrough)

┌─────────────────────────────────────────────────────────────────┐
│                     TLS HANDSHAKE                                │
│  ClientHello SNI: "api.example.com"    (SslTargetNameOverride)    │
│  Server sends certificate for: "api.example.com"                │
│  Client validates cert against: "api.example.com" ✓             │
│  [mTLS: Client certificate sent if configured]                  │
└─────────────────────────────────────────────────────────────────┘
Back to Blog

相关文章

阅读更多 »

异步 DNS

请提供您希望翻译的具体摘录或摘要内容,我才能为您进行简体中文翻译。