C#에서 환경 변수 없이 gRPC용 HTTP 프록시 구성
Source: Dev.to

The Challenge
C#에서 Grpc.Core를 사용하고 있는 gRPC 클라이언트가 HTTP 프록시를 통해 트래픽을 라우팅해야 합니다. 간단해 보이죠?
그렇지 않습니다.
보통 보게 되는 접근 방식:
http_proxy환경 변수를 설정 ✅ 작동하지만 전체 HTTP 트래픽에 영향을 줍니다Grpc.Net.Client와HttpClientHandler.Proxy사용 ✅ 깔끔하지만 라이브러리 마이그레이션이 필요합니다grpc.http_proxy채널 옵션 설정 ❌Grpc.Core에서는 동작하지 않음
다른 트래픽에 영향을 주지 않으면서 라이브러리를 마이그레이션하지 않고 채널별 프록시 설정이 필요했습니다. 그래서 http_proxy가 실제로 어떻게 동작하는지 이해하기 위해 gRPC C‑core 소스 코드를 파고들었습니다.
Source Code References (gRPC v1.10.0)
| File | Purpose |
|---|---|
http_proxy.cc | http_proxy 환경 변수를 읽고 채널 인자를 설정 |
http_connect_handshaker.cc | 프록시에게 HTTP CONNECT 요청을 전송 |
secure_channel_create.cc | SSL/TLS 대상 이름 오버라이드 처리 |
channel.cc | 기본 authority 헤더 로직 |
ChannelOptions.cs | C# 채널 옵션 상수 |
What I Discovered
gRPC가 http_proxy 환경 변수를 존중할 때는 단순히:
- 프록시 URL을 파싱
- 내부 채널 인자를 설정
- 연결 시 해당 인자를 사용
핵심 인사이트: 이 채널 인자는 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] │
└─────────────────────────────────────────────────────────────────┘