/
gateway_listener.go
478 lines (401 loc) · 16 KB
/
gateway_listener.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
package graph
import (
"errors"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
v1 "sigs.k8s.io/gateway-api/apis/v1"
"github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions"
staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
)
// Listener represents a Listener of the Gateway resource.
// For now, we only support HTTP and HTTPS listeners.
type Listener struct {
Name string
// Source holds the source of the Listener from the Gateway resource.
Source v1.Listener
// Routes holds the routes attached to the Listener.
// Only valid routes are attached.
Routes map[types.NamespacedName]*Route
// AllowedRouteLabelSelector is the label selector for this Listener's allowed routes, if defined.
AllowedRouteLabelSelector labels.Selector
// ResolvedSecret is the namespaced name of the Secret resolved for this listener.
// Only applicable for HTTPS listeners.
ResolvedSecret *types.NamespacedName
// Conditions holds the conditions of the Listener.
Conditions []conditions.Condition
// SupportedKinds is the list of RouteGroupKinds allowed by the listener.
SupportedKinds []v1.RouteGroupKind
// Valid shows whether the Listener is valid.
// A Listener is considered valid if NGF can generate valid NGINX configuration for it.
Valid bool
// Attachable shows whether Routes can attach to the Listener.
// Listener can be invalid but still attachable.
Attachable bool
}
func buildListeners(
gw *v1.Gateway,
secretResolver *secretResolver,
refGrantResolver *referenceGrantResolver,
protectedPorts ProtectedPorts,
) []*Listener {
listeners := make([]*Listener, 0, len(gw.Spec.Listeners))
listenerFactory := newListenerConfiguratorFactory(gw, secretResolver, refGrantResolver, protectedPorts)
for _, gl := range gw.Spec.Listeners {
configurator := listenerFactory.getConfiguratorForListener(gl)
listeners = append(listeners, configurator.configure(gl))
}
return listeners
}
type listenerConfiguratorFactory struct {
http, https, unsupportedProtocol *listenerConfigurator
}
func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1.Listener) *listenerConfigurator {
switch l.Protocol {
case v1.HTTPProtocolType:
return f.http
case v1.HTTPSProtocolType:
return f.https
default:
return f.unsupportedProtocol
}
}
func newListenerConfiguratorFactory(
gw *v1.Gateway,
secretResolver *secretResolver,
refGrantResolver *referenceGrantResolver,
protectedPorts ProtectedPorts,
) *listenerConfiguratorFactory {
sharedPortConflictResolver := createPortConflictResolver()
return &listenerConfiguratorFactory{
unsupportedProtocol: &listenerConfigurator{
validators: []listenerValidator{
func(listener v1.Listener) ([]conditions.Condition, bool) {
valErr := field.NotSupported(
field.NewPath("protocol"),
listener.Protocol,
[]string{string(v1.HTTPProtocolType), string(v1.HTTPSProtocolType)},
)
return staticConds.NewListenerUnsupportedProtocol(valErr.Error()), false /* not attachable */
},
},
},
http: &listenerConfigurator{
validators: []listenerValidator{
validateListenerAllowedRouteKind,
validateListenerLabelSelector,
validateListenerHostname,
createHTTPListenerValidator(protectedPorts),
},
conflictResolvers: []listenerConflictResolver{
sharedPortConflictResolver,
},
},
https: &listenerConfigurator{
validators: []listenerValidator{
validateListenerAllowedRouteKind,
validateListenerLabelSelector,
validateListenerHostname,
createHTTPSListenerValidator(protectedPorts),
},
conflictResolvers: []listenerConflictResolver{
sharedPortConflictResolver,
},
externalReferenceResolvers: []listenerExternalReferenceResolver{
createExternalReferencesForTLSSecretsResolver(gw.Namespace, secretResolver, refGrantResolver),
},
},
}
}
// listenerValidator validates a listener. If the listener is invalid, the validator will return appropriate conditions.
// It also returns whether the listener is attachable, which is independent of whether the listener is valid.
type listenerValidator func(v1.Listener) (conds []conditions.Condition, attachable bool)
// listenerConflictResolver resolves conflicts between listeners. In case of a conflict, the resolver will make
// the conflicting listeners invalid - i.e. it will modify the passed listener and the previously processed conflicting
// listener. It will also add appropriate conditions to the listeners.
type listenerConflictResolver func(listener *Listener)
// listenerExternalReferenceResolver resolves external references for a listener. If the reference is not resolvable,
// the resolver will make the listener invalid and add appropriate conditions.
type listenerExternalReferenceResolver func(listener *Listener)
// listenerConfigurator is responsible for configuring a listener.
// validators, conflictResolvers, externalReferenceResolvers generate conditions for invalid fields of the listener.
// Because the Gateway status includes a status field for each listener, the messages in those conditions
// don't need to include the full path to the field (e.g. "spec.listeners[0].hostname"). They will include
// a path starting from the field of a listener (e.g. "hostname", "tls.options").
type listenerConfigurator struct {
validators []listenerValidator
// conflictResolvers can depend on validators - they will only be executed if all validators pass.
conflictResolvers []listenerConflictResolver
// externalReferenceResolvers can depend on validators - they will only be executed if all validators pass.
externalReferenceResolvers []listenerExternalReferenceResolver
}
func (c *listenerConfigurator) configure(listener v1.Listener) *Listener {
var conds []conditions.Condition
attachable := true
// validators might return different conditions, so we run them all.
for _, validator := range c.validators {
currConds, currAttachable := validator(listener)
conds = append(conds, currConds...)
attachable = attachable && currAttachable
}
valid := len(conds) == 0
var allowedRouteSelector labels.Selector
if selector := GetAllowedRouteLabelSelector(listener); selector != nil {
var err error
allowedRouteSelector, err = metav1.LabelSelectorAsSelector(selector)
if err != nil {
msg := fmt.Sprintf("invalid label selector: %s", err.Error())
conds = append(conds, staticConds.NewListenerUnsupportedValue(msg)...)
valid = false
}
}
supportedKinds := getListenerSupportedKinds(listener)
l := &Listener{
Name: string(listener.Name),
Source: listener,
Conditions: conds,
AllowedRouteLabelSelector: allowedRouteSelector,
Routes: make(map[types.NamespacedName]*Route),
Valid: valid,
Attachable: attachable,
SupportedKinds: supportedKinds,
}
if !l.Valid {
return l
}
// resolvers might add different conditions to the listener, so we run them all.
for _, resolver := range c.conflictResolvers {
resolver(l)
}
for _, resolver := range c.externalReferenceResolvers {
resolver(l)
}
return l
}
func validateListenerHostname(listener v1.Listener) (conds []conditions.Condition, attachable bool) {
if listener.Hostname == nil {
return nil, true
}
h := string(*listener.Hostname)
if h == "" {
return nil, true
}
if err := validateHostname(h); err != nil {
path := field.NewPath("hostname")
valErr := field.Invalid(path, listener.Hostname, err.Error())
return staticConds.NewListenerUnsupportedValue(valErr.Error()), false
}
return nil, true
}
func getAndValidateListenerSupportedKinds(listener v1.Listener) (
[]conditions.Condition,
[]v1.RouteGroupKind,
) {
if listener.AllowedRoutes == nil || listener.AllowedRoutes.Kinds == nil {
return nil, []v1.RouteGroupKind{
{
Kind: "HTTPRoute",
},
}
}
var conds []conditions.Condition
supportedKinds := make([]v1.RouteGroupKind, 0, len(listener.AllowedRoutes.Kinds))
validHTTPRouteKind := func(kind v1.RouteGroupKind) bool {
if kind.Kind != v1.Kind("HTTPRoute") {
return false
}
if kind.Group == nil || *kind.Group != v1.GroupName {
return false
}
return true
}
switch listener.Protocol {
case v1.HTTPProtocolType, v1.HTTPSProtocolType:
for _, kind := range listener.AllowedRoutes.Kinds {
if !validHTTPRouteKind(kind) {
msg := fmt.Sprintf("Unsupported route kind \"%s/%s\"", *kind.Group, kind.Kind)
conds = append(conds, staticConds.NewListenerInvalidRouteKinds(msg)...)
continue
}
supportedKinds = append(supportedKinds, kind)
}
}
return conds, supportedKinds
}
func validateListenerAllowedRouteKind(listener v1.Listener) (conds []conditions.Condition, attachable bool) {
conds, _ = getAndValidateListenerSupportedKinds(listener)
return conds, len(conds) == 0
}
func getListenerSupportedKinds(listener v1.Listener) []v1.RouteGroupKind {
_, kinds := getAndValidateListenerSupportedKinds(listener)
return kinds
}
func validateListenerLabelSelector(listener v1.Listener) (conds []conditions.Condition, attachable bool) {
if listener.AllowedRoutes != nil &&
listener.AllowedRoutes.Namespaces != nil &&
listener.AllowedRoutes.Namespaces.From != nil &&
*listener.AllowedRoutes.Namespaces.From == v1.NamespacesFromSelector &&
listener.AllowedRoutes.Namespaces.Selector == nil {
msg := "Listener's AllowedRoutes Selector must be set when From is set to type Selector"
return staticConds.NewListenerUnsupportedValue(msg), false
}
return nil, true
}
func createHTTPListenerValidator(protectedPorts ProtectedPorts) listenerValidator {
return func(listener v1.Listener) (conds []conditions.Condition, attachable bool) {
if err := validateListenerPort(listener.Port, protectedPorts); err != nil {
path := field.NewPath("port")
valErr := field.Invalid(path, listener.Port, err.Error())
conds = append(conds, staticConds.NewListenerUnsupportedValue(valErr.Error())...)
}
if listener.TLS != nil {
path := field.NewPath("tls")
valErr := field.Forbidden(path, "tls is not supported for HTTP listener")
conds = append(conds, staticConds.NewListenerUnsupportedValue(valErr.Error())...)
}
return conds, true
}
}
func validateListenerPort(port v1.PortNumber, protectedPorts ProtectedPorts) error {
if port < 1 || port > 65535 {
return errors.New("port must be between 1-65535")
}
if portName, ok := protectedPorts[int32(port)]; ok {
return fmt.Errorf("port is already in use as %v", portName)
}
return nil
}
func createHTTPSListenerValidator(protectedPorts ProtectedPorts) listenerValidator {
return func(listener v1.Listener) (conds []conditions.Condition, attachable bool) {
if err := validateListenerPort(listener.Port, protectedPorts); err != nil {
path := field.NewPath("port")
valErr := field.Invalid(path, listener.Port, err.Error())
conds = append(conds, staticConds.NewListenerUnsupportedValue(valErr.Error())...)
}
if listener.TLS == nil {
valErr := field.Required(field.NewPath("TLS"), "tls must be defined for HTTPS listener")
conds = append(conds, staticConds.NewListenerUnsupportedValue(valErr.Error())...)
return conds, true
}
tlsPath := field.NewPath("tls")
if *listener.TLS.Mode != v1.TLSModeTerminate {
valErr := field.NotSupported(
tlsPath.Child("mode"),
*listener.TLS.Mode,
[]string{string(v1.TLSModeTerminate)},
)
conds = append(conds, staticConds.NewListenerUnsupportedValue(valErr.Error())...)
}
if len(listener.TLS.Options) > 0 {
path := tlsPath.Child("options")
valErr := field.Forbidden(path, "options are not supported")
conds = append(conds, staticConds.NewListenerUnsupportedValue(valErr.Error())...)
}
if len(listener.TLS.CertificateRefs) == 0 {
msg := "certificateRefs must be defined for TLS mode terminate"
valErr := field.Required(tlsPath.Child("certificateRefs"), msg)
conds = append(conds, staticConds.NewListenerInvalidCertificateRef(valErr.Error())...)
return conds, true
}
certRef := listener.TLS.CertificateRefs[0]
certRefPath := tlsPath.Child("certificateRefs").Index(0)
if certRef.Kind != nil && *certRef.Kind != "Secret" {
path := certRefPath.Child("kind")
valErr := field.NotSupported(path, *certRef.Kind, []string{"Secret"})
conds = append(conds, staticConds.NewListenerInvalidCertificateRef(valErr.Error())...)
}
// for Kind Secret, certRef.Group must be nil or empty
if certRef.Group != nil && *certRef.Group != "" {
path := certRefPath.Child("group")
valErr := field.NotSupported(path, *certRef.Group, []string{""})
conds = append(conds, staticConds.NewListenerInvalidCertificateRef(valErr.Error())...)
}
if l := len(listener.TLS.CertificateRefs); l > 1 {
path := tlsPath.Child("certificateRefs")
valErr := field.TooMany(path, l, 1)
conds = append(conds, staticConds.NewListenerUnsupportedValue(valErr.Error())...)
}
return conds, true
}
}
func createPortConflictResolver() listenerConflictResolver {
conflictedPorts := make(map[v1.PortNumber]bool)
portProtocolOwner := make(map[v1.PortNumber]v1.ProtocolType)
listenersByPort := make(map[v1.PortNumber][]*Listener)
format := "Multiple listeners for the same port %d specify incompatible protocols; " +
"ensure only one protocol per port"
return func(l *Listener) {
port := l.Source.Port
// if port is in map of conflictedPorts then we only need to set the current listener to invalid
if conflictedPorts[port] {
l.Valid = false
conflictedConds := staticConds.NewListenerProtocolConflict(fmt.Sprintf(format, port))
l.Conditions = append(l.Conditions, conflictedConds...)
return
}
// otherwise, we add the listener to the list of listeners for this port
// and then check if the protocol owner for the port is different from the current listener's protocol.
listenersByPort[port] = append(listenersByPort[port], l)
protocol, ok := portProtocolOwner[port]
if !ok {
portProtocolOwner[port] = l.Source.Protocol
return
}
// if protocol owner doesn't match the listener's protocol we mark the port as conflicted,
// and invalidate all listeners we've seen for this port.
if protocol != l.Source.Protocol {
conflictedPorts[port] = true
for _, l := range listenersByPort[port] {
l.Valid = false
conflictedConds := staticConds.NewListenerProtocolConflict(fmt.Sprintf(format, port))
l.Conditions = append(l.Conditions, conflictedConds...)
}
}
}
}
func createExternalReferencesForTLSSecretsResolver(
gwNs string,
secretResolver *secretResolver,
refGrantResolver *referenceGrantResolver,
) listenerExternalReferenceResolver {
return func(l *Listener) {
certRef := l.Source.TLS.CertificateRefs[0]
certRefNs := gwNs
if certRef.Namespace != nil {
certRefNs = string(*certRef.Namespace)
}
certRefNsName := types.NamespacedName{
Namespace: certRefNs,
Name: string(certRef.Name),
}
if certRefNs != gwNs {
if !refGrantResolver.refAllowed(toSecret(certRefNsName), fromGateway(gwNs)) {
msg := fmt.Sprintf("Certificate ref to secret %s not permitted by any ReferenceGrant", certRefNsName)
l.Conditions = append(l.Conditions, staticConds.NewListenerRefNotPermitted(msg)...)
l.Valid = false
return
}
}
if err := secretResolver.resolve(certRefNsName); err != nil {
path := field.NewPath("tls", "certificateRefs").Index(0)
// field.NotFound could be better, but it doesn't allow us to set the error message.
valErr := field.Invalid(path, certRefNsName, err.Error())
l.Conditions = append(l.Conditions, staticConds.NewListenerInvalidCertificateRef(valErr.Error())...)
l.Valid = false
} else {
l.ResolvedSecret = &certRefNsName
}
}
}
// GetAllowedRouteLabelSelector returns a listener's AllowedRoutes label selector if it exists.
func GetAllowedRouteLabelSelector(l v1.Listener) *metav1.LabelSelector {
if l.AllowedRoutes != nil && l.AllowedRoutes.Namespaces != nil {
if *l.AllowedRoutes.Namespaces.From == v1.NamespacesFromSelector &&
l.AllowedRoutes.Namespaces.Selector != nil {
return l.AllowedRoutes.Namespaces.Selector
}
}
return nil
}