diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go index 1121776d98f..1ecb231ff83 100644 --- a/caddyconfig/httpcaddyfile/addresses.go +++ b/caddyconfig/httpcaddyfile/addresses.go @@ -338,7 +338,7 @@ func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Ad if err != nil { return nil, fmt.Errorf("parsing network address: %v", err) } - if _, ok := listeners[addr.String()]; !ok { + if _, ok := listeners[networkAddr.String()]; !ok { listeners[networkAddr.String()] = map[string]struct{}{} } for _, protocol := range lnCfgVal.protocols { diff --git a/caddyconfig/httpcaddyfile/httptype_test.go b/caddyconfig/httpcaddyfile/httptype_test.go index b9a94fca94b..cb0f0e7ec7a 100644 --- a/caddyconfig/httpcaddyfile/httptype_test.go +++ b/caddyconfig/httpcaddyfile/httptype_test.go @@ -2,6 +2,7 @@ package httpcaddyfile import ( "encoding/json" + "slices" "strings" "testing" @@ -298,3 +299,103 @@ func TestDefaultSNIWithoutHTTPS(t *testing.T) { t.Errorf("Expected default_sni 'my-sni.com' in TLS connection policies, but it was missing. Generated JSON: %s", string(result)) } } + +// TestBindProtocolsMergedAcrossDirectives ensures that the protocols declared +// by every bind directive resolving to the same listener address are unioned +// together, instead of each bind directive discarding the ones before it. +func TestBindProtocolsMergedAcrossDirectives(t *testing.T) { + for i, tc := range []struct { + name string + input string + expectListen []string + expectProtocols [][]string + }{ + { + name: "two bind directives on the same address", + input: `example.com { + bind 127.0.0.1 { + protocols h1 + } + bind 127.0.0.1 { + protocols h2 + } +}`, + expectListen: []string{"127.0.0.1:443"}, + expectProtocols: [][]string{{"h1", "h2"}}, + }, + { + name: "later bind without protocols does not clear earlier ones", + input: `example.com { + bind 127.0.0.1 { + protocols h1 h3 + } + bind 127.0.0.1 +}`, + expectListen: []string{"127.0.0.1:443"}, + expectProtocols: [][]string{{"h1", "h3"}}, + }, + { + name: "a single bind directive is unaffected", + input: `example.com { + bind 127.0.0.1 { + protocols h1 + } +}`, + expectListen: []string{"127.0.0.1:443"}, + expectProtocols: [][]string{{"h1"}}, + }, + { + name: "bind directives on distinct addresses stay separate", + input: `example.com { + bind 127.0.0.1 { + protocols h1 + } + bind 127.0.0.2 { + protocols h2 + } +}`, + expectListen: []string{"127.0.0.1:443", "127.0.0.2:443"}, + expectProtocols: [][]string{{"h1"}, {"h2"}}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + adapter := caddyfile.Adapter{ServerType: ServerType{}} + result, _, err := adapter.Adapt([]byte(tc.input), nil) + if err != nil { + t.Fatalf("Failed to adapt Caddyfile: %v", err) + } + + var config struct { + Apps struct { + HTTP struct { + Servers map[string]*caddyhttp.Server `json:"servers"` + } `json:"http"` + } `json:"apps"` + } + if err := json.Unmarshal(result, &config); err != nil { + t.Fatalf("Failed to unmarshal JSON config: %v", err) + } + + server, ok := config.Apps.HTTP.Servers["srv0"] + if !ok { + t.Fatalf("Expected server 'srv0' to be created; got JSON: %s", result) + } + + if !slices.Equal(server.Listen, tc.expectListen) { + t.Errorf("Test %d: expected listen addresses %v, got %v", i, tc.expectListen, server.Listen) + } + if len(server.ListenProtocols) != len(tc.expectProtocols) { + t.Errorf("Test %d: expected listen protocols %v, got %v; generated JSON: %s", + i, tc.expectProtocols, server.ListenProtocols, result) + } else { + for j := range tc.expectProtocols { + if !slices.Equal(server.ListenProtocols[j], tc.expectProtocols[j]) { + t.Errorf("Test %d: expected listen protocols %v, got %v; generated JSON: %s", + i, tc.expectProtocols, server.ListenProtocols, result) + break + } + } + } + }) + } +}