@marcel91200 / caddy Mirror · read-only / commits / d7b21c61
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 14:45 UTC

reverseproxy: fix tls dialing w/ proxy protocol (#7508)

Mohammed Al Sahaf committed Feb 22, 2026 at 05:37 UTC d7b21c610494e30e147de77a4783c18e5f206d99
2 files changed +91 -4
modules/caddyhttp/reverseproxy/httptransport.go
+11 -4
@@ -412,8 +412,13 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
412 return nil, fmt.Errorf("making TLS client config: %v", err)
413 }
414
415 - // servername has a placeholder, so we need to replace it
416 - if strings.Contains(h.TLS.ServerName, "{") {
415 + serverNameHasPlaceholder := strings.Contains(h.TLS.ServerName, "{")
416 +
417 + // We need to use custom DialTLSContext if:
418 + // 1. ServerName has a placeholder that needs to be replaced at request-time, OR
419 + // 2. ProxyProtocol is enabled, because req.URL.Host is modified to include
420 + // client address info with "->" separator which breaks Go's address parsing
421 + if serverNameHasPlaceholder || h.ProxyProtocol != "" {
422 rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
423 // reuses the dialer from above to establish a plaintext connection
424 conn, err := dialContext(ctx, network, addr)
@@ -422,9 +427,11 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
427 }
428
429 // but add our own handshake logic
425 - repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
430 tlsConfig := rt.TLSClientConfig.Clone()
427 - tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
431 + if serverNameHasPlaceholder {
432 + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
433 + tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
434 + }
435
436 // h1 only
437 if caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true {
modules/caddyhttp/reverseproxy/httptransport_test.go
+80
@@ -1,11 +1,13 @@
1 package reverseproxy
2
3 import (
4 + "context"
5 "encoding/json"
6 "fmt"
7 "reflect"
8 "testing"
9
10 + "github.com/caddyserver/caddy/v2"
11 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
12 )
13
@@ -115,3 +117,81 @@ func TestHTTPTransport_RequestHeaderOps_TLS(t *testing.T) {
117 t.Fatalf("unexpected Host value; want placeholder, got: %s", got)
118 }
119 }
120 +
121 +// TestHTTPTransport_DialTLSContext_ProxyProtocol verifies that when TLS and
122 +// ProxyProtocol are both enabled, DialTLSContext is set. This is critical because
123 +// ProxyProtocol modifies req.URL.Host to include client info with "->" separator
124 +// (e.g., "[2001:db8::1]:12345->127.0.0.1:443"), which breaks Go's address parsing.
125 +// Without a custom DialTLSContext, Go's HTTP library would fail with
126 +// "too many colons in address" when trying to parse the mangled host.
127 +func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
128 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
129 + defer cancel()
130 +
131 + tests := []struct {
132 + name string
133 + tls *TLSConfig
134 + proxyProtocol string
135 + serverNameHasPlaceholder bool
136 + expectDialTLSContext bool
137 + }{
138 + {
139 + name: "no TLS, no proxy protocol",
140 + tls: nil,
141 + proxyProtocol: "",
142 + expectDialTLSContext: false,
143 + },
144 + {
145 + name: "TLS without proxy protocol",
146 + tls: &TLSConfig{},
147 + proxyProtocol: "",
148 + expectDialTLSContext: false,
149 + },
150 + {
151 + name: "TLS with proxy protocol v1",
152 + tls: &TLSConfig{},
153 + proxyProtocol: "v1",
154 + expectDialTLSContext: true,
155 + },
156 + {
157 + name: "TLS with proxy protocol v2",
158 + tls: &TLSConfig{},
159 + proxyProtocol: "v2",
160 + expectDialTLSContext: true,
161 + },
162 + {
163 + name: "TLS with placeholder ServerName",
164 + tls: &TLSConfig{ServerName: "{http.request.host}"},
165 + proxyProtocol: "",
166 + serverNameHasPlaceholder: true,
167 + expectDialTLSContext: true,
168 + },
169 + {
170 + name: "TLS with placeholder ServerName and proxy protocol",
171 + tls: &TLSConfig{ServerName: "{http.request.host}"},
172 + proxyProtocol: "v2",
173 + serverNameHasPlaceholder: true,
174 + expectDialTLSContext: true,
175 + },
176 + }
177 +
178 + for _, tt := range tests {
179 + t.Run(tt.name, func(t *testing.T) {
180 + ht := &HTTPTransport{
181 + TLS: tt.tls,
182 + ProxyProtocol: tt.proxyProtocol,
183 + }
184 +
185 + rt, err := ht.NewTransport(ctx)
186 + if err != nil {
187 + t.Fatalf("NewTransport() error = %v", err)
188 + }
189 +
190 + hasDialTLSContext := rt.DialTLSContext != nil
191 + if hasDialTLSContext != tt.expectDialTLSContext {
192 + t.Errorf("DialTLSContext set = %v, want %v", hasDialTLSContext, tt.expectDialTLSContext)
193 + }
194 + })
195 + }
196 +}
197 +