http-libs
↑↓ navigate ↵ select esc close
GitHub

Forward proxy overview

DamianH.Http.ForwardProxy turns an ASP.NET Core application into an HTTP forward proxy. A forward proxy acts on behalf of clients: they opt in by configuring it (HTTPS_PROXY, HttpClient.Proxy, curl --proxy) and then ask it to reach arbitrary destinations. A reverse proxy such as YARP acts on behalf of servers and routes to a fixed set of backends.

Forward proxy Reverse proxy
Destination chosen by Client (request target) Proxy configuration
https:// traffic Opaque CONNECT tunnel, or intercepted Terminated TLS
Authentication Proxy-Authorization / 407 Authorization / 401
Main risk SSRF and open-proxy abuse Backend exposure

Installation

dotnet add package DamianH.Http.ForwardProxy

Request types

Request Example Handled by
HTTP/1.1 CONNECT CONNECT example.com:443 HTTP/1.1 ListenOptions.UseForwardProxy()
HTTP/2 CONNECT :method CONNECT, :authority example.com:443 app.UseForwardProxy()
Absolute-form http:// GET http://example.com/ HTTP/1.1 app.UseForwardProxy()

Kestrel has no API to turn an HTTP/1.1 request into a raw tunnel, so HTTP/1.1 CONNECT is handled by connection middleware that inspects the first request on each connection. CONNECT must be the first request on an HTTP/1.1 connection, which is what proxy clients do. A later CONNECT on a keep-alive connection gets 501. Other traffic passes to Kestrel unchanged, so the same endpoint can serve health checks and other endpoints.

HTTP/2 CONNECT needs TLS with ALPN (h2) on the listener. If the destination connection fails (for example, it is reset), the tunnel's stream is reset with CONNECT_ERROR; if the client resets the stream, the destination connection is closed with a TCP RST (RFC 9113 §8.5). Extended CONNECT (WebSockets over HTTP/2, :protocol) is not intercepted. Absolute-form https:// targets are rejected with 400; clients must use CONNECT. HTTP/3 CONNECT is not supported and returns 501.

CONNECT tunnels are opaque unless you enable TLS interception, which decrypts selected tunnels so middleware can change the requests inside them, for example to inject credentials.

Forwarding http:// requests

The request target's path and query are sent to the destination exactly as received, without normalizing dot segments or percent-encoding. The Host header comes from the target, so ListenOptions.UseForwardProxy() turns on KestrelServerOptions.AllowHostHeaderOverride. That setting is server-wide: Kestrel then replaces a mismatched Host header with the absolute target's authority on every listener.

Hop-by-hop headers are removed, including headers the client names in Connection. Kestrel keeps only close, keep-alive or upgrade from a Connection header that lists one of them, so on HTTP/1.1 listeners the proxy also reads the request head as received to find the other names. That needs ListenOptions.UseForwardProxy() on the listener.

For TRACE and OPTIONS, a valid Max-Forwards value is decremented before the request is forwarded. At Max-Forwards: 0 the proxy answers without contacting the destination: OPTIONS gets an empty 200, and TRACE gets 405 (the proxy never echoes a request, which could expose credentials to cross-site tracing). An invalid value is forwarded unchanged. Other methods and requests inside intercepted tunnels are not affected.

Hosting

using DamianH.Http.ForwardProxy;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(kestrel =>
{
// Plain-text proxy endpoint (HTTP/1.1 only).
kestrel.ListenAnyIP(8080, listen => listen.UseForwardProxy());
// TLS proxy endpoint with HTTP/2 CONNECT. UseForwardProxy must follow UseHttps.
kestrel.ListenAnyIP(8443, listen =>
{
listen.Protocols = HttpProtocols.Http1AndHttp2;
listen.UseHttps("proxy.pfx", "password");
listen.UseForwardProxy();
});
});
builder.Services.AddForwardProxy(options =>
{
// See Authentication and Destination policy.
});
var app = builder.Build();
app.UseForwardProxy();
app.MapGet("/healthz", () => "ok");
app.Run();

Call app.UseForwardProxy() before routing, authentication and authorization middleware. Proxy requests are handled there and never reach endpoint routing, so GET http://example.com/healthz is forwarded, not served by your /healthz endpoint. Do not put [Authorize] or RequireAuthorization() on the proxy; it runs its own policy (see Authentication).

Sample

forward-proxy/samples/ForwardProxySample runs a Basic-authenticated proxy on localhost:8080:

dotnet run --project forward-proxy/samples/ForwardProxySample
curl --proxy http://demo:change-me@localhost:8080 https://example.com/