diff --git a/modules/caddyhttp/reverseproxy/httptransport_test.go b/modules/caddyhttp/reverseproxy/httptransport_test.go index 55ca3fd33..f64b58468 100644 --- a/modules/caddyhttp/reverseproxy/httptransport_test.go +++ b/modules/caddyhttp/reverseproxy/httptransport_test.go @@ -4,11 +4,14 @@ import ( "context" "encoding/json" "fmt" + "net" + "net/url" "reflect" "testing" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) { @@ -194,3 +197,85 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) { }) } } + +// TestHTTPTransport_DialContext_DialInfoOverride is a regression test for +// issue #6447: a `tcp4/`-prefixed upstream silently fell back to plain `tcp` +// because dialContext only honored DialInfo for unix networks. PR #7300 widened +// the condition so DialInfo is honored when no upstream HTTP proxy is in use, +// and skipped (for non-unix networks) when one is. Both halves are pinned here. +func TestHTTPTransport_DialContext_DialInfoOverride(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + ln, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + t.Cleanup(func() { ln.Close() }) + go func() { + for { + c, err := ln.Accept() + if err != nil { + return + } + c.Close() + } + }() + + ht := &HTTPTransport{} + rt, err := ht.NewTransport(ctx) + if err != nil { + t.Fatalf("NewTransport: %v", err) + } + + proxyURL, err := url.Parse("http://proxy.example:8080") + if err != nil { + t.Fatalf("parse proxy URL: %v", err) + } + + tests := []struct { + name string + proxy bool + dialInfo string + defaultAddr string + }{ + { + // no proxy: DialInfo should be applied, so the dial lands on + // the live listener despite the bogus default address. + name: "honors DialInfo when no proxy", + proxy: false, + dialInfo: ln.Addr().String(), + defaultAddr: "127.0.0.1:1", + }, + { + // proxy active: DialInfo must NOT be applied for non-unix + // networks; the default address (the live listener) is used. + name: "skips DialInfo when proxy active", + proxy: true, + dialInfo: "127.0.0.1:1", + defaultAddr: ln.Addr().String(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dialCtx := context.WithValue(context.Background(), caddyhttp.VarsCtxKey, make(map[string]any)) + caddyhttp.SetVar(dialCtx, dialInfoVarKey, DialInfo{ + Network: "tcp4", + Address: tt.dialInfo, + }) + if tt.proxy { + caddyhttp.SetVar(dialCtx, proxyVarKey, proxyURL) + } + + conn, err := rt.DialContext(dialCtx, "tcp", tt.defaultAddr) + if err != nil { + t.Fatalf("DialContext: %v", err) + } + t.Cleanup(func() { conn.Close() }) + if got := conn.RemoteAddr().String(); got != ln.Addr().String() { + t.Fatalf("conn.RemoteAddr() = %s, want %s", got, ln.Addr().String()) + } + }) + } +}