C#에서 환경 변수 없이 gRPC용 HTTP 프록시 구성

발행: (2025년 12월 13일 오후 09:50 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

Cover image for Configuring HTTP Proxy for gRPC in C# Without Environment Variables

The Challenge

C#에서 Grpc.Core를 사용하고 있는 gRPC 클라이언트가 HTTP 프록시를 통해 트래픽을 라우팅해야 합니다. 간단해 보이죠?

그렇지 않습니다.

보통 보게 되는 접근 방식:

  • http_proxy 환경 변수를 설정 ✅ 작동하지만 전체 HTTP 트래픽에 영향을 줍니다
  • Grpc.Net.ClientHttpClientHandler.Proxy 사용 ✅ 깔끔하지만 라이브러리 마이그레이션이 필요합니다
  • grpc.http_proxy 채널 옵션 설정 ❌ Grpc.Core에서는 동작하지 않음

다른 트래픽에 영향을 주지 않으면서 라이브러리를 마이그레이션하지 않고 채널별 프록시 설정이 필요했습니다. 그래서 http_proxy가 실제로 어떻게 동작하는지 이해하기 위해 gRPC C‑core 소스 코드를 파고들었습니다.

Source Code References (gRPC v1.10.0)

FilePurpose
http_proxy.cchttp_proxy 환경 변수를 읽고 채널 인자를 설정
http_connect_handshaker.cc프록시에게 HTTP CONNECT 요청을 전송
secure_channel_create.ccSSL/TLS 대상 이름 오버라이드 처리
channel.cc기본 authority 헤더 로직
ChannelOptions.csC# 채널 옵션 상수

What I Discovered

gRPC가 http_proxy 환경 변수를 존중할 때는 단순히:

  1. 프록시 URL을 파싱
  2. 내부 채널 인자를 설정
  3. 연결 시 해당 인자를 사용

핵심 인사이트: 이 채널 인자는 C#의 ChannelOption을 통해 접근 가능하다는 점입니다.

The Solution

Understanding HTTP CONNECT Tunneling

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 통신이 투명하게 그 안을 통과합니다.

The Three Magic Channel Options

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

    if (!string.IsNullOrEmpty(proxy))
    {
        // Extract hostname without port for SSL validation
        string targetHost = targetEndpoint.Contains(":")
            ? targetEndpoint.Substring(0, targetEndpoint.LastIndexOf(':'))
            : targetEndpoint;

        var options = new[]
        {
            // 1. HTTP CONNECT tunnel target (host:port)
            new ChannelOption("grpc.http_connect_server", targetEndpoint),

            // 2. SSL SNI + certificate validation (hostname only)
            new ChannelOption(ChannelOptions.SslTargetNameOverride, targetHost),

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

        // Channel connects to PROXY, tunnels to TARGET
        return new Channel(proxy, credentials, options);
    }

    // Direct connection
    return new Channel(targetEndpoint, credentials);
}

What Each Option Does

1. grpc.http_connect_server

Purpose: gRPC에게 터널링할 대상을 알려줍니다.
Effect: 채널이 프록시(채널 대상)와 연결될 때 다음과 같이 전송합니다:

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

Format: host:port (포트는 필수)

2. SslTargetNameOverride

Purpose: SNI와 인증서 검증에 사용할 SSL/TLS 호스트명.
Problem without it: gRPC가 프록시 호스트명을 SNI에 사용하고 프록시 인증서를 검증하게 되며, 이는 잘못된 동작.
Effect: TLS ClientHello에 올바른 SNI(api.example.com)가 들어가고, 인증서 검증도 api.example.com에 대해 수행됩니다.
Format: hostname (포트 제외)

3. DefaultAuthority

Purpose: HTTP/2 :authority 의사 헤더를 설정.
Problem without it: :authority 헤더가 프록시 주소가 되어 서버 라우팅이 깨집니다.
Effect:

:method: POST
:scheme: https
:authority: api.example.com:443   ← 올바른 값!
:path: /mypackage.MyService/MyMethod

Format: host:port (서버 라우팅을 위해 포트가 자주 필요)

Complete Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│                        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

죄송합니다만, 번역하려는 텍스트를 제공해 주시면 번역해 드리겠습니다.