Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions modules/caddyhttp/reverseproxy/httptransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,13 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
return nil, fmt.Errorf("making TLS client config: %v", err)
}

// servername has a placeholder, so we need to replace it
if strings.Contains(h.TLS.ServerName, "{") {
serverNameHasPlaceholder := strings.Contains(h.TLS.ServerName, "{")

// We need to use custom DialTLSContext if:
// 1. ServerName has a placeholder that needs to be replaced at request-time, OR
// 2. ProxyProtocol is enabled, because req.URL.Host is modified to include
// client address info with "->" separator which breaks Go's address parsing
if serverNameHasPlaceholder || h.ProxyProtocol != "" {
rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
// reuses the dialer from above to establish a plaintext connection
conn, err := dialContext(ctx, network, addr)
Expand All @@ -422,9 +427,11 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
}

// but add our own handshake logic
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsConfig := rt.TLSClientConfig.Clone()
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
if serverNameHasPlaceholder {
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
}

// h1 only
if caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true {
Expand Down
80 changes: 80 additions & 0 deletions modules/caddyhttp/reverseproxy/httptransport_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package reverseproxy

import (
"context"
"encoding/json"
"fmt"
"reflect"
"testing"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)

Expand Down Expand Up @@ -115,3 +117,81 @@ func TestHTTPTransport_RequestHeaderOps_TLS(t *testing.T) {
t.Fatalf("unexpected Host value; want placeholder, got: %s", got)
}
}

// TestHTTPTransport_DialTLSContext_ProxyProtocol verifies that when TLS and
// ProxyProtocol are both enabled, DialTLSContext is set. This is critical because
// ProxyProtocol modifies req.URL.Host to include client info with "->" separator
// (e.g., "[2001:db8::1]:12345->127.0.0.1:443"), which breaks Go's address parsing.
// Without a custom DialTLSContext, Go's HTTP library would fail with
// "too many colons in address" when trying to parse the mangled host.
func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()

tests := []struct {
name string
tls *TLSConfig
proxyProtocol string
serverNameHasPlaceholder bool
expectDialTLSContext bool
}{
{
name: "no TLS, no proxy protocol",
tls: nil,
proxyProtocol: "",
expectDialTLSContext: false,
},
{
name: "TLS without proxy protocol",
tls: &TLSConfig{},
proxyProtocol: "",
expectDialTLSContext: false,
},
{
name: "TLS with proxy protocol v1",
tls: &TLSConfig{},
proxyProtocol: "v1",
expectDialTLSContext: true,
},
{
name: "TLS with proxy protocol v2",
tls: &TLSConfig{},
proxyProtocol: "v2",
expectDialTLSContext: true,
},
{
name: "TLS with placeholder ServerName",
tls: &TLSConfig{ServerName: "{http.request.host}"},
proxyProtocol: "",
serverNameHasPlaceholder: true,
expectDialTLSContext: true,
},
{
name: "TLS with placeholder ServerName and proxy protocol",
tls: &TLSConfig{ServerName: "{http.request.host}"},
proxyProtocol: "v2",
serverNameHasPlaceholder: true,
expectDialTLSContext: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ht := &HTTPTransport{
TLS: tt.tls,
ProxyProtocol: tt.proxyProtocol,
}

rt, err := ht.NewTransport(ctx)
if err != nil {
t.Fatalf("NewTransport() error = %v", err)
}

hasDialTLSContext := rt.DialTLSContext != nil
if hasDialTLSContext != tt.expectDialTLSContext {
t.Errorf("DialTLSContext set = %v, want %v", hasDialTLSContext, tt.expectDialTLSContext)
}
})
}
}

Loading