Skip to content

Commit

Permalink
feat:support configuring xff trusted cidrs
Browse files Browse the repository at this point in the history
Signed-off-by: Rudrakh Panigrahi <[email protected]>
  • Loading branch information
rudrakhp committed Nov 24, 2024
1 parent 71c0b54 commit 1fde45a
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 41 deletions.
93 changes: 52 additions & 41 deletions internal/ir/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]")
)
Expand Down Expand Up @@ -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)
}

Check warning on line 359 in internal/ir/xds.go

View check run for this annotation

Codecov / codecov/patch

internal/ir/xds.go#L354-L359

Added lines #L354 - L359 were not covered by tests
}
}
return errs
}

Expand Down
23 changes: 23 additions & 0 deletions internal/xds/translator/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,23 @@ http:
customHeader:
name: "x-my-custom-header"
failClosed: true
- name: "fourth-listener"
address: "0.0.0.0"
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"
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,20 @@
outlierDetection: {}
perConnectionBufferLimitBytes: 32768
type: EDS
- circuitBreakers:
thresholds:
- maxRetries: 1024
commonLbConfig:
localityWeightedLbConfig: {}
connectTimeout: 10s
edsClusterConfig:
edsConfig:
ads: {}
resourceApiVersion: V3
serviceName: fourth-route-dest
ignoreHealthOnHostRemoval: true
lbPolicy: LEAST_REQUEST
name: fourth-route-dest
outlierDetection: {}
perConnectionBufferLimitBytes: 32768
type: EDS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,49 @@
name: third-listener
name: third-listener
perConnectionBufferLimitBytes: 32768
- address:
socketAddress:
address: 0.0.0.0
ipv4Compat: true
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
99 changes: 99 additions & 0 deletions test/e2e/testdata/authorization-client-ip-trusted-cidrs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: http-with-authorization-client-ip-1
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: same-namespace
rules:
- matches:
- path:
type: Exact
value: /protected1
backendRefs:
- name: infra-backend-v1
port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: http-with-authorization-client-ip-2
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: same-namespace
rules:
- matches:
- path:
type: Exact
value: /protected2
backendRefs:
- name: infra-backend-v1
port: 8080
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: authorization-client-ip-1
namespace: gateway-conformance-infra
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: http-with-authorization-client-ip-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
- name: "allow-location-1"
action: Allow
principal:
clientCIDRs:
- 192.168.1.0/24
- 192.168.2.0/24 # First matching rule is applied, so 12.168.2.0/24 will be allowed
- name: "deny-location-2"
action: Allow
principal:
clientCIDRs:
- 192.168.2.0/24
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: authorization-client-ip-2
namespace: gateway-conformance-infra
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: http-with-authorization-client-ip-2
authorization:
defaultAction: Deny
rules:
- action: Allow
principal:
clientCIDRs:
- 10.0.1.0/24
- 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
namespace: gateway-conformance-infra
spec:
clientIPDetection:
xForwardedFor:
trustedCidrs:
- "172.16.0.0/12"
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: same-namespace
Loading

0 comments on commit 1fde45a

Please sign in to comment.