From 948788ab4a9614478d8af546b8c7d2dab5480aad Mon Sep 17 00:00:00 2001 From: Rudrakh Panigrahi Date: Mon, 11 Nov 2024 14:37:37 +0530 Subject: [PATCH] feat:support configuring xff trusted cidrs Signed-off-by: Rudrakh Panigrahi --- internal/ir/xds.go | 93 +++++++++++-------- internal/xds/translator/listener.go | 23 +++++ .../in/xds-ir/client-ip-detection.yaml | 20 ++++ .../xds-ir/client-ip-detection.clusters.yaml | 18 ++++ .../xds-ir/client-ip-detection.endpoints.yaml | 12 +++ .../xds-ir/client-ip-detection.listeners.yaml | 45 +++++++++ .../xds-ir/client-ip-detection.routes.yaml | 14 +++ ...authorization-client-ip-trusted-cidrs.yaml | 87 +++++++++++++++++ test/e2e/tests/authorization_client_ip.go | 46 +++++++++ 9 files changed, 317 insertions(+), 41 deletions(-) create mode 100644 test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml diff --git a/internal/ir/xds.go b/internal/ir/xds.go index c9fb1dd56b8..8cc88b3599b 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -33,47 +33,49 @@ const ( ) var ( - ErrListenerNameEmpty = errors.New("field Name must be specified") - ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") - ErrListenerPortInvalid = errors.New("field Port specified is invalid") - ErrHTTPListenerHostnamesEmpty = errors.New("field Hostnames must be specified with at least a single hostname entry") - ErrTCPRouteSNIsEmpty = errors.New("field SNIs must be specified with at least a single server name entry") - ErrTLSServerCertEmpty = errors.New("field ServerCertificate must be specified") - ErrTLSPrivateKey = errors.New("field PrivateKey must be specified") - ErrRouteNameEmpty = errors.New("field Name must be specified") - ErrHTTPRouteHostnameEmpty = errors.New("field Hostname must be specified") - ErrDestinationNameEmpty = errors.New("field Name must be specified") - ErrDestEndpointHostInvalid = errors.New("field Address must be a valid IP or FQDN address") - ErrDestEndpointPortInvalid = errors.New("field Port specified is invalid") - ErrDestEndpointUDSPortInvalid = errors.New("field Port must not be specified for Unix Domain Socket address") - ErrDestEndpointUDSHostInvalid = errors.New("field Host must not be specified for Unix Domain Socket address") - ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix, SafeRegex or Distinct fields must be set") - ErrStringMatchInvertDistinctInvalid = errors.New("only one of the Invert or Distinct fields can be set") - ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") - ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse") - ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters") - ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters") - ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies more than one of fullPathReplace, prefixMatchReplace and regexMatchReplace") - ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") - ErrHTTPPathRegexModifierNoSetting = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") - ErrHTTPHostModifierDoubleReplace = errors.New("redirect filter cannot have a host modifier that supplies more than one of Hostname, Header and Backend") - ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") - ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") - ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)") - ErrLoadBalancerInvalid = errors.New("loadBalancer setting is invalid, only one setting can be set") - ErrHealthCheckTimeoutInvalid = errors.New("field HealthCheck.Timeout must be specified") - ErrHealthCheckIntervalInvalid = errors.New("field HealthCheck.Interval must be specified") - ErrHealthCheckUnhealthyThresholdInvalid = errors.New("field HealthCheck.UnhealthyThreshold should be greater than 0") - ErrHealthCheckHealthyThresholdInvalid = errors.New("field HealthCheck.HealthyThreshold should be greater than 0") - ErrHealthCheckerInvalid = errors.New("health checker setting is invalid, only one health checker can be set") - ErrHCHTTPHostInvalid = errors.New("field HTTPHealthChecker.Host should be specified") - ErrHCHTTPPathInvalid = errors.New("field HTTPHealthChecker.Path should be specified") - ErrHCHTTPMethodInvalid = errors.New("only one of the GET, HEAD, POST, DELETE, OPTIONS, TRACE, PATCH of HTTPHealthChecker.Method could be set") - ErrHCHTTPExpectedStatusesInvalid = errors.New("field HTTPHealthChecker.ExpectedStatuses should be specified") - ErrHealthCheckPayloadInvalid = errors.New("one of Text, Binary fields must be set in payload") - ErrHTTPStatusInvalid = errors.New("HTTPStatus should be in [200,600)") - ErrOutlierDetectionBaseEjectionTimeInvalid = errors.New("field OutlierDetection.BaseEjectionTime must be specified") - ErrOutlierDetectionIntervalInvalid = errors.New("field OutlierDetection.Interval must be specified") + ErrListenerNameEmpty = errors.New("field Name must be specified") + ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") + ErrListenerPortInvalid = errors.New("field Port specified is invalid") + ErrHTTPListenerHostnamesEmpty = errors.New("field Hostnames must be specified with at least a single hostname entry") + ErrTCPRouteSNIsEmpty = errors.New("field SNIs must be specified with at least a single server name entry") + ErrTLSServerCertEmpty = errors.New("field ServerCertificate must be specified") + ErrTLSPrivateKey = errors.New("field PrivateKey must be specified") + ErrRouteNameEmpty = errors.New("field Name must be specified") + ErrHTTPRouteHostnameEmpty = errors.New("field Hostname must be specified") + ErrDestinationNameEmpty = errors.New("field Name must be specified") + ErrDestEndpointHostInvalid = errors.New("field Address must be a valid IP or FQDN address") + ErrDestEndpointPortInvalid = errors.New("field Port specified is invalid") + ErrDestEndpointUDSPortInvalid = errors.New("field Port must not be specified for Unix Domain Socket address") + ErrDestEndpointUDSHostInvalid = errors.New("field Host must not be specified for Unix Domain Socket address") + ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix, SafeRegex or Distinct fields must be set") + ErrStringMatchInvertDistinctInvalid = errors.New("only one of the Invert or Distinct fields can be set") + ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") + ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse") + ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters") + ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters") + ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies more than one of fullPathReplace, prefixMatchReplace and regexMatchReplace") + ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") + ErrHTTPPathRegexModifierNoSetting = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") + ErrHTTPHostModifierDoubleReplace = errors.New("redirect filter cannot have a host modifier that supplies more than one of Hostname, Header and Backend") + ErrAddHeaderEmptyName = errors.New("header modifier filter cannot configure a header without a name to be added") + ErrAddHeaderDuplicate = errors.New("header modifier filter attempts to add the same header more than once (case insensitive)") + ErrRemoveHeaderDuplicate = errors.New("header modifier filter attempts to remove the same header more than once (case insensitive)") + ErrLoadBalancerInvalid = errors.New("loadBalancer setting is invalid, only one setting can be set") + ErrHealthCheckTimeoutInvalid = errors.New("field HealthCheck.Timeout must be specified") + ErrHealthCheckIntervalInvalid = errors.New("field HealthCheck.Interval must be specified") + ErrHealthCheckUnhealthyThresholdInvalid = errors.New("field HealthCheck.UnhealthyThreshold should be greater than 0") + ErrHealthCheckHealthyThresholdInvalid = errors.New("field HealthCheck.HealthyThreshold should be greater than 0") + ErrHealthCheckerInvalid = errors.New("health checker setting is invalid, only one health checker can be set") + ErrHCHTTPHostInvalid = errors.New("field HTTPHealthChecker.Host should be specified") + ErrHCHTTPPathInvalid = errors.New("field HTTPHealthChecker.Path should be specified") + ErrHCHTTPMethodInvalid = errors.New("only one of the GET, HEAD, POST, DELETE, OPTIONS, TRACE, PATCH of HTTPHealthChecker.Method could be set") + ErrHCHTTPExpectedStatusesInvalid = errors.New("field HTTPHealthChecker.ExpectedStatuses should be specified") + ErrHealthCheckPayloadInvalid = errors.New("one of Text, Binary fields must be set in payload") + ErrHTTPStatusInvalid = errors.New("HTTPStatus should be in [200,600)") + ErrOutlierDetectionBaseEjectionTimeInvalid = errors.New("field OutlierDetection.BaseEjectionTime must be specified") + ErrOutlierDetectionIntervalInvalid = errors.New("field OutlierDetection.Interval must be specified") + ErrBothXForwardedForAndCustomHeaderInvalid = errors.New("only one of ClientIPDetection.XForwardedFor and ClientIPDetection.CustomHeader must be set") + ErrBothNumTrustedHopsAndTrustedCIDRsInvalid = errors.New("only one of ClientIPDetection.XForwardedFor.NumTrustedHops and ClientIPDetection.XForwardedFor.TrustedCIDRs must be set") redacted = []byte("[redacted]") ) @@ -348,6 +350,15 @@ func (h HTTPListener) Validate() error { errs = errors.Join(errs, err) } } + if h.ClientIPDetection != nil { + if h.ClientIPDetection.XForwardedFor != nil && h.ClientIPDetection.CustomHeader != nil { + errs = errors.Join(errs, ErrBothXForwardedForAndCustomHeaderInvalid) + } else if h.ClientIPDetection.XForwardedFor != nil { + if h.ClientIPDetection.XForwardedFor.NumTrustedHops != nil && h.ClientIPDetection.XForwardedFor.TrustedCIDRs != nil { + errs = errors.Join(errs, ErrBothNumTrustedHopsAndTrustedCIDRsInvalid) + } + } + } return errs } diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index 1568ed3e570..918de39c74a 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -23,6 +23,7 @@ import ( early_header_mutationv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/early_header_mutation/header_mutation/v3" preservecasev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3" customheaderv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/original_ip_detection/custom_header/v3" + xffv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/original_ip_detection/xff/v3" quicv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/quic/v3" tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" @@ -141,6 +142,28 @@ func originalIPDetectionExtensions(clientIPDetection *ir.ClientIPDetectionSettin Name: "envoy.extensions.http.original_ip_detection.custom_header", TypedConfig: customHeaderConfigAny, }) + } else if clientIPDetection.XForwardedFor != nil && clientIPDetection.XForwardedFor.TrustedCIDRs != nil { + trustedCidrs := make([]*corev3.CidrRange, 0) + for _, cidr := range clientIPDetection.XForwardedFor.TrustedCIDRs { + parsedCidr := strings.Split(string(cidr), "/") + addressPrefix := parsedCidr[0] + prefixLen, _ := strconv.ParseUint(parsedCidr[1], 10, 32) + trustedCidrs = append(trustedCidrs, &corev3.CidrRange{ + AddressPrefix: addressPrefix, + PrefixLen: wrapperspb.UInt32(uint32(prefixLen)), + }) + } + xffHeaderConfigAny, _ := protocov.ToAnyWithValidation(&xffv3.XffConfig{ + XffTrustedCidrs: &xffv3.XffTrustedCidrs{ + Cidrs: trustedCidrs, + }, + SkipXffAppend: wrapperspb.Bool(false), + }) + + extensionConfig = append(extensionConfig, &corev3.TypedExtensionConfig{ + Name: "envoy.extensions.http.original_ip_detection.xff", + TypedConfig: xffHeaderConfigAny, + }) } return extensionConfig diff --git a/internal/xds/translator/testdata/in/xds-ir/client-ip-detection.yaml b/internal/xds/translator/testdata/in/xds-ir/client-ip-detection.yaml index 1894902a0ba..9dcdae06a4c 100644 --- a/internal/xds/translator/testdata/in/xds-ir/client-ip-detection.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/client-ip-detection.yaml @@ -52,3 +52,23 @@ http: customHeader: name: "x-my-custom-header" failClosed: true +- name: "fourth-listener" + address: "::" + port: 8084 + hostnames: + - "*" + routes: + - name: "fourth-route" + hostname: "*" + destination: + name: "fourth-route-dest" + settings: + - endpoints: + - host: "4.4.4.4" + port: 8084 + clientIPDetection: + xForwardedFor: + trustedCidrs: + - "192.168.1.0/24" + - "10.0.0.0/16" + - "172.16.0.0/12" diff --git a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.clusters.yaml index ab0b4fca62c..58a2b85afd9 100644 --- a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.clusters.yaml @@ -52,3 +52,21 @@ outlierDetection: {} perConnectionBufferLimitBytes: 32768 type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: fourth-route-dest + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + name: fourth-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.endpoints.yaml index 59545ddec3a..ad653a1de59 100644 --- a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.endpoints.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.endpoints.yaml @@ -34,3 +34,15 @@ loadBalancingWeight: 1 locality: region: third-route-dest/backend/0 +- clusterName: fourth-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 4.4.4.4 + portValue: 8084 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: fourth-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.listeners.yaml index 4515aa70761..a69836fd1be 100644 --- a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.listeners.yaml @@ -109,3 +109,48 @@ name: third-listener name: third-listener perConnectionBufferLimitBytes: 32768 +- address: + socketAddress: + address: '::' + portValue: 8084 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + normalizePath: true + originalIpDetectionExtensions: + - name: envoy.extensions.http.original_ip_detection.xff + typedConfig: + '@type': type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig + skipXffAppend: false + xffTrustedCidrs: + cidrs: + - addressPrefix: 192.168.1.0 + prefixLen: 24 + - addressPrefix: 10.0.0.0 + prefixLen: 16 + - addressPrefix: 172.16.0.0 + prefixLen: 12 + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: fourth-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-8084 + useRemoteAddress: false + name: fourth-listener + name: fourth-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.routes.yaml index 12a38a14ef8..a0e9171307d 100644 --- a/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/client-ip-detection.routes.yaml @@ -40,3 +40,17 @@ cluster: third-route-dest upgradeConfigs: - upgradeType: websocket +- ignorePortInHostMatching: true + name: fourth-listener + virtualHosts: + - domains: + - '*' + name: fourth-listener/* + routes: + - match: + prefix: / + name: fourth-route + route: + cluster: fourth-route-dest + upgradeConfigs: + - upgradeType: websocket diff --git a/test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml b/test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml new file mode 100644 index 00000000000..1984bce9072 --- /dev/null +++ b/test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml @@ -0,0 +1,87 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-authorization-client-ip-trusted-cidr-1 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: Exact + value: /protected3 + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-authorization-client-ip-trusted-cidr-2 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: Exact + value: /protected4 + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: authorization-client-ip-trusted-cidr-1 + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-authorization-client-ip-trusted-cidr-1 + authorization: + defaultAction: Allow + rules: + - name: "deny-location-1" # First matching rule is applied, so 192.168.1.0/24 will be denied + action: Deny + principal: + clientCIDRs: + - 192.168.1.0/24 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: authorization-client-ip-trusted-cidr-2 + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-authorization-client-ip-trusted-cidr-2 + authorization: + defaultAction: Deny + rules: + - action: Allow + principal: + clientCIDRs: + - 10.0.2.0/24 +--- +# This is a client traffic policy that enables client IP detection using the XFF header. +# So, the client IP can be detected from the XFF header and used for authorization. +#apiVersion: gateway.envoyproxy.io/v1alpha1 +#kind: ClientTrafficPolicy +#metadata: +# name: enable-client-ip-detection-trusted-cidr +# namespace: gateway-conformance-infra +#spec: +# clientIPDetection: +# xForwardedFor: +# trustedCidrs: +# - "172.16.0.0/12" +# targetRefs: +# - group: gateway.networking.k8s.io +# kind: Gateway +# name: same-namespace diff --git a/test/e2e/tests/authorization_client_ip.go b/test/e2e/tests/authorization_client_ip.go index 698a4d73a6a..e5dadf02da1 100644 --- a/test/e2e/tests/authorization_client_ip.go +++ b/test/e2e/tests/authorization_client_ip.go @@ -23,6 +23,7 @@ import ( func init() { ConformanceTests = append(ConformanceTests, AuthorizationClientIPTest) + ConformanceTests = append(ConformanceTests, AuthorizationClientIPTrustedCidrsTest) } var AuthorizationClientIPTest = suite.ConformanceTest{ @@ -150,3 +151,48 @@ var AuthorizationClientIPTest = suite.ConformanceTest{ }) }, } + +var AuthorizationClientIPTrustedCidrsTest = suite.ConformanceTest{ + ShortName: "AuthzWithClientIPTrustedCIDRs", + Description: "Authorization with client IP Allow/Deny list using trusted CIDRs", + Manifests: []string{"testdata/authorization-client-ip-trusted-cidrs.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + route1NN := types.NamespacedName{Name: "http-with-authorization-client-ip-trusted-cidr-1", Namespace: ns} + route2NN := types.NamespacedName{Name: "http-with-authorization-client-ip-trusted-cidr-2", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), route1NN, route2NN) + + ancestorRef := gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "authorization-client-ip-trusted-cidr-1", Namespace: ns}, suite.ControllerName, ancestorRef) + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "authorization-client-ip-trusted-cidr-2", Namespace: ns}, suite.ControllerName, ancestorRef) + + t.Run("first route-allowed IP", func(t *testing.T) { + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/protected3", + Headers: map[string]string{ + "X-Forwarded-For": "192.168.2.1", // in the allowed list + }, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/protected3", + Headers: nil, // don't check headers since Envoy will append the client IP to the X-Forwarded-For header + }, + }, + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + }, +}