diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index c22bc65ffe8..7be4db24e29 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -512,9 +512,13 @@ func (t *Translator) processSecurityPolicyForRoute( // then run it once to keep the flow linear and easier to read. validator := validateSecurityPolicy errMsg := "invalid SecurityPolicy" - if currTarget.Kind == resource.KindTCPRoute { - validator = validateSecurityPolicyForTCP + switch currTarget.Kind { + case resource.KindTCPRoute: + validator = func(p *egv1a1.SecurityPolicy) error { return validateSecurityPolicyForL4(p, "TCP") } errMsg = "invalid SecurityPolicy for TCP route" + case resource.KindUDPRoute: + validator = func(p *egv1a1.SecurityPolicy) error { return validateSecurityPolicyForL4(p, "UDP") } + errMsg = "invalid SecurityPolicy for UDP route" } if err := validator(policy); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, @@ -968,16 +972,18 @@ func validateSecurityPolicy(p *egv1a1.SecurityPolicy) error { return nil } -// validateSecurityPolicyForTCP ensures SecurityPolicy usage on TCP is compatible. +// validateSecurityPolicyForL4 ensures SecurityPolicy usage on an L4 protocol +// (TCP or UDP) is compatible. proto names the protocol in the returned errors. // -// TCP supports Authorization with ClientCIDRs ONLY. +// L4 supports Authorization with ClientCIDRs ONLY, because there is no HTTP +// request to inspect: // - Principals.JWT => invalid (HTTP-only) // - Principals.Headers => invalid (HTTP-only) -// - Empty/no Authorization is allowed and results in no-op on TCP. +// - Empty/no Authorization is allowed and results in no-op on L4. // Returns an error when any HTTP-only field is present or CIDRs are invalid. -func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error { +func validateSecurityPolicyForL4(p *egv1a1.SecurityPolicy, proto string) error { if p.Spec.CORS != nil || p.Spec.CSRF != nil || p.Spec.JWT != nil || p.Spec.OIDC != nil || p.Spec.APIKeyAuth != nil || p.Spec.BasicAuth != nil || p.Spec.ExtAuth != nil { - return fmt.Errorf("only authorization is supported for TCP (routes/listeners)") + return fmt.Errorf("only authorization is supported for %s (routes/listeners)", proto) } if p.Spec.Authorization == nil || len(p.Spec.Authorization.Rules) == 0 { return nil @@ -985,19 +991,19 @@ func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error { for i := range p.Spec.Authorization.Rules { rule := &p.Spec.Authorization.Rules[i] if rule.CEL != nil { - return fmt.Errorf("rule %d: CEL not supported for TCP", i) + return fmt.Errorf("rule %d: CEL not supported for %s", i, proto) } if rule.Principal == nil { continue } if rule.Principal.JWT != nil { - return fmt.Errorf("rule %d: JWT not supported for TCP", i) + return fmt.Errorf("rule %d: JWT not supported for %s", i, proto) } if len(rule.Principal.Headers) > 0 { - return fmt.Errorf("rule %d: headers not supported for TCP", i) + return fmt.Errorf("rule %d: headers not supported for %s", i, proto) } if len(rule.Principal.ClientIPGeoLocations) > 0 { - return fmt.Errorf("rule %d: clientIPGeoLocations not supported for TCP", i) + return fmt.Errorf("rule %d: clientIPGeoLocations not supported for %s", i, proto) } if err := validateCIDRs(rule.Principal.ClientCIDRs); err != nil { return fmt.Errorf("rule %d: %w", i, err) @@ -1006,7 +1012,7 @@ func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error { return nil } -// validateCIDRs validates CIDR strings for TCP authorization rules. +// validateCIDRs validates CIDR strings for L4 authorization rules. func validateCIDRs(cidrs []egv1a1.CIDR) error { for _, c := range cidrs { if _, _, err := net.ParseCIDR(string(c)); err != nil { @@ -1385,6 +1391,9 @@ func (t *Translator) translateSecurityPolicyForRoute( continue } tl := xdsIR[irKey].GetTCPListener(irListenerName(listener)) + if tl == nil { + continue + } for _, r := range tl.Routes { // If target.SectionName is specified it must match the route-rule section name // in the IR. For HTTP/GRPC routes this is r.Metadata.SectionName; for TCP @@ -1403,6 +1412,34 @@ func (t *Translator) translateSecurityPolicyForRoute( } } } + case resource.KindUDPRoute: + for _, listener := range parentRefCtx.listeners { + // If targetListener is set, only apply to that exact listener. + if targetListener != nil && targetListenerName != irListenerName(listener) { + continue + } + ul := xdsIR[irKey].GetUDPListener(irListenerName(listener)) + // A UDP listener holds at most one route: when several UDPRoutes name the + // same listener only the oldest one is attached, so a policy targeting any + // of the others has nothing to apply to. + if ul == nil || ul.Route == nil { + continue + } + r := ul.Route + // As with TCP, the route-rule section name lives on the destination metadata. + if target.SectionName != nil && string(*target.SectionName) != r.Destination.Metadata.SectionName { + continue + } + + if r.Authorization != nil { + continue + } + // Only authorization for UDP + if authorization != nil { + authCopy := *authorization + r.Authorization = &authCopy + } + } case resource.KindHTTPRoute, resource.KindGRPCRoute: var ( hasBaseErrs = errs != nil @@ -1695,15 +1732,15 @@ func (t *Translator) translateSecurityPolicyForListeners( ) } - // Pre-create a TCP-only authorization object to avoid re-allocation - var tcpAuthorization *ir.Authorization + // Pre-create an L4-only authorization object to avoid re-allocation + var l4Authorization *ir.Authorization if authorization != nil { authCopy := *authorization - tcpAuthorization = &authCopy + l4Authorization = &authCopy } // Apply to TCP listeners (Authorization only). - if tcpAuthorization != nil { + if l4Authorization != nil { for _, tl := range x.TCP { if tl == nil || len(tl.Routes) == 0 { continue @@ -1718,8 +1755,27 @@ func (t *Translator) translateSecurityPolicyForListeners( if r.Authorization != nil { continue } - r.Authorization = tcpAuthorization + r.Authorization = l4Authorization + } + } + } + + // Apply to UDP listeners (Authorization only). + if l4Authorization != nil { + for _, ul := range x.UDP { + // A UDP listener holds at most one route. + if ul == nil || ul.Route == nil { + continue + } + if !listenerNames.Has(ul.Name) { + continue + } + // A Policy targeting the specific scope(xRoute rule, xRoute, Gateway listener) wins over a policy + // targeting a lesser specific scope(Gateway). + if ul.Route.Authorization != nil { + continue } + ul.Route.Authorization = l4Authorization } } diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go index 5ff44c8d4c5..0a083a2619c 100644 --- a/internal/gatewayapi/securitypolicy_test.go +++ b/internal/gatewayapi/securitypolicy_test.go @@ -1056,7 +1056,7 @@ func SetRouteParentContext(route RouteContext, parentRef gwapiv1.ParentReference route.SetRouteParentContext(parentRef, ctx) } -// --- TCP branch: validateSecurityPolicyForTCP(...) returns err -> SetTranslationErrorForPolicyAncestors(...) + return +// --- L4 branch: validateSecurityPolicyForL4(...) returns err -> SetTranslationErrorForPolicyAncestors(...) + return func Test_SecurityPolicy_TCP_Invalid_setsStatus_and_returns(t *testing.T) { tr := &Translator{GatewayControllerName: "gateway.envoyproxy.io/gatewayclass-controller"} trContext := &TranslatorContext{} @@ -1208,7 +1208,7 @@ func Test_SecurityPolicy_HTTP_Invalid_setsStatus_and_returns(t *testing.T) { require.True(t, hasParentFalseCondition(policy)) } -func Test_validateSecurityPolicyForTCP_Table(t *testing.T) { +func Test_validateSecurityPolicyForL4_Table(t *testing.T) { tests := []struct { name string spec egv1a1.SecurityPolicySpec @@ -1375,17 +1375,36 @@ func Test_validateSecurityPolicyForTCP_Table(t *testing.T) { }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - p := &egv1a1.SecurityPolicy{Spec: tc.spec} - err := validateSecurityPolicyForTCP(p) - if tc.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - }) + // TCP and UDP share the same rules, so every case must hold for both. + for _, proto := range []string{"TCP", "UDP"} { + for _, tc := range tests { + t.Run(proto+"/"+tc.name, func(t *testing.T) { + p := &egv1a1.SecurityPolicy{Spec: tc.spec} + err := validateSecurityPolicyForL4(p, proto) + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } } + + // Rejections that are about the protocol rather than the input name it, so the + // status condition tells the user which listener protocol refused the field. + t.Run("protocol named in protocol-specific errors", func(t *testing.T) { + p := &egv1a1.SecurityPolicy{Spec: egv1a1.SecurityPolicySpec{ + Authorization: &egv1a1.Authorization{ + Rules: []egv1a1.AuthorizationRule{{ + Action: egv1a1.AuthorizationActionAllow, + Principal: &egv1a1.Principal{Headers: []egv1a1.AuthorizationHeaderMatch{{Name: "x-user", Values: []string{"foo"}}}}, + }}, + }, + }} + + require.ErrorContains(t, validateSecurityPolicyForL4(p, "TCP"), "headers not supported for TCP") + require.ErrorContains(t, validateSecurityPolicyForL4(p, "UDP"), "headers not supported for UDP") + }) } func Test_validateAuthorizationGeoIPForHTTP(t *testing.T) { diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.in.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.in.yaml new file mode 100644 index 00000000000..22a0e764896 --- /dev/null +++ b/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.in.yaml @@ -0,0 +1,259 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-udp + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: foo + protocol: UDP + port: 8088 + allowedRoutes: + namespaces: + from: All + - name: bar + protocol: UDP + port: 8089 + allowedRoutes: + namespaces: + from: All + - name: baz + protocol: UDP + port: 8090 + allowedRoutes: + namespaces: + from: All +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-mixed + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - name: foo + protocol: UDP + port: 8443 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: hr-app-mixed + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-mixed + sectionName: http + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: service-1 + port: 8080 +udpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + namespace: default + name: udpr-app-foo + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-udp + sectionName: foo + rules: + - backendRefs: + - name: service-1 + port: 8163 +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + namespace: default + name: udpr-app-bar + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-udp + sectionName: bar + rules: + - backendRefs: + - name: service-1 + port: 8163 +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + namespace: default + name: udpr-app-baz + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-udp + sectionName: baz + rules: + - name: rule-baz + backendRefs: + - name: service-1 + port: 8163 +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + namespace: default + name: udpr-app-mixed + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-mixed + sectionName: foo + rules: + - backendRefs: + - name: service-1 + port: 8163 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: sp-gw-udp-whole + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-gateway + principal: + clientCIDRs: + - 10.10.0.0/24 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: sp-gw-udp-section-foo # This policy should attach sectionName foo + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + sectionName: foo + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-section-foo + principal: + clientCIDRs: + - 10.10.1.0/24 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: sp-udpr-app-bar # This policy should attach udproute bar + spec: + targetRef: + group: gateway.networking.k8s.io + kind: UDPRoute + name: udpr-app-bar + authorization: + defaultAction: Allow + rules: + - action: Deny + name: deny-route-bar + principal: + clientCIDRs: + - 10.10.2.0/24 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: sp-udpr-app-conflict # This policy should NOT attach udproute bar due to conflict with above policy + spec: + targetRef: + group: gateway.networking.k8s.io + kind: UDPRoute + name: udpr-app-bar + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-conflict + principal: + clientCIDRs: + - 10.10.3.0/24 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: sp-udpr-app-baz-section # This policy should attach udproute baz via its rule name + spec: + targetRef: + group: gateway.networking.k8s.io + kind: UDPRoute + name: udpr-app-baz + sectionName: rule-baz + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-baz + principal: + clientCIDRs: + - 10.10.6.0/24 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: sp-udpr-app-invalid-section # This policy should NOT attach due to invalid section + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + sectionName: bogus + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-invalid-section + principal: + clientCIDRs: + - 10.10.4.0/24 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: sp-mixed-gateway # This policy should attach to gateway-mixed, applying to all listeners, but only whats supported + spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway-mixed + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - 10.10.5.0/24 + cors: + allowOrigins: + - https://example.com + allowMethods: + - GET + - POST diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.out.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.out.yaml new file mode 100644 index 00000000000..92e47c74af6 --- /dev/null +++ b/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.out.yaml @@ -0,0 +1,893 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-udp + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: foo + port: 8088 + protocol: UDP + - allowedRoutes: + namespaces: + from: All + name: bar + port: 8089 + protocol: UDP + - allowedRoutes: + namespaces: + from: All + name: baz + port: 8090 + protocol: UDP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: foo + supportedKinds: + - group: gateway.networking.k8s.io + kind: UDPRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: bar + supportedKinds: + - group: gateway.networking.k8s.io + kind: UDPRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: baz + supportedKinds: + - group: gateway.networking.k8s.io + kind: UDPRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-mixed + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + name: foo + port: 8443 + protocol: UDP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: foo + supportedKinds: + - group: gateway.networking.k8s.io + kind: UDPRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: hr-app-mixed + namespace: default + spec: + parentRefs: + - name: gateway-mixed + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + type: PathPrefix + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-mixed + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-mixed: + proxy: + listeners: + - name: envoy-gateway/gateway-mixed/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + - name: envoy-gateway/gateway-mixed/foo + ports: + - containerPort: 8443 + name: udp-8443 + protocol: UDP + servicePort: 8443 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-mixed + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-mixed + namespace: envoy-gateway-system + envoy-gateway/gateway-udp: + proxy: + listeners: + - name: envoy-gateway/gateway-udp/foo + ports: + - containerPort: 8088 + name: udp-8088 + protocol: UDP + servicePort: 8088 + - name: envoy-gateway/gateway-udp/bar + ports: + - containerPort: 8089 + name: udp-8089 + protocol: UDP + servicePort: 8089 + - name: envoy-gateway/gateway-udp/baz + ports: + - containerPort: 8090 + name: udp-8090 + protocol: UDP + servicePort: 8090 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-udp + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-udp + namespace: envoy-gateway-system +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-udpr-app-baz-section + namespace: default + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-baz + principal: + clientCIDRs: + - 10.10.6.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: UDPRoute + name: udpr-app-baz + sectionName: rule-baz + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: baz + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-udpr-app-bar + namespace: default + spec: + authorization: + defaultAction: Allow + rules: + - action: Deny + name: deny-route-bar + principal: + clientCIDRs: + - 10.10.2.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: UDPRoute + name: udpr-app-bar + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: bar + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-udpr-app-conflict + namespace: default + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-conflict + principal: + clientCIDRs: + - 10.10.3.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: UDPRoute + name: udpr-app-bar + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: bar + conditions: + - lastTransitionTime: null + message: Unable to target UDPRoute udpr-app-bar, another SecurityPolicy has + already attached to it + reason: Conflicted + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-gw-udp-section-foo + namespace: envoy-gateway + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-section-foo + principal: + clientCIDRs: + - 10.10.1.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + sectionName: foo + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: foo + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-udpr-app-invalid-section + namespace: envoy-gateway + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-invalid-section + principal: + clientCIDRs: + - 10.10.4.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + sectionName: bogus + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: bogus + conditions: + - lastTransitionTime: null + message: No section name bogus found for Gateway envoy-gateway/gateway-udp + reason: TargetNotFound + status: "False" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-gw-udp-whole + namespace: envoy-gateway + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-gateway + principal: + clientCIDRs: + - 10.10.0.0/24 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'This policy is being overridden by other securityPolicies for these + gateway listeners: [envoy-gateway/gateway-udp/foo] and these routes: [default/udpr-app-bar + default/udpr-app-baz]' + reason: Overridden + status: "True" + type: Overridden + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-mixed-gateway + namespace: envoy-gateway + spec: + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - 10.10.5.0/24 + cors: + allowMethods: + - GET + - POST + allowOrigins: + - https://example.com + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway-mixed + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-mixed + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +udpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + name: udpr-app-foo + namespace: default + spec: + parentRefs: + - name: gateway-udp + namespace: envoy-gateway + sectionName: foo + rules: + - backendRefs: + - name: service-1 + port: 8163 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: UDP Port 8163 not found on Service default/service-1 + reason: PortNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-udp + namespace: envoy-gateway + sectionName: foo +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + name: udpr-app-bar + namespace: default + spec: + parentRefs: + - name: gateway-udp + namespace: envoy-gateway + sectionName: bar + rules: + - backendRefs: + - name: service-1 + port: 8163 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: UDP Port 8163 not found on Service default/service-1 + reason: PortNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-udp + namespace: envoy-gateway + sectionName: bar +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + name: udpr-app-baz + namespace: default + spec: + parentRefs: + - name: gateway-udp + namespace: envoy-gateway + sectionName: baz + rules: + - backendRefs: + - name: service-1 + port: 8163 + name: rule-baz + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: UDP Port 8163 not found on Service default/service-1 + reason: PortNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-udp + namespace: envoy-gateway + sectionName: baz +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + name: udpr-app-mixed + namespace: default + spec: + parentRefs: + - name: gateway-mixed + namespace: envoy-gateway + sectionName: foo + rules: + - backendRefs: + - name: service-1 + port: 8163 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: UDP Port 8163 not found on Service default/service-1 + reason: PortNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-mixed + namespace: envoy-gateway + sectionName: foo +xdsIR: + envoy-gateway/gateway-mixed: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-mixed-94aaf8eb + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-mixed + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-mixed-94aaf8eb + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-mixed + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-mixed + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-mixed/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: hr-app-mixed + namespace: default + name: httproute/default/hr-app-mixed/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/hr-app-mixed/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: hr-app-mixed + namespace: default + name: httproute/default/hr-app-mixed/rule/0/match/0/* + pathMatch: + distinct: false + name: "" + prefix: / + security: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: securitypolicy/envoy-gateway/sp-mixed-gateway/authorization/rule/0 + principal: + clientCIDRs: + - cidr: 10.10.5.0/24 + distinct: false + invert: false + isIPv6: false + maskLen: 24 + cors: + allowMethods: + - GET + - POST + allowOrigins: + - distinct: false + exact: https://example.com + name: "" + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + udp: + - address: 0.0.0.0 + externalPort: 8443 + metadata: + kind: Gateway + name: gateway-mixed + namespace: envoy-gateway + sectionName: foo + name: envoy-gateway/gateway-mixed/foo + port: 8443 + route: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: securitypolicy/envoy-gateway/sp-mixed-gateway/authorization/rule/0 + principal: + clientCIDRs: + - cidr: 10.10.5.0/24 + distinct: false + invert: false + isIPv6: false + maskLen: 24 + destination: + metadata: + kind: UDPRoute + name: udpr-app-mixed + namespace: default + name: udproute/default/udpr-app-mixed/rule/-1 + name: udproute/default/udpr-app-mixed + envoy-gateway/gateway-udp: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-udp-2934226e + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-udp + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-udp-2934226e + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-udp + protocol: TCP + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + udp: + - address: 0.0.0.0 + externalPort: 8088 + metadata: + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: foo + name: envoy-gateway/gateway-udp/foo + port: 8088 + route: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-section-foo + principal: + clientCIDRs: + - cidr: 10.10.1.0/24 + distinct: false + invert: false + isIPv6: false + maskLen: 24 + destination: + metadata: + kind: UDPRoute + name: udpr-app-foo + namespace: default + name: udproute/default/udpr-app-foo/rule/-1 + name: udproute/default/udpr-app-foo + - address: 0.0.0.0 + externalPort: 8089 + metadata: + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: bar + name: envoy-gateway/gateway-udp/bar + port: 8089 + route: + authorization: + defaultAction: Allow + rules: + - action: Deny + name: deny-route-bar + principal: + clientCIDRs: + - cidr: 10.10.2.0/24 + distinct: false + invert: false + isIPv6: false + maskLen: 24 + destination: + metadata: + kind: UDPRoute + name: udpr-app-bar + namespace: default + name: udproute/default/udpr-app-bar/rule/-1 + name: udproute/default/udpr-app-bar + - address: 0.0.0.0 + externalPort: 8090 + metadata: + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: baz + name: envoy-gateway/gateway-udp/baz + port: 8090 + route: + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-route-baz + principal: + clientCIDRs: + - cidr: 10.10.6.0/24 + distinct: false + invert: false + isIPv6: false + maskLen: 24 + destination: + metadata: + kind: UDPRoute + name: udpr-app-baz + namespace: default + sectionName: rule-baz + name: udproute/default/udpr-app-baz/rule/-1 + name: udproute/default/udpr-app-baz diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.in.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.in.yaml new file mode 100644 index 00000000000..21d0d3892c5 --- /dev/null +++ b/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.in.yaml @@ -0,0 +1,41 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-udp + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: udp + protocol: UDP + port: 8088 + allowedRoutes: + namespaces: + from: All +udpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + namespace: default + name: udpr-app + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-udp + sectionName: udp + rules: + - backendRefs: + - name: service-1 + port: 8163 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: sp-udpr-no-auth + spec: + targetRef: + group: gateway.networking.k8s.io + kind: UDPRoute + name: udpr-app diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.out.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.out.yaml new file mode 100644 index 00000000000..f2a4351ecee --- /dev/null +++ b/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.out.yaml @@ -0,0 +1,170 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-udp + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: udp + port: 8088 + protocol: UDP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: udp + supportedKinds: + - group: gateway.networking.k8s.io + kind: UDPRoute +infraIR: + envoy-gateway/gateway-udp: + proxy: + listeners: + - name: envoy-gateway/gateway-udp/udp + ports: + - containerPort: 8088 + name: udp-8088 + protocol: UDP + servicePort: 8088 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-udp + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-udp + namespace: envoy-gateway-system +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + name: sp-udpr-no-auth + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: UDPRoute + name: udpr-app + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: udp + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +udpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: UDPRoute + metadata: + name: udpr-app + namespace: default + spec: + parentRefs: + - name: gateway-udp + namespace: envoy-gateway + sectionName: udp + rules: + - backendRefs: + - name: service-1 + port: 8163 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: UDP Port 8163 not found on Service default/service-1 + reason: PortNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-udp + namespace: envoy-gateway + sectionName: udp +xdsIR: + envoy-gateway/gateway-udp: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-udp-2934226e + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-udp + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-udp-2934226e + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-udp + protocol: TCP + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + udp: + - address: 0.0.0.0 + externalPort: 8088 + metadata: + kind: Gateway + name: gateway-udp + namespace: envoy-gateway + sectionName: udp + name: envoy-gateway/gateway-udp/udp + port: 8088 + route: + destination: + metadata: + kind: UDPRoute + name: udpr-app + namespace: default + name: udproute/default/udpr-app/rule/-1 + name: udproute/default/udpr-app diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 07c493967fb..552fb1e0b9a 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -2815,6 +2815,8 @@ type UDPRoute struct { LoadBalancer *LoadBalancer `json:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"` // DNS is used to configure how DNS resolution is handled by the Envoy Proxy cluster DNS *DNS `json:"dns,omitempty" yaml:"dns,omitempty"` + // Authorization defines the schema for the authorization. + Authorization *Authorization `json:"authorization,omitempty" yaml:"authorization,omitempty"` } // Validate the fields within the UDPListener structure diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 141f3f81efc..c5c649c8953 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -5560,6 +5560,11 @@ func (in *UDPRoute) DeepCopyInto(out *UDPRoute) { *out = new(DNS) (*in).DeepCopyInto(*out) } + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(Authorization) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UDPRoute. diff --git a/internal/xds/translator/authorization_udp.go b/internal/xds/translator/authorization_udp.go new file mode 100644 index 00000000000..8cd8c168d6e --- /dev/null +++ b/internal/xds/translator/authorization_udp.go @@ -0,0 +1,141 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + cncfv3 "github.com/cncf/xds/go/xds/core/v3" + matcherv3 "github.com/cncf/xds/go/xds/type/matcher/v3" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" +) + +// buildUDPProxyMatcher builds the matcher that udp_proxy uses to pick a route, +// folding client-IP authorization into it. +// +// A UDP listener has no network filter chain, so the network RBAC filter used for +// TCPRoutes cannot be reused here. Instead the authorization decision rides along +// with route selection: a datagram whose source IP matches is routed to the +// cluster, and a datagram that matches nothing is dropped by udp_proxy, which +// counts it as downstream_sess_no_route. +// +// Since a denial can only be expressed as the absence of a match, the ordered +// Allow/Deny rules are compiled into allow-only predicates. A Deny rule never +// becomes an entry of its own; it subtracts from the Allow rules that follow it +// and from a permissive default action. +func buildUDPProxyMatcher(routeAction *cncfv3.TypedExtensionConfig, authorization *ir.Authorization) (*matcherv3.Matcher, error) { + onRoute := &matcherv3.Matcher_OnMatch{ + OnMatch: &matcherv3.Matcher_OnMatch_Action{Action: routeAction}, + } + + if authorization == nil { + return &matcherv3.Matcher{OnNoMatch: onRoute}, nil + } + + var ( + matchers []*matcherv3.Matcher_MatcherList_FieldMatcher + // Predicates of the Deny rules seen so far, in rule order. + denied []*matcherv3.Matcher_MatcherList_Predicate + ) + + for _, rule := range authorization.Rules { + // Only client CIDRs are enforceable on the UDP path. The Gateway API layer + // rejects every other principal for L4 targets, so anything else here is a + // rule that cannot match rather than one that matches everything. + if len(rule.Principal.ClientCIDRs) == 0 { + continue + } + + predicate, err := buildIPPredicate(rule.Principal.ClientCIDRs) + if err != nil { + return nil, err + } + + if rule.Action == egv1a1.AuthorizationActionDeny { + denied = append(denied, predicate) + continue + } + + // Rules are first-match-wins, so an Allow rule only covers the sources that + // no preceding Deny rule already claimed. Preceding Allow rules need no such + // exclusion: had one matched, the datagram would already have been routed. + conjuncts := []*matcherv3.Matcher_MatcherList_Predicate{predicate} + for _, d := range denied { + conjuncts = append(conjuncts, notPredicate(d)) + } + + matchers = append(matchers, &matcherv3.Matcher_MatcherList_FieldMatcher{ + Predicate: andPredicates(conjuncts), + OnMatch: onRoute, + }) + } + + if authorization.DefaultAction == egv1a1.AuthorizationActionAllow { + // Nothing is denied, so every datagram is routed either way and the Allow + // rules are redundant. Emit the same matcher as an unauthorized listener. + if len(denied) == 0 { + return &matcherv3.Matcher{OnNoMatch: onRoute}, nil + } + + // Route whatever no Deny rule claimed. This goes last so the Allow rules + // above keep their precedence. + matchers = append(matchers, &matcherv3.Matcher_MatcherList_FieldMatcher{ + Predicate: notPredicate(orPredicates(denied)), + OnMatch: onRoute, + }) + } + + matcher := &matcherv3.Matcher{} + // An empty matcher list fails proto validation, so the matcher type is left + // unset when nothing can be allowed. + if len(matchers) > 0 { + matcher.MatcherType = &matcherv3.Matcher_MatcherList_{ + MatcherList: &matcherv3.Matcher_MatcherList{Matchers: matchers}, + } + } + // on_no_match is deliberately left unset: that absence is what makes udp_proxy + // drop a datagram no Allow rule accounted for. + return matcher, nil +} + +// notPredicate negates a predicate. +func notPredicate(predicate *matcherv3.Matcher_MatcherList_Predicate) *matcherv3.Matcher_MatcherList_Predicate { + return &matcherv3.Matcher_MatcherList_Predicate{ + MatchType: &matcherv3.Matcher_MatcherList_Predicate_NotMatcher{ + NotMatcher: predicate, + }, + } +} + +// andPredicates conjoins predicates. A predicate list must hold at least two +// entries, so a lone predicate is returned as-is. +func andPredicates(predicates []*matcherv3.Matcher_MatcherList_Predicate) *matcherv3.Matcher_MatcherList_Predicate { + if len(predicates) == 1 { + return predicates[0] + } + return &matcherv3.Matcher_MatcherList_Predicate{ + MatchType: &matcherv3.Matcher_MatcherList_Predicate_AndMatcher{ + AndMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{ + Predicate: predicates, + }, + }, + } +} + +// orPredicates disjoins predicates. A predicate list must hold at least two +// entries, so a lone predicate is returned as-is. +func orPredicates(predicates []*matcherv3.Matcher_MatcherList_Predicate) *matcherv3.Matcher_MatcherList_Predicate { + if len(predicates) == 1 { + return predicates[0] + } + return &matcherv3.Matcher_MatcherList_Predicate{ + MatchType: &matcherv3.Matcher_MatcherList_Predicate_OrMatcher{ + OrMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{ + Predicate: predicates, + }, + }, + } +} diff --git a/internal/xds/translator/authorization_udp_test.go b/internal/xds/translator/authorization_udp_test.go new file mode 100644 index 00000000000..4bbef6b2fb2 --- /dev/null +++ b/internal/xds/translator/authorization_udp_test.go @@ -0,0 +1,151 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "testing" + + cncfv3 "github.com/cncf/xds/go/xds/core/v3" + matcherv3 "github.com/cncf/xds/go/xds/type/matcher/v3" + "github.com/stretchr/testify/require" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" +) + +func cidrRule(action egv1a1.AuthorizationAction, cidr string, maskLen uint32) *ir.AuthorizationRule { + return &ir.AuthorizationRule{ + Action: action, + Principal: ir.Principal{ + ClientCIDRs: []*ir.CIDRMatch{{CIDR: cidr, MaskLen: maskLen}}, + }, + } +} + +// udp_proxy can only route or not route, so a denial is expressed as the absence of a +// match. These cases pin down how the ordered Allow/Deny rules collapse into +// allow-only predicates, and in particular that on_no_match is set only when the +// default action routes unconditionally. +func TestBuildUDPProxyMatcher(t *testing.T) { + routeAction := &cncfv3.TypedExtensionConfig{Name: "route"} + + tests := []struct { + name string + authorization *ir.Authorization + wantMatchers int + wantOnNoMatch bool + }{ + { + name: "no authorization routes everything", + authorization: nil, + wantMatchers: 0, + wantOnNoMatch: true, + }, + { + name: "allowlist emits one matcher per allow rule and drops the rest", + authorization: &ir.Authorization{ + DefaultAction: egv1a1.AuthorizationActionDeny, + Rules: []*ir.AuthorizationRule{ + cidrRule(egv1a1.AuthorizationActionAllow, "192.168.100.0/24", 24), + cidrRule(egv1a1.AuthorizationActionAllow, "10.1.0.0/16", 16), + }, + }, + wantMatchers: 2, + wantOnNoMatch: false, + }, + { + name: "denylist collapses to a single negated matcher", + authorization: &ir.Authorization{ + DefaultAction: egv1a1.AuthorizationActionAllow, + Rules: []*ir.AuthorizationRule{ + cidrRule(egv1a1.AuthorizationActionDeny, "10.0.0.0/24", 24), + cidrRule(egv1a1.AuthorizationActionDeny, "172.16.0.0/12", 12), + }, + }, + wantMatchers: 1, + wantOnNoMatch: false, + }, + { + name: "a preceding deny rule narrows the allow rule and the default", + authorization: &ir.Authorization{ + DefaultAction: egv1a1.AuthorizationActionAllow, + Rules: []*ir.AuthorizationRule{ + cidrRule(egv1a1.AuthorizationActionDeny, "10.0.0.0/8", 8), + cidrRule(egv1a1.AuthorizationActionAllow, "10.1.0.0/16", 16), + }, + }, + // The narrowed allow rule, plus the catch-all for the permissive default. + wantMatchers: 2, + wantOnNoMatch: false, + }, + { + name: "an allow default with nothing denied is the same as no authorization", + authorization: &ir.Authorization{ + DefaultAction: egv1a1.AuthorizationActionAllow, + Rules: []*ir.AuthorizationRule{ + cidrRule(egv1a1.AuthorizationActionAllow, "192.168.100.0/24", 24), + }, + }, + wantMatchers: 0, + wantOnNoMatch: true, + }, + { + name: "a deny default with no allow rules drops everything", + authorization: &ir.Authorization{ + DefaultAction: egv1a1.AuthorizationActionDeny, + }, + wantMatchers: 0, + wantOnNoMatch: false, + }, + { + name: "rules without client CIDRs cannot match and are skipped", + authorization: &ir.Authorization{ + DefaultAction: egv1a1.AuthorizationActionDeny, + Rules: []*ir.AuthorizationRule{ + {Action: egv1a1.AuthorizationActionAllow}, + cidrRule(egv1a1.AuthorizationActionAllow, "192.168.100.0/24", 24), + }, + }, + wantMatchers: 1, + wantOnNoMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := buildUDPProxyMatcher(routeAction, tt.authorization) + require.NoError(t, err) + + require.Len(t, m.GetMatcherList().GetMatchers(), tt.wantMatchers) + require.Equal(t, tt.wantOnNoMatch, m.GetOnNoMatch() != nil) + + // An empty matcher list fails proto validation, so it must be left unset + // rather than emitted empty. + if tt.wantMatchers == 0 { + require.Nil(t, m.GetMatcherList()) + } + for _, fm := range m.GetMatcherList().GetMatchers() { + require.NotNil(t, fm.GetPredicate()) + require.NotNil(t, fm.GetOnMatch()) + } + }) + } +} + +// A predicate list must hold at least two entries, so a lone predicate has to be +// returned bare rather than wrapped. +func TestPredicateCombinatorsSkipSingletonLists(t *testing.T) { + single := &matcherv3.Matcher_MatcherList_Predicate{ + MatchType: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_{}, + } + + require.Same(t, single, andPredicates([]*matcherv3.Matcher_MatcherList_Predicate{single})) + require.Same(t, single, orPredicates([]*matcherv3.Matcher_MatcherList_Predicate{single})) + + require.Len(t, andPredicates([]*matcherv3.Matcher_MatcherList_Predicate{single, single}).GetAndMatcher().GetPredicate(), 2) + require.Len(t, orPredicates([]*matcherv3.Matcher_MatcherList_Predicate{single, single}).GetOrMatcher().GetPredicate(), 2) + require.Same(t, single, notPredicate(single).GetNotMatcher()) +} diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index e1548e93fa2..e11fc23e64b 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -13,7 +13,6 @@ import ( "strings" xdscore "github.com/cncf/xds/go/xds/core/v3" - matcher "github.com/cncf/xds/go/xds/type/matcher/v3" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" tls_inspectorv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3" @@ -1183,6 +1182,21 @@ func buildXdsUDPListener( return nil, err } + var authorization *ir.Authorization + if udpListener.Route != nil { + authorization = udpListener.Route.Authorization + } + routeMatcher, err := buildUDPProxyMatcher( + &xdscore.TypedExtensionConfig{ + Name: "route", + TypedConfig: routeAny, + }, + authorization, + ) + if err != nil { + return nil, err + } + al, error := buildXdsAccessLog(accesslog, ir.ProxyAccessLogTypeRoute) if error != nil { return nil, error @@ -1191,16 +1205,7 @@ func buildXdsUDPListener( StatPrefix: statPrefix, AccessLog: al, RouteSpecifier: &udpv3.UdpProxyConfig_Matcher{ - Matcher: &matcher.Matcher{ - OnNoMatch: &matcher.Matcher_OnMatch{ - OnMatch: &matcher.Matcher_OnMatch_Action{ - Action: &xdscore.TypedExtensionConfig{ - Name: "route", - TypedConfig: routeAny, - }, - }, - }, - }, + Matcher: routeMatcher, }, } udpProxyAny, err := proto.ToAnyWithValidation(udpProxy) diff --git a/internal/xds/translator/testdata/in/xds-ir/udp-route-authorization.yaml b/internal/xds/translator/testdata/in/xds-ir/udp-route-authorization.yaml new file mode 100644 index 00000000000..7db5a6ca1dd --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/udp-route-authorization.yaml @@ -0,0 +1,130 @@ +udp: +# Allowlist: a Deny default with Allow rules, so only the listed sources are routed +# and everything else falls through to no match and is dropped. +- name: "udp-listener-allowlist" + address: "::" + port: 10080 + route: + name: "udp-route-allowlist" + authorization: + defaultAction: Deny + rules: + - action: Allow + name: allow-corp + principal: + clientCIDRs: + - cidr: 192.168.100.0/24 + maskLen: 24 + - action: Allow + name: allow-branch + principal: + clientCIDRs: + - cidr: 10.1.0.0/16 + maskLen: 16 + - cidr: 2001:db8::/64 + isIPv6: true + maskLen: 64 + destination: + name: "udp-route-allowlist-dest" + settings: + - endpoints: + - host: "10.2.3.4" + port: 50000 + name: "udp-route-allowlist-dest/backend/0" +# Denylist: an Allow default with Deny rules, which compiles to a single negated +# predicate covering every denied source. +- name: "udp-listener-denylist" + address: "::" + port: 10081 + route: + name: "udp-route-denylist" + authorization: + defaultAction: Allow + rules: + - action: Deny + name: deny-office + principal: + clientCIDRs: + - cidr: 10.0.0.0/24 + maskLen: 24 + - action: Deny + name: deny-lab + principal: + clientCIDRs: + - cidr: 172.16.0.0/12 + maskLen: 12 + destination: + name: "udp-route-denylist-dest" + settings: + - endpoints: + - host: "10.2.3.5" + port: 50000 + name: "udp-route-denylist-dest/backend/0" +# Mixed ordering: the Deny rule precedes the Allow rule, so the Allow predicate is +# narrowed by it, and the Allow default is narrowed by every Deny rule. +- name: "udp-listener-mixed" + address: "::" + port: 10082 + route: + name: "udp-route-mixed" + authorization: + defaultAction: Allow + rules: + - action: Deny + name: deny-office + principal: + clientCIDRs: + - cidr: 10.0.0.0/8 + maskLen: 8 + - action: Allow + name: allow-branch + principal: + clientCIDRs: + - cidr: 10.1.0.0/16 + maskLen: 16 + destination: + name: "udp-route-mixed-dest" + settings: + - endpoints: + - host: "10.2.3.6" + port: 50000 + name: "udp-route-mixed-dest/backend/0" +# Deny all: a Deny default with no Allow rules leaves nothing to match on, so the +# matcher has neither a matcher list nor an on_no_match and every datagram is dropped. +- name: "udp-listener-deny-all" + address: "::" + port: 10083 + route: + name: "udp-route-deny-all" + authorization: + defaultAction: Deny + destination: + name: "udp-route-deny-all-dest" + settings: + - endpoints: + - host: "10.2.3.7" + port: 50000 + name: "udp-route-deny-all-dest/backend/0" +# Allow all: an Allow default with no Deny rules routes everything, so the matcher +# is identical to a listener without authorization. +- name: "udp-listener-allow-all" + address: "::" + port: 10084 + route: + name: "udp-route-allow-all" + authorization: + defaultAction: Allow + rules: + - action: Allow + name: allow-corp + principal: + clientCIDRs: + - cidr: 192.168.100.0/24 + maskLen: 24 + destination: + name: "udp-route-allow-all-dest" + settings: + - endpoints: + - host: "10.2.3.8" + port: 50000 + name: "udp-route-allow-all-dest/backend/0" diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.clusters.yaml new file mode 100644 index 00000000000..608368a5025 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.clusters.yaml @@ -0,0 +1,115 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: udp-route-allowlist-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: udp-route-allowlist-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: udp-route-denylist-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: udp-route-denylist-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: udp-route-mixed-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: udp-route-mixed-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: udp-route-deny-all-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: udp-route-deny-all-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: udp-route-allow-all-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: udp-route-allow-all-dest + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.endpoints.yaml new file mode 100644 index 00000000000..a05b26d41f5 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.endpoints.yaml @@ -0,0 +1,60 @@ +- clusterName: udp-route-allowlist-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 10.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: udp-route-allowlist-dest/backend/0 +- clusterName: udp-route-denylist-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 10.2.3.5 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: udp-route-denylist-dest/backend/0 +- clusterName: udp-route-mixed-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 10.2.3.6 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: udp-route-mixed-dest/backend/0 +- clusterName: udp-route-deny-all-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 10.2.3.7 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: udp-route-deny-all-dest/backend/0 +- clusterName: udp-route-allow-all-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 10.2.3.8 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: udp-route-allow-all-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.listeners.yaml new file mode 100644 index 00000000000..28bb96bd81b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.listeners.yaml @@ -0,0 +1,207 @@ +- address: + socketAddress: + address: '::' + portValue: 10080 + protocol: UDP + listenerFilters: + - name: envoy.filters.udp_listener.udp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig + matcher: + matcherList: + matchers: + - onMatch: + action: + name: route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route + cluster: udp-route-allowlist-dest + predicate: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 192.168.100.0 + prefixLen: 24 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + - onMatch: + action: + name: route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route + cluster: udp-route-allowlist-dest + predicate: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 10.1.0.0 + prefixLen: 16 + - addressPrefix: '2001:db8::' + prefixLen: 64 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + statPrefix: service + name: udp-listener-allowlist +- address: + socketAddress: + address: '::' + portValue: 10081 + protocol: UDP + listenerFilters: + - name: envoy.filters.udp_listener.udp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig + matcher: + matcherList: + matchers: + - onMatch: + action: + name: route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route + cluster: udp-route-denylist-dest + predicate: + notMatcher: + orMatcher: + predicate: + - singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 10.0.0.0 + prefixLen: 24 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + - singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 172.16.0.0 + prefixLen: 12 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + statPrefix: service + name: udp-listener-denylist +- address: + socketAddress: + address: '::' + portValue: 10082 + protocol: UDP + listenerFilters: + - name: envoy.filters.udp_listener.udp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig + matcher: + matcherList: + matchers: + - onMatch: + action: + name: route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route + cluster: udp-route-mixed-dest + predicate: + andMatcher: + predicate: + - singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 10.1.0.0 + prefixLen: 16 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + - notMatcher: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 10.0.0.0 + prefixLen: 8 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + - onMatch: + action: + name: route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route + cluster: udp-route-mixed-dest + predicate: + notMatcher: + singlePredicate: + customMatch: + name: ip_matcher + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip + cidrRanges: + - addressPrefix: 10.0.0.0 + prefixLen: 8 + statPrefix: client_ip + input: + name: client_ip + typedConfig: + '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput + statPrefix: service + name: udp-listener-mixed +- address: + socketAddress: + address: '::' + portValue: 10083 + protocol: UDP + listenerFilters: + - name: envoy.filters.udp_listener.udp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig + matcher: {} + statPrefix: service + name: udp-listener-deny-all +- address: + socketAddress: + address: '::' + portValue: 10084 + protocol: UDP + listenerFilters: + - name: envoy.filters.udp_listener.udp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig + matcher: + onNoMatch: + action: + name: route + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route + cluster: udp-route-allow-all-dest + statPrefix: service + name: udp-listener-allow-all diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.routes.yaml new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.routes.yaml @@ -0,0 +1 @@ +[]