Skip to content

Commit e78f4c4

Browse files
asauberjoestringer
authored andcommitted
envoy: reject listeners with duplicate filter chains
Perform a check for duplicate filter chains at listener validation time. Envoy will reject listeners that contain duplicate filter chains. This can result in a runaway stream of errors from Envoy if we allow such CECs to enter the agent cache. This patch prevents many of these cases. This check directly compares protobuf data, so is a strictly weaker check than the validation performed on the Envoy side. Signed-off-by: Andrew Sauber <andrew.sauber@isovalent.com>
1 parent ba9d5a1 commit e78f4c4

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

pkg/ciliumenvoyconfig/cec_resource_parser.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,9 @@ func (r *CECResourceParser) ParseResources(cecNamespace string, cecName string,
534534
if err := listener.Validate(); err != nil {
535535
return xds.Resources{}, fmt.Errorf("failed to validate Listener %q (%w): %s", listener.Name, err, listener.String())
536536
}
537+
if listenerHasDuplicateFilterChainMatch(listener) {
538+
return xds.Resources{}, fmt.Errorf("Listener %q contains filter chains with duplicate matching rules", listener.Name)
539+
}
537540
}
538541
}
539542

@@ -598,6 +601,23 @@ func (r *CECResourceParser) ParseResources(cecNamespace string, cecName string,
598601
return resources, nil
599602
}
600603

604+
// listenerHasDuplicateFilterChainMatch reports whether the listener contains
605+
// filter chains with identical matching rules. Rejection here is order
606+
// sensitive, so some configs pass here and Envoy will later reject them.
607+
func listenerHasDuplicateFilterChainMatch(listener *envoy_config_listener.Listener) bool {
608+
seenMatches := make([]*envoy_config_listener.FilterChainMatch, 0, len(listener.GetFilterChains()))
609+
for _, filterChain := range listener.GetFilterChains() {
610+
match := filterChain.GetFilterChainMatch()
611+
for _, seenMatch := range seenMatches {
612+
if proto.Equal(match, seenMatch) {
613+
return true
614+
}
615+
}
616+
seenMatches = append(seenMatches, match)
617+
}
618+
return false
619+
}
620+
601621
// 'l7lb' triggers the upstream mark to embed source pod EndpointID instead of source security ID
602622
func (r *CECResourceParser) getBPFMetadataListenerFilter(useOriginalSourceAddr bool, l7lb bool, proxyPort uint16, isHTTPListener bool) *envoy_config_listener.ListenerFilter {
603623
conf := &cilium.BpfMetadata{
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of Cilium
3+
4+
package ciliumenvoyconfig
5+
6+
import (
7+
"testing"
8+
9+
envoy_config_listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestListenerHasDuplicateFilterChainMatch(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
listener *envoy_config_listener.Listener
17+
expectDuplicate bool
18+
}{
19+
{
20+
name: "no filter chains",
21+
listener: &envoy_config_listener.Listener{},
22+
},
23+
{
24+
name: "single filter chain",
25+
listener: listenerWithFilterChains(t, &envoy_config_listener.FilterChainMatch{ServerNames: []string{"one.example.com"}}),
26+
},
27+
{
28+
name: "unique filter chain matches",
29+
listener: listenerWithFilterChains(t,
30+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"one.example.com"}},
31+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"two.example.com"}},
32+
),
33+
},
34+
{
35+
name: "duplicate filter chain match",
36+
listener: listenerWithFilterChains(t,
37+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"same.example.com"}},
38+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"same.example.com"}},
39+
),
40+
expectDuplicate: true,
41+
},
42+
{
43+
name: "duplicate empty filter chain matches",
44+
listener: listenerWithFilterChains(t,
45+
&envoy_config_listener.FilterChainMatch{},
46+
&envoy_config_listener.FilterChainMatch{},
47+
),
48+
expectDuplicate: true,
49+
},
50+
{
51+
name: "duplicate nil filter chain matches",
52+
listener: listenerWithFilterChains(t,
53+
nil,
54+
nil,
55+
),
56+
expectDuplicate: true,
57+
},
58+
{
59+
name: "differing transport protocol is not a duplicate",
60+
listener: listenerWithFilterChains(t,
61+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"same.example.com"}, TransportProtocol: "tls"},
62+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"same.example.com"}, TransportProtocol: "raw_buffer"},
63+
),
64+
},
65+
{
66+
name: "same server names with matching transport protocol is a duplicate",
67+
listener: listenerWithFilterChains(t,
68+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"same.example.com"}, TransportProtocol: "tls"},
69+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"same.example.com"}, TransportProtocol: "tls"},
70+
),
71+
expectDuplicate: true,
72+
},
73+
{
74+
// Known limitation: the validator is order-sensitive, so it does
75+
// not flag these as duplicates. Envoy will later reject them.
76+
name: "server name order difference not detected (known limitation vs Envoy)",
77+
listener: listenerWithFilterChains(t,
78+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"a.example.com", "b.example.com"}},
79+
&envoy_config_listener.FilterChainMatch{ServerNames: []string{"b.example.com", "a.example.com"}},
80+
),
81+
},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
assert.Equal(t, tt.expectDuplicate, listenerHasDuplicateFilterChainMatch(tt.listener))
87+
})
88+
}
89+
}
90+
91+
func listenerWithFilterChains(t *testing.T, matches ...*envoy_config_listener.FilterChainMatch) *envoy_config_listener.Listener {
92+
t.Helper()
93+
94+
listener := &envoy_config_listener.Listener{}
95+
for _, match := range matches {
96+
listener.FilterChains = append(listener.FilterChains, &envoy_config_listener.FilterChain{FilterChainMatch: match})
97+
}
98+
return listener
99+
}

0 commit comments

Comments
 (0)