Tools
Configuring HTTP Proxy for gRPC in C# Without Environment Variables
2025-12-13
0 views
admin
The Challenge ## Source Code References (gRPC v1.10.0) ## What I Discovered ## The Solution ## Understanding HTTP CONNECT Tunneling ## The Three Magic Channel Options ## What Each Option Does ## 1. grpc.http_connect_server ## 2. SslTargetNameOverride ## 3. DefaultAuthority ## Complete Flow Diagram ## Why Not Just Use Environment Variables? You have a gRPC client in C# using Grpc.Core that needs to route traffic through an HTTP proxy. Sounds simple, right? If you've searched for solutions, you've probably found: I needed per-channel proxy configuration without affecting other traffic and without migrating libraries. So I dove into the gRPC C-core source code to understand how http_proxy actually works. If you want to explore the internals yourself, here are the key files: When gRPC honors the http_proxy environment variable, it doesn't do anything magical. It simply: The key insight: these channel arguments are accessible via ChannelOption in C#! HTTP proxies use the CONNECT method to create TCP tunnels: Once the tunnel is established, TLS handshake and gRPC communication flow through transparently. Purpose: Tells gRPC where to tunnel What happens: When gRPC connects to the channel target (the proxy), it sends: Format: host:port (port is required!) Purpose: SSL/TLS hostname for SNI and certificate validation The problem: Without this, gRPC would: Format: hostname (NO port — SSL certificates don't include ports) Purpose: Sets the HTTP/2 :authority pseudo-header The problem: gRPC servers use :authority for routing. Without this override, it would be set to the proxy address. Format: host:port (port often required for server routing) Environment variables are global. If your application makes HTTP calls to multiple services, and only ONE needs proxying, you can't use environment variables safely. Channel options give you surgical precision. By understanding how gRPC handles proxies internally, we can configure per-channel proxy support without environment variables, keeping our other HTTP traffic unaffected. Have you faced similar challenges with gRPC proxying? Drop a comment below! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
┌────────┐ ┌───────┐ ┌────────────┐
│ 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) │ │ │ │ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
┌────────┐ ┌───────┐ ┌────────────┐
│ 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) │ │ │ │ CODE_BLOCK:
┌────────┐ ┌───────┐ ┌────────────┐
│ 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) │ │ │ │ CODE_BLOCK:
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);
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
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);
} CODE_BLOCK:
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);
} CODE_BLOCK:
CONNECT api.example.com:443 HTTP/1.1
Host: api.example.com:443 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
CONNECT api.example.com:443 HTTP/1.1
Host: api.example.com:443 CODE_BLOCK:
CONNECT api.example.com:443 HTTP/1.1
Host: api.example.com:443 CODE_BLOCK:
:method: POST
:scheme: https
:authority: api.example.com:443 ← Correct!
:path: /mypackage.MyService/MyMethod Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
:method: POST
:scheme: https
:authority: api.example.com:443 ← Correct!
:path: /mypackage.MyService/MyMethod CODE_BLOCK:
:method: POST
:scheme: https
:authority: api.example.com:443 ← Correct!
:path: /mypackage.MyService/MyMethod CODE_BLOCK:
┌─────────────────────────────────────────────────────────────────┐
│ 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] │
└──────────────────────────────┬──────────────────────────────────┘ │ Step 3: Encrypted │ ▼
┌─────────────────────────────────────────────────────────────────┐
│ gRPC SERVER │
│ Receives HTTP/2 request: │
│ :authority = "api.example.com:443" (DefaultAuthority) │
│ :path = /mypackage.MyService/MyMethod │
│ Routes and processes request ✓ │
└─────────────────────────────────────────────────────────────────┘ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
┌─────────────────────────────────────────────────────────────────┐
│ 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] │
└──────────────────────────────┬──────────────────────────────────┘ │ Step 3: Encrypted │ ▼
┌─────────────────────────────────────────────────────────────────┐
│ gRPC SERVER │
│ Receives HTTP/2 request: │
│ :authority = "api.example.com:443" (DefaultAuthority) │
│ :path = /mypackage.MyService/MyMethod │
│ Routes and processes request ✓ │
└─────────────────────────────────────────────────────────────────┘ CODE_BLOCK:
┌─────────────────────────────────────────────────────────────────┐
│ 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] │
└──────────────────────────────┬──────────────────────────────────┘ │ Step 3: Encrypted │ ▼
┌─────────────────────────────────────────────────────────────────┐
│ gRPC SERVER │
│ Receives HTTP/2 request: │
│ :authority = "api.example.com:443" (DefaultAuthority) │
│ :path = /mypackage.MyService/MyMethod │
│ Routes and processes request ✓ │
└─────────────────────────────────────────────────────────────────┘ - Set http_proxy environment variable ✅ Works, but affects ALL HTTP traffic
- Use Grpc.Net.Client with HttpClientHandler.Proxy ✅ Clean, but requires library migration
- Set grpc.http_proxy channel option ❌ Doesn't work in Grpc.Core - Parses the proxy URL
- Sets internal channel arguments
- Uses these arguments during connection - Send SNI for the proxy hostname
- Validate the certificate against the proxy hostname
- Both are WRONG — we need the actual target's certificate! - TLS ClientHello contains correct SNI: api.example.com
- Certificate validation checks against: api.example.com
how-totutorialguidedev.toaiserverroutingssl