diff --git a/CODEOWNERS b/CODEOWNERS index d199614f2534..90b5d3a35482 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -183,3 +183,6 @@ extensions/filters/http/oauth2 @rgs1 @derekargueta @snowp /*/extensions/filters/common/ext_authz @esmet @gsagula @dio /*/extensions/filters/http/ext_authz @esmet @gsagula @dio /*/extensions/filters/network/ext_authz @esmet @gsagula @dio +# Original IP detection +/*/extensions/http/original_ip_detection/custom_header @rgs1 @alyssawilk @antoniovicente +/*/extensions/http/original_ip_detection/xff @rgs1 @alyssawilk @antoniovicente diff --git a/api/BUILD b/api/BUILD index 7a6671dd681f..04b94ff211fd 100644 --- a/api/BUILD +++ b/api/BUILD @@ -245,6 +245,8 @@ proto_library( "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", + "//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg", + "//envoy/extensions/http/original_ip_detection/xff/v3:pkg", "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v3/BUILD b/api/envoy/extensions/filters/network/http_connection_manager/v3/BUILD index 55b63248136c..456f4e9e6170 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v3/BUILD +++ b/api/envoy/extensions/filters/network/http_connection_manager/v3/BUILD @@ -6,6 +6,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ + "//envoy/annotations:pkg", "//envoy/config/accesslog/v3:pkg", "//envoy/config/core/v3:pkg", "//envoy/config/filter/network/http_connection_manager/v2:pkg", diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index e46032daa426..90f4b276dc3c 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -19,6 +19,7 @@ import "google/protobuf/any.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; +import "envoy/annotations/deprecation.proto"; import "udpa/annotations/migrate.proto"; import "udpa/annotations/security.proto"; import "udpa/annotations/status.proto"; @@ -34,7 +35,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 46] +// [#next-free-field: 47] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -495,7 +496,36 @@ message HttpConnectionManager { // determining the origin client's IP address. The default is zero if this option // is not specified. See the documentation for // :ref:`config_http_conn_man_headers_x-forwarded-for` for more information. - uint32 xff_num_trusted_hops = 19; + // + // .. note:: + // This field is deprecated and instead :ref:`original_ip_detection_extensions + // ` + // should be used to configure the :ref:`xff extension ` + // to configure IP detection using the :ref:`config_http_conn_man_headers_x-forwarded-for` header. To replace + // this field use a config like the following: + // + // .. code-block:: yaml + // + // original_ip_detection_extensions: + // typed_config: + // "@type": type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig + // xff_num_trusted_hops: 1 + // + uint32 xff_num_trusted_hops = 19 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // The configuration for the original IP detection extensions. + // + // When configured the extensions will be called along with the request headers + // and information about the downstream connection, such as the directly connected address. + // Each extension will then use these parameters to decide the request's effective remote address. + // If an extension fails to detect the original IP address and isn't configured to reject + // the request, the HCM will try the remaining extensions until one succeeds or rejects + // the request. If the request isn't rejected nor any extension succeeds, the HCM will + // fallback to using the remote address. + // + // [#extension-category: envoy.http.original_ip_detection] + repeated config.core.v3.TypedExtensionConfig original_ip_detection_extensions = 46; // Configures what network addresses are considered internal for stats and header sanitation // purposes. If unspecified, only RFC1918 IP addresses will be considered internal. diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index 59d0d5c74984..4f61ffb8ccca 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -33,7 +33,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 46] +// [#next-free-field: 47] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; @@ -304,9 +304,9 @@ message HttpConnectionManager { type.http.v3.PathTransformation http_filter_transformation = 2; } - reserved 27, 11; + reserved 27, 11, 19; - reserved "idle_timeout"; + reserved "idle_timeout", "xff_num_trusted_hops"; // Supplies the type of codec that the connection manager should use. CodecType codec_type = 1 [(validate.rules).enum = {defined_only: true}]; @@ -493,12 +493,18 @@ message HttpConnectionManager { google.protobuf.BoolValue use_remote_address = 14 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; - // The number of additional ingress proxy hops from the right side of the - // :ref:`config_http_conn_man_headers_x-forwarded-for` HTTP header to trust when - // determining the origin client's IP address. The default is zero if this option - // is not specified. See the documentation for - // :ref:`config_http_conn_man_headers_x-forwarded-for` for more information. - uint32 xff_num_trusted_hops = 19; + // The configuration for the original IP detection extensions. + // + // When configured the extensions will be called along with the request headers + // and information about the downstream connection, such as the directly connected address. + // Each extension will then use these parameters to decide the request's effective remote address. + // If an extension fails to detect the original IP address and isn't configured to reject + // the request, the HCM will try the remaining extensions until one succeeds or rejects + // the request. If the request isn't rejected nor any extension succeeds, the HCM will + // fallback to using the remote address. + // + // [#extension-category: envoy.http.original_ip_detection] + repeated config.core.v4alpha.TypedExtensionConfig original_ip_detection_extensions = 46; // Configures what network addresses are considered internal for stats and header sanitation // purposes. If unspecified, only RFC1918 IP addresses will be considered internal. diff --git a/api/envoy/extensions/http/original_ip_detection/custom_header/v3/BUILD b/api/envoy/extensions/http/original_ip_detection/custom_header/v3/BUILD new file mode 100644 index 000000000000..9a76b7e148e0 --- /dev/null +++ b/api/envoy/extensions/http/original_ip_detection/custom_header/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto b/api/envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto new file mode 100644 index 000000000000..5ea93d754843 --- /dev/null +++ b/api/envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package envoy.extensions.http.original_ip_detection.custom_header.v3; + +import "envoy/type/v3/http_status.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.http.original_ip_detection.custom_header.v3"; +option java_outer_classname = "CustomHeaderProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Custom header original IP detection extension] + +// This extension allows for the original downstream remote IP to be detected +// by reading the value from a configured header name. If the value is successfully parsed +// as an IP, it'll be treated as the effective downstream remote address and seen as such +// by all filters. See :ref:`original_ip_detection_extensions +// ` +// for an overview of how extensions operate and what happens when an extension fails +// to detect the remote IP. +// +// [#extension: envoy.http.original_ip_detection.custom_header] +message CustomHeaderConfig { + // The header name containing the original downstream remote address, if present. + // + // Note: in the case of a multi-valued header, only the first value is tried and the rest are ignored. + string header_name = 1 + [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_NAME strict: true}]; + + // If set to true, the extension could decide that the detected address should be treated as + // trusted by the HCM. If the address is considered :ref:`trusted`, + // it might be used as input to determine if the request is internal (among other things). + bool allow_extension_to_set_address_as_trusted = 2; + + // If this is set, the request will be rejected when detection fails using it as the HTTP response status. + // + // .. note:: + // If this is set to < 400 or > 511, the default status 403 will be used instead. + type.v3.HttpStatus reject_with_status = 3; +} diff --git a/api/envoy/extensions/http/original_ip_detection/xff/v3/BUILD b/api/envoy/extensions/http/original_ip_detection/xff/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/api/envoy/extensions/http/original_ip_detection/xff/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/http/original_ip_detection/xff/v3/xff.proto b/api/envoy/extensions/http/original_ip_detection/xff/v3/xff.proto new file mode 100644 index 000000000000..6864788f9f18 --- /dev/null +++ b/api/envoy/extensions/http/original_ip_detection/xff/v3/xff.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package envoy.extensions.http.original_ip_detection.xff.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.http.original_ip_detection.xff.v3"; +option java_outer_classname = "XffProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: XFF original IP detection extension] + +// This extension allows for the original downstream remote IP to be detected +// by reading the :ref:`config_http_conn_man_headers_x-forwarded-for` header. +// +// [#extension: envoy.http.original_ip_detection.xff] +message XffConfig { + // The number of additional ingress proxy hops from the right side of the + // :ref:`config_http_conn_man_headers_x-forwarded-for` HTTP header to trust when + // determining the origin client's IP address. The default is zero if this option + // is not specified. See the documentation for + // :ref:`config_http_conn_man_headers_x-forwarded-for` for more information. + uint32 xff_num_trusted_hops = 1; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 338a8cdb80f2..eac32a5cdfd3 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -128,6 +128,8 @@ proto_library( "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", + "//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg", + "//envoy/extensions/http/original_ip_detection/xff/v3:pkg", "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", diff --git a/bazel/envoy_library.bzl b/bazel/envoy_library.bzl index 7f9b745a504b..0c4ae3a53a36 100644 --- a/bazel/envoy_library.bzl +++ b/bazel/envoy_library.bzl @@ -84,6 +84,7 @@ EXTENSION_CATEGORIES = [ "envoy.http.stateful_header_formatters", "envoy.internal_redirect_predicates", "envoy.io_socket", + "envoy.http.original_ip_detection", "envoy.matching.common_inputs", "envoy.matching.input_matchers", "envoy.rate_limit_descriptors", diff --git a/docs/root/api-v3/config/config.rst b/docs/root/api-v3/config/config.rst index 84bbc17d48a3..712ed03fa278 100644 --- a/docs/root/api-v3/config/config.rst +++ b/docs/root/api-v3/config/config.rst @@ -27,3 +27,4 @@ Extensions descriptors/descriptors request_id/request_id http/header_formatters + http/original_ip_detection diff --git a/docs/root/api-v3/config/http/original_ip_detection.rst b/docs/root/api-v3/config/http/original_ip_detection.rst new file mode 100644 index 000000000000..1d333c2f53c9 --- /dev/null +++ b/docs/root/api-v3/config/http/original_ip_detection.rst @@ -0,0 +1,8 @@ +Original IP Detection +===================== + +.. toctree:: + :glob: + :maxdepth: 2 + + ../../extensions/http/original_ip_detection/*/v3/* diff --git a/docs/root/configuration/http/http_conn_man/headers.rst b/docs/root/configuration/http/http_conn_man/headers.rst index d3a9d2aa96d6..57c34ec2e7f6 100644 --- a/docs/root/configuration/http/http_conn_man/headers.rst +++ b/docs/root/configuration/http/http_conn_man/headers.rst @@ -189,7 +189,9 @@ Given an HTTP request that has traveled through a series of zero or more proxies Envoy, the trusted client address is the earliest source IP address that is known to be accurate. The source IP address of the immediate downstream node's connection to Envoy is trusted. XFF *sometimes* can be trusted. Malicious clients can forge XFF, but the last -address in XFF can be trusted if it was put there by a trusted proxy. +address in XFF can be trusted if it was put there by a trusted proxy. Alternatively, Envoy +supports :ref:`extensions ` +for determining the *trusted client address* or original IP address. Envoy's default rules for determining the trusted client address (*before* appending anything to XFF) are: @@ -200,8 +202,11 @@ to XFF) are: node's connection to Envoy. In an environment where there are one or more trusted proxies in front of an edge -Envoy instance, the *xff_num_trusted_hops* configuration option can be used to trust -additional addresses from XFF: +Envoy instance, the :ref:`XFF extension ` +can be configured via the :ref:`original_ip_detection_extensions field +` +to set the *xff_num_trusted_hops* option which controls the number of additional +addresses that are to be trusted: * If *use_remote_address* is false and *xff_num_trusted_hops* is set to a value *N* that is greater than zero, the trusted client address is the (N+1)th address from the right end diff --git a/docs/root/configuration/http/http_conn_man/response_code_details.rst b/docs/root/configuration/http/http_conn_man/response_code_details.rst index 24d94164ca78..def0da6cbba2 100644 --- a/docs/root/configuration/http/http_conn_man/response_code_details.rst +++ b/docs/root/configuration/http/http_conn_man/response_code_details.rst @@ -35,6 +35,7 @@ Below are the list of reasons the HttpConnectionManager or Router filter may sen missing_path_rejected, The request was rejected due to a missing Path or :path header field. no_healthy_upstream, The request was rejected by the router filter because there was no healthy upstream found. overload, The request was rejected due to the Overload Manager reaching configured resource limits. + original_ip_detection_failed, The request was rejected because the original IP couldn't be detected. path_normalization_failed, "The request was rejected because path normalization was configured on and failed, probably due to an invalid path." request_headers_failed_strict_check, The request was rejected due to x-envoy-* headers failing strict header validation. request_overall_timeout, The per-stream total request timeout was exceeded. diff --git a/docs/root/configuration/http/http_conn_man/stats.rst b/docs/root/configuration/http/http_conn_man/stats.rst index b8d81e7ce7a8..7fd66553222f 100644 --- a/docs/root/configuration/http/http_conn_man/stats.rst +++ b/docs/root/configuration/http/http_conn_man/stats.rst @@ -45,6 +45,7 @@ statistics: downstream_rq_http2_total, Counter, Total HTTP/2 requests downstream_rq_http3_total, Counter, Total HTTP/3 requests downstream_rq_active, Gauge, Total active requests + downstream_rq_rejected_via_ip_detection, Counter, Total requests rejected because the original IP detection failed downstream_rq_response_before_rq_complete, Counter, Total responses sent before the request was complete downstream_rq_rx_reset, Counter, Total request resets received downstream_rq_tx_reset, Counter, Total request resets sent diff --git a/docs/root/intro/arch_overview/other_features/ip_transparency.rst b/docs/root/intro/arch_overview/other_features/ip_transparency.rst index 2bb277205adf..92b519421c3f 100644 --- a/docs/root/intro/arch_overview/other_features/ip_transparency.rst +++ b/docs/root/intro/arch_overview/other_features/ip_transparency.rst @@ -20,6 +20,14 @@ called the *downstream remote address*, for many reasons. Some examples include: Envoy supports multiple methods for providing the downstream remote address to the upstream host. These techniques vary in complexity and applicability. +Envoy also supports +:ref:`extensions ` +for detecting the original IP address. This might be useful if none of the techniques below is +applicable to your setup. Two available extensions are the :ref:`custom header +` +extension and the :ref:`xff ` +extension. + HTTP Headers ------------ diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index bf3719e65761..b00d52cbde73 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -61,6 +61,9 @@ Removed Config or Runtime New Features ------------ +* http: added support for :ref:`original IP detection extensions`. + Two initial extensions were added, the :ref:`custom header ` extension and the + :ref:`xff ` extension. * http: added the ability to :ref:`unescape slash sequences` in the path. Requests with unescaped slashes can be proxied, rejected or redirected to the new unescaped path. By default this feature is disabled. The default behavior can be overridden through :ref:`http_connection_manager.path_with_escaped_slashes_action` runtime variable. This action can be selectively enabled for a portion of requests by setting the :ref:`http_connection_manager.path_with_escaped_slashes_action_sampling` runtime variable. * http: added upstream and downstream alpha HTTP/3 support! See :ref:`quic_options ` for downstream and the new http3_protocol_options in :ref:`http_protocol_options ` for upstream HTTP/3. * listener: added ability to change an existing listener's address. @@ -69,3 +72,5 @@ New Features Deprecated ---------- + +* http: :ref:`xff_num_trusted_hops ` is deprecated in favor of :ref:`original IP detection extensions`. diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index 7a6671dd681f..04b94ff211fd 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -245,6 +245,8 @@ proto_library( "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", + "//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg", + "//envoy/extensions/http/original_ip_detection/xff/v3:pkg", "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 3714c7cca8aa..1ac96a5a292c 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -36,7 +36,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 46] +// [#next-free-field: 47] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -501,7 +501,36 @@ message HttpConnectionManager { // determining the origin client's IP address. The default is zero if this option // is not specified. See the documentation for // :ref:`config_http_conn_man_headers_x-forwarded-for` for more information. - uint32 xff_num_trusted_hops = 19; + // + // .. note:: + // This field is deprecated and instead :ref:`original_ip_detection_extensions + // ` + // should be used to configure the :ref:`xff extension ` + // to configure IP detection using the :ref:`config_http_conn_man_headers_x-forwarded-for` header. To replace + // this field use a config like the following: + // + // .. code-block:: yaml + // + // original_ip_detection_extensions: + // typed_config: + // "@type": type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig + // xff_num_trusted_hops: 1 + // + uint32 xff_num_trusted_hops = 19 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // The configuration for the original IP detection extensions. + // + // When configured the extensions will be called along with the request headers + // and information about the downstream connection, such as the directly connected address. + // Each extension will then use these parameters to decide the request's effective remote address. + // If an extension fails to detect the original IP address and isn't configured to reject + // the request, the HCM will try the remaining extensions until one succeeds or rejects + // the request. If the request isn't rejected nor any extension succeeds, the HCM will + // fallback to using the remote address. + // + // [#extension-category: envoy.http.original_ip_detection] + repeated config.core.v3.TypedExtensionConfig original_ip_detection_extensions = 46; // Configures what network addresses are considered internal for stats and header sanitation // purposes. If unspecified, only RFC1918 IP addresses will be considered internal. diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/BUILD b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/BUILD index 64536cdef30b..37cbc68f1915 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/BUILD +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/BUILD @@ -6,6 +6,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ + "//envoy/annotations:pkg", "//envoy/config/accesslog/v4alpha:pkg", "//envoy/config/core/v4alpha:pkg", "//envoy/config/route/v4alpha:pkg", diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index 59d0d5c74984..7582fc2611ee 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -19,6 +19,7 @@ import "google/protobuf/any.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/wrappers.proto"; +import "envoy/annotations/deprecation.proto"; import "udpa/annotations/security.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; @@ -33,7 +34,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 46] +// [#next-free-field: 47] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; @@ -498,7 +499,36 @@ message HttpConnectionManager { // determining the origin client's IP address. The default is zero if this option // is not specified. See the documentation for // :ref:`config_http_conn_man_headers_x-forwarded-for` for more information. - uint32 xff_num_trusted_hops = 19; + // + // .. note:: + // This field is deprecated and instead :ref:`original_ip_detection_extensions + // ` + // should be used to configure the :ref:`xff extension ` + // to configure IP detection using the :ref:`config_http_conn_man_headers_x-forwarded-for` header. To replace + // this field use a config like the following: + // + // .. code-block:: yaml + // + // original_ip_detection_extensions: + // typed_config: + // "@type": type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig + // xff_num_trusted_hops: 1 + // + uint32 hidden_envoy_deprecated_xff_num_trusted_hops = 19 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // The configuration for the original IP detection extensions. + // + // When configured the extensions will be called along with the request headers + // and information about the downstream connection, such as the directly connected address. + // Each extension will then use these parameters to decide the request's effective remote address. + // If an extension fails to detect the original IP address and isn't configured to reject + // the request, the HCM will try the remaining extensions until one succeeds or rejects + // the request. If the request isn't rejected nor any extension succeeds, the HCM will + // fallback to using the remote address. + // + // [#extension-category: envoy.http.original_ip_detection] + repeated config.core.v4alpha.TypedExtensionConfig original_ip_detection_extensions = 46; // Configures what network addresses are considered internal for stats and header sanitation // purposes. If unspecified, only RFC1918 IP addresses will be considered internal. diff --git a/generated_api_shadow/envoy/extensions/http/original_ip_detection/custom_header/v3/BUILD b/generated_api_shadow/envoy/extensions/http/original_ip_detection/custom_header/v3/BUILD new file mode 100644 index 000000000000..9a76b7e148e0 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/http/original_ip_detection/custom_header/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto b/generated_api_shadow/envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto new file mode 100644 index 000000000000..5ea93d754843 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package envoy.extensions.http.original_ip_detection.custom_header.v3; + +import "envoy/type/v3/http_status.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.http.original_ip_detection.custom_header.v3"; +option java_outer_classname = "CustomHeaderProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Custom header original IP detection extension] + +// This extension allows for the original downstream remote IP to be detected +// by reading the value from a configured header name. If the value is successfully parsed +// as an IP, it'll be treated as the effective downstream remote address and seen as such +// by all filters. See :ref:`original_ip_detection_extensions +// ` +// for an overview of how extensions operate and what happens when an extension fails +// to detect the remote IP. +// +// [#extension: envoy.http.original_ip_detection.custom_header] +message CustomHeaderConfig { + // The header name containing the original downstream remote address, if present. + // + // Note: in the case of a multi-valued header, only the first value is tried and the rest are ignored. + string header_name = 1 + [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_NAME strict: true}]; + + // If set to true, the extension could decide that the detected address should be treated as + // trusted by the HCM. If the address is considered :ref:`trusted`, + // it might be used as input to determine if the request is internal (among other things). + bool allow_extension_to_set_address_as_trusted = 2; + + // If this is set, the request will be rejected when detection fails using it as the HTTP response status. + // + // .. note:: + // If this is set to < 400 or > 511, the default status 403 will be used instead. + type.v3.HttpStatus reject_with_status = 3; +} diff --git a/generated_api_shadow/envoy/extensions/http/original_ip_detection/xff/v3/BUILD b/generated_api_shadow/envoy/extensions/http/original_ip_detection/xff/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/http/original_ip_detection/xff/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/generated_api_shadow/envoy/extensions/http/original_ip_detection/xff/v3/xff.proto b/generated_api_shadow/envoy/extensions/http/original_ip_detection/xff/v3/xff.proto new file mode 100644 index 000000000000..6864788f9f18 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/http/original_ip_detection/xff/v3/xff.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package envoy.extensions.http.original_ip_detection.xff.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.http.original_ip_detection.xff.v3"; +option java_outer_classname = "XffProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: XFF original IP detection extension] + +// This extension allows for the original downstream remote IP to be detected +// by reading the :ref:`config_http_conn_man_headers_x-forwarded-for` header. +// +// [#extension: envoy.http.original_ip_detection.xff] +message XffConfig { + // The number of additional ingress proxy hops from the right side of the + // :ref:`config_http_conn_man_headers_x-forwarded-for` HTTP header to trust when + // determining the origin client's IP address. The default is zero if this option + // is not specified. See the documentation for + // :ref:`config_http_conn_man_headers_x-forwarded-for` for more information. + uint32 xff_num_trusted_hops = 1; +} diff --git a/include/envoy/http/BUILD b/include/envoy/http/BUILD index 7b97f7089cc8..749355e04c5d 100644 --- a/include/envoy/http/BUILD +++ b/include/envoy/http/BUILD @@ -157,3 +157,15 @@ envoy_cc_library( "//include/envoy/config:typed_config_interface", ], ) + +envoy_cc_library( + name = "original_ip_detection_interface", + hdrs = ["original_ip_detection.h"], + deps = [ + ":codes_interface", + ":header_map_interface", + "//include/envoy/config:typed_config_interface", + "//include/envoy/network:address_interface", + "//include/envoy/server:factory_context_interface", + ], +) diff --git a/include/envoy/http/original_ip_detection.h b/include/envoy/http/original_ip_detection.h new file mode 100644 index 000000000000..27d52d59e2f2 --- /dev/null +++ b/include/envoy/http/original_ip_detection.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +#include "envoy/common/pure.h" +#include "envoy/config/typed_config.h" +#include "envoy/http/codes.h" +#include "envoy/http/header_map.h" +#include "envoy/network/address.h" +#include "envoy/server/factory_context.h" + +namespace Envoy { +namespace Http { + +struct OriginalIPDetectionParams { + // The request headers from downstream. + // + // Note that while extensions can modify the headers, they will undergo standard Envoy + // sanitation after the detect() call so additions made here may be removed before + // filters have access to headers. + Http::RequestHeaderMap& request_headers; + // The downstream directly connected address. + const Network::Address::InstanceConstSharedPtr& downstream_remote_address; +}; + +// Parameters to be used for sending a local reply when detection fails. +struct OriginalIPRejectRequestOptions { + Code response_code; + std::string body; +}; + +struct OriginalIPDetectionResult { + // An address that represents the detected address or nullptr if detection failed. + Network::Address::InstanceConstSharedPtr detected_remote_address; + // Is the detected address trusted (e.g.: can it be used to determine if this is an internal + // request). + bool allow_trusted_address_checks; + // If set, these parameters will be used to signal that detection failed and the request should + // be rejected. + absl::optional reject_options; +}; + +/** + * Interface class for original IP detection extensions. + */ +class OriginalIPDetection { +public: + virtual ~OriginalIPDetection() = default; + + /** + * Detect the final remote address. + * + * If the call to this method succeeds in detecting the remote IP address or + * fails and is configured to reject the request in that case, no other + * configured extensions will be called (if any). + * + * @param param supplies the OriginalIPDetectionParams params for detection. + * @return OriginalIPDetectionResult the result of the extension's attempt to detect + * the final remote address. + */ + virtual OriginalIPDetectionResult detect(OriginalIPDetectionParams& params) PURE; +}; + +using OriginalIPDetectionSharedPtr = std::shared_ptr; + +/* + * A factory for creating original IP detection extensions. + */ +class OriginalIPDetectionFactory : public Envoy::Config::TypedFactory { +public: + ~OriginalIPDetectionFactory() override = default; + + /** + * Creates a particular extension implementation. + * + * @param config supplies the configuration for the original IP detection extension. + * @return OriginalIPDetectionSharedPtr the extension instance. + */ + virtual OriginalIPDetectionSharedPtr + createExtension(const Protobuf::Message& config, + Server::Configuration::FactoryContext& context) PURE; + + std::string category() const override { return "envoy.http.original_ip_detection"; } +}; + +using OriginalIPDetectionFactoryPtr = std::unique_ptr; + +} // namespace Http +} // namespace Envoy diff --git a/include/envoy/stream_info/stream_info.h b/include/envoy/stream_info/stream_info.h index 71d4fdbb2886..acf84800d730 100644 --- a/include/envoy/stream_info/stream_info.h +++ b/include/envoy/stream_info/stream_info.h @@ -181,8 +181,10 @@ struct ResponseCodeDetailValues { const std::string InternalRedirect = "internal_redirect"; // The request was rejected because configured filters erroneously removed required headers. const std::string FilterRemovedRequiredHeaders = "filter_removed_required_headers"; + // The request was rejected because the original IP couldn't be detected. + const std::string OriginalIPDetectionFailed = "rejecting because detection failed"; // Changes or additions to details should be reflected in - // docs/root/configuration/http/http_conn_man/response_code_details_details.rst + // docs/root/configuration/http/http_conn_man/response_code_details.rst }; using ResponseCodeDetails = ConstSingleton; diff --git a/source/common/http/BUILD b/source/common/http/BUILD index f6e23941dfbc..2dfa803c7d94 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -188,6 +188,7 @@ envoy_cc_library( ":date_provider_lib", "//include/envoy/config:config_provider_interface", "//include/envoy/http:filter_interface", + "//include/envoy/http:original_ip_detection_interface", "//include/envoy/http:request_id_extension_interface", "//include/envoy/router:rds_interface", "//source/common/local_reply:local_reply_lib", diff --git a/source/common/http/conn_manager_config.h b/source/common/http/conn_manager_config.h index 2033da5c321c..8b05b4bbff49 100644 --- a/source/common/http/conn_manager_config.h +++ b/source/common/http/conn_manager_config.h @@ -3,6 +3,7 @@ #include "envoy/config/config_provider.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "envoy/http/filter.h" +#include "envoy/http/original_ip_detection.h" #include "envoy/http/request_id_extension.h" #include "envoy/router/rds.h" #include "envoy/stats/scope.h" @@ -57,6 +58,7 @@ namespace Http { COUNTER(downstream_rq_non_relative_path) \ COUNTER(downstream_rq_overload_close) \ COUNTER(downstream_rq_redirected_with_normalized_path) \ + COUNTER(downstream_rq_rejected_via_ip_detection) \ COUNTER(downstream_rq_response_before_rq_complete) \ COUNTER(downstream_rq_rx_reset) \ COUNTER(downstream_rq_timeout) \ @@ -476,6 +478,12 @@ class ConnectionManagerConfig { virtual envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager:: PathWithEscapedSlashesAction pathWithEscapedSlashesAction() const PURE; + + /** + * @return vector of OriginalIPDetectionSharedPtr original IP detection extensions. + */ + virtual const std::vector& + originalIpDetectionExtensions() const PURE; }; } // namespace Http } // namespace Envoy diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index a9e7205651b6..285a4f362773 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -1002,9 +1002,22 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&& he if (!state_.is_internally_created_) { // Only sanitize headers on first pass. // Modify the downstream remote address depending on configuration and headers. - filter_manager_.setDownstreamRemoteAddress(ConnectionManagerUtility::mutateRequestHeaders( + const auto mutate_result = ConnectionManagerUtility::mutateRequestHeaders( *request_headers_, connection_manager_.read_callbacks_->connection(), - connection_manager_.config_, *snapped_route_config_, connection_manager_.local_info_)); + connection_manager_.config_, *snapped_route_config_, connection_manager_.local_info_); + + // IP detection failed, reject the request. + if (mutate_result.reject_request.has_value()) { + const auto& reject_request_params = mutate_result.reject_request.value(); + connection_manager_.stats_.named_.downstream_rq_rejected_via_ip_detection_.inc(); + sendLocalReply(Grpc::Common::isGrpcRequestHeaders(*request_headers_), + reject_request_params.response_code, reject_request_params.body, nullptr, + absl::nullopt, + StreamInfo::ResponseCodeDetails::get().OriginalIPDetectionFailed); + return; + } + + filter_manager_.setDownstreamRemoteAddress(mutate_result.final_remote_address); } ASSERT(filter_manager_.streamInfo().downstreamAddressProvider().remoteAddress() != nullptr); diff --git a/source/common/http/conn_manager_utility.cc b/source/common/http/conn_manager_utility.cc index 38747b89f63b..1131f40113bb 100644 --- a/source/common/http/conn_manager_utility.cc +++ b/source/common/http/conn_manager_utility.cc @@ -72,7 +72,7 @@ ServerConnectionPtr ConnectionManagerUtility::autoCreateCodec( } } -Network::Address::InstanceConstSharedPtr ConnectionManagerUtility::mutateRequestHeaders( +ConnectionManagerUtility::MutateRequestHeadersResult ConnectionManagerUtility::mutateRequestHeaders( RequestHeaderMap& request_headers, Network::Connection& connection, ConnectionManagerConfig& config, const Router::Config& route_config, const LocalInfo::LocalInfo& local_info) { @@ -104,11 +104,11 @@ Network::Address::InstanceConstSharedPtr ConnectionManagerUtility::mutateRequest // peer. Cases where we don't "use remote address" include trusted double proxy where we expect // our peer to have already properly set XFF, etc. Network::Address::InstanceConstSharedPtr final_remote_address; - bool single_xff_address; + bool allow_trusted_address_checks = false; const uint32_t xff_num_trusted_hops = config.xffNumTrustedHops(); if (config.useRemoteAddress()) { - single_xff_address = request_headers.ForwardedFor() == nullptr; + allow_trusted_address_checks = request_headers.ForwardedFor() == nullptr; // If there are any trusted proxies in front of this Envoy instance (as indicated by // the xff_num_trusted_hops configuration option), get the trusted client address // from the XFF before we append to XFF. @@ -136,12 +136,27 @@ Network::Address::InstanceConstSharedPtr ConnectionManagerUtility::mutateRequest connection.ssl() ? Headers::get().SchemeValues.Https : Headers::get().SchemeValues.Http); } } else { - // If we are not using remote address, attempt to pull a valid IPv4 or IPv6 address out of XFF. + // If we are not using remote address, attempt to pull a valid IPv4 or IPv6 address out of XFF + // or through an extension. An extension might be needed when XFF doesn't work (e.g. an + // irregular network). + // // If we find one, it will be used as the downstream address for logging. It may or may not be // used for determining internal/external status (see below). - auto ret = Utility::getLastAddressFromXFF(request_headers, xff_num_trusted_hops); - final_remote_address = ret.address_; - single_xff_address = ret.single_address_; + OriginalIPDetectionParams params = {request_headers, + connection.addressProvider().remoteAddress()}; + for (const auto& detection_extension : config.originalIpDetectionExtensions()) { + const auto result = detection_extension->detect(params); + + if (result.reject_options.has_value()) { + return {nullptr, result.reject_options}; + } + + if (result.detected_remote_address) { + final_remote_address = result.detected_remote_address; + allow_trusted_address_checks = result.allow_trusted_address_checks; + break; + } + } } // If the x-forwarded-proto header is not set, set it here, since Envoy uses it for determining @@ -169,7 +184,7 @@ Network::Address::InstanceConstSharedPtr ConnectionManagerUtility::mutateRequest // we can't change it at this point. In the future we will likely need to add // additional inference modes and make this mode legacy. const bool internal_request = - single_xff_address && final_remote_address != nullptr && + allow_trusted_address_checks && final_remote_address != nullptr && config.internalAddressConfig().isInternalAddress(*final_remote_address); // After determining internal request status, if there is no final remote address, due to no XFF, @@ -187,30 +202,7 @@ Network::Address::InstanceConstSharedPtr ConnectionManagerUtility::mutateRequest request_headers.setReferenceEnvoyInternalRequest( Headers::get().EnvoyInternalRequestValues.True); } else { - if (edge_request) { - request_headers.removeEnvoyDecoratorOperation(); - request_headers.removeEnvoyDownstreamServiceCluster(); - request_headers.removeEnvoyDownstreamServiceNode(); - } - - request_headers.removeEnvoyRetriableStatusCodes(); - request_headers.removeEnvoyRetriableHeaderNames(); - request_headers.removeEnvoyRetryOn(); - request_headers.removeEnvoyRetryGrpcOn(); - request_headers.removeEnvoyMaxRetries(); - request_headers.removeEnvoyUpstreamAltStatName(); - request_headers.removeEnvoyUpstreamRequestTimeoutMs(); - request_headers.removeEnvoyUpstreamRequestPerTryTimeoutMs(); - request_headers.removeEnvoyUpstreamRequestTimeoutAltResponse(); - request_headers.removeEnvoyExpectedRequestTimeoutMs(); - request_headers.removeEnvoyForceTrace(); - request_headers.removeEnvoyIpTags(); - request_headers.removeEnvoyOriginalUrl(); - request_headers.removeEnvoyHedgeOnPerTryTimeout(); - - for (const LowerCaseString& header : route_config.internalOnlyHeaders()) { - request_headers.remove(header); - } + cleanInternalHeaders(request_headers, edge_request, route_config.internalOnlyHeaders()); } if (config.userAgent()) { @@ -250,7 +242,36 @@ Network::Address::InstanceConstSharedPtr ConnectionManagerUtility::mutateRequest mutateXfccRequestHeader(request_headers, connection, config); - return final_remote_address; + return {final_remote_address, absl::nullopt}; +} + +void ConnectionManagerUtility::cleanInternalHeaders( + RequestHeaderMap& request_headers, bool edge_request, + const std::list& internal_only_headers) { + if (edge_request) { + request_headers.removeEnvoyDecoratorOperation(); + request_headers.removeEnvoyDownstreamServiceCluster(); + request_headers.removeEnvoyDownstreamServiceNode(); + } + + request_headers.removeEnvoyRetriableStatusCodes(); + request_headers.removeEnvoyRetriableHeaderNames(); + request_headers.removeEnvoyRetryOn(); + request_headers.removeEnvoyRetryGrpcOn(); + request_headers.removeEnvoyMaxRetries(); + request_headers.removeEnvoyUpstreamAltStatName(); + request_headers.removeEnvoyUpstreamRequestTimeoutMs(); + request_headers.removeEnvoyUpstreamRequestPerTryTimeoutMs(); + request_headers.removeEnvoyUpstreamRequestTimeoutAltResponse(); + request_headers.removeEnvoyExpectedRequestTimeoutMs(); + request_headers.removeEnvoyForceTrace(); + request_headers.removeEnvoyIpTags(); + request_headers.removeEnvoyOriginalUrl(); + request_headers.removeEnvoyHedgeOnPerTryTimeout(); + + for (const LowerCaseString& header : internal_only_headers) { + request_headers.remove(header); + } } Tracing::Reason ConnectionManagerUtility::mutateTracingRequestHeader( diff --git a/source/common/http/conn_manager_utility.h b/source/common/http/conn_manager_utility.h index d85af74ce570..f5b15eebdfeb 100644 --- a/source/common/http/conn_manager_utility.h +++ b/source/common/http/conn_manager_utility.h @@ -47,6 +47,15 @@ class ConnectionManagerUtility { envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action); + /* The result after calling mutateRequestHeaders(), containing the final remote address. Note that + * an extension used for detecting the original IP of the request might decide it should be + * rejected if the detection failed. In this case, the reject_request optional will be set. + */ + struct MutateRequestHeadersResult { + Network::Address::InstanceConstSharedPtr final_remote_address; + absl::optional reject_request; + }; + /** * Mutates request headers in various ways. This functionality is broken out because of its * complexity for ease of testing. See the method itself for detailed comments on what @@ -55,13 +64,16 @@ class ConnectionManagerUtility { * Note this function may be called twice on the response path if there are * 100-Continue headers. * - * @return the final trusted remote address. This depends on various settings and the - * existence of the x-forwarded-for header. Again see the method for more details. + * @return MutateRequestHeadersResult containing the final trusted remote address if detected. + * This depends on various settings and the existence of the x-forwarded-for header. + * Note that an extension might also be used. If detection fails, the result may contain + * options for rejecting the request. */ - static Network::Address::InstanceConstSharedPtr - mutateRequestHeaders(RequestHeaderMap& request_headers, Network::Connection& connection, - ConnectionManagerConfig& config, const Router::Config& route_config, - const LocalInfo::LocalInfo& local_info); + static MutateRequestHeadersResult mutateRequestHeaders(RequestHeaderMap& request_headers, + Network::Connection& connection, + ConnectionManagerConfig& config, + const Router::Config& route_config, + const LocalInfo::LocalInfo& local_info); static void mutateResponseHeaders(ResponseHeaderMap& response_headers, const RequestHeaderMap* request_headers, @@ -95,6 +107,8 @@ class ConnectionManagerUtility { static void mutateXfccRequestHeader(RequestHeaderMap& request_headers, Network::Connection& connection, ConnectionManagerConfig& config); + static void cleanInternalHeaders(RequestHeaderMap& request_headers, bool edge_request, + const std::list& internal_only_headers); }; } // namespace Http diff --git a/source/common/http/utility.h b/source/common/http/utility.h index e01f65357fee..b05ff1b0b510 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -357,8 +357,8 @@ void sendLocalReply(const bool& is_reset, const EncodeFunctions& encode_function struct GetLastAddressFromXffInfo { // Last valid address pulled from the XFF header. Network::Address::InstanceConstSharedPtr address_; - // Whether this is the only address in the XFF header. - bool single_address_; + // Whether this address can be used to determine if it's an internal request. + bool allow_trusted_address_checks_; }; /** diff --git a/source/extensions/all_extensions.bzl b/source/extensions/all_extensions.bzl index f1f2f9901850..761c3c75e0bc 100644 --- a/source/extensions/all_extensions.bzl +++ b/source/extensions/all_extensions.bzl @@ -5,6 +5,7 @@ load("@envoy_build_config//:extensions_build_config.bzl", "EXTENSIONS") # The map may be overridden by extensions specified in envoy_build_config. _required_extensions = { "envoy.common.crypto.utility_lib": "//source/extensions/common/crypto:utility_lib", + "envoy.http.original_ip_detection.xff": "//source/extensions/http/original_ip_detection/xff:config", "envoy.request_id.uuid": "//source/extensions/request_id/uuid:config", "envoy.transport_sockets.tls": "//source/extensions/transport_sockets/tls:config", } diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 616d7714ef79..ee8ab2a61b71 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -269,6 +269,14 @@ EXTENSIONS = { # "envoy.http.stateful_header_formatters.preserve_case": "//source/extensions/http/header_formatters/preserve_case:preserve_case_formatter", + + # + # Original IP detection + # + + "envoy.http.original_ip_detection.custom_header": "//source/extensions/http/original_ip_detection/custom_header:config", + "envoy.http.original_ip_detection.xff": "//source/extensions/http/original_ip_detection/xff:config", + } # These can be changed to ["//visibility:public"], for downstream builds which diff --git a/source/extensions/filters/network/http_connection_manager/BUILD b/source/extensions/filters/network/http_connection_manager/BUILD index 60c8ce180e04..c2c6a19c2e94 100644 --- a/source/extensions/filters/network/http_connection_manager/BUILD +++ b/source/extensions/filters/network/http_connection_manager/BUILD @@ -28,6 +28,7 @@ envoy_cc_extension( "//include/envoy/filesystem:filesystem_interface", "//include/envoy/http:codec_interface", "//include/envoy/http:filter_interface", + "//include/envoy/http:original_ip_detection_interface", "//include/envoy/http:request_id_extension_interface", "//include/envoy/registry", "//include/envoy/router:route_config_provider_manager_interface", @@ -58,9 +59,11 @@ envoy_cc_extension( "//source/extensions/filters/http/common:pass_through_filter_lib", "//source/extensions/filters/network:well_known_names", "//source/extensions/filters/network/common:factory_base_lib", + "//source/extensions/http/original_ip_detection/xff:config", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/request_id/uuid/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/http/original_ip_detection/xff/v3:pkg_cc_proto", "@envoy_api//envoy/type/tracing/v3:pkg_cc_proto", "@envoy_api//envoy/type/v3:pkg_cc_proto", ] + envoy_select_enable_http3([ diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index 03669895e655..684fae1fca17 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -8,6 +8,7 @@ #include "envoy/config/core/v3/base.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.validate.h" +#include "envoy/extensions/http/original_ip_detection/xff/v3/xff.pb.h" #include "envoy/extensions/request_id/uuid/v3/uuid.pb.h" #include "envoy/filesystem/filesystem.h" #include "envoy/registry/registry.h" @@ -344,6 +345,34 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( } request_id_extension_ = Http::RequestIDExtensionFactory::fromProto(final_rid_config, context_); + // Check if IP detection extensions were configured, otherwise fall back to XFF. + auto ip_detection_extensions = config.original_ip_detection_extensions(); + if (ip_detection_extensions.empty()) { + envoy::extensions::http::original_ip_detection::xff::v3::XffConfig xff_config; + xff_config.set_xff_num_trusted_hops(xff_num_trusted_hops_); + + auto* extension = ip_detection_extensions.Add(); + extension->set_name("envoy.http.original_ip_detection.xff"); + extension->mutable_typed_config()->PackFrom(xff_config); + } + + original_ip_detection_extensions_.reserve(ip_detection_extensions.size()); + for (const auto& extension_config : ip_detection_extensions) { + auto* factory = + Envoy::Config::Utility::getFactory(extension_config); + if (!factory) { + throw EnvoyException( + fmt::format("Original IP detection extension not found: '{}'", extension_config.name())); + } + + auto extension = factory->createExtension(extension_config.typed_config(), context_); + if (!extension) { + throw EnvoyException(fmt::format("Original IP detection extension could not be created: '{}'", + extension_config.name())); + } + original_ip_detection_extensions_.push_back(extension); + } + // If scoped RDS is enabled, avoid creating a route config provider. Route config providers will // be managed by the scoped routing logic instead. switch (config.route_specifier_case()) { @@ -740,7 +769,7 @@ const envoy::config::trace::v3::Tracing_Http* HttpConnectionManagerConfig::getPe if (config.tracing().has_provider()) { return &config.tracing().provider(); } - // Otherwise, for the sake of backwards compatibility, fallback to using tracing provider + // Otherwise, for the sake of backwards compatibility, fall back to using tracing provider // configuration defined in the bootstrap config. if (context_.httpContext().defaultTracingConfig().has_http()) { return &context_.httpContext().defaultTracingConfig().http(); diff --git a/source/extensions/filters/network/http_connection_manager/config.h b/source/extensions/filters/network/http_connection_manager/config.h index 6b023bafb6fe..b98629ee8ecc 100644 --- a/source/extensions/filters/network/http_connection_manager/config.h +++ b/source/extensions/filters/network/http_connection_manager/config.h @@ -13,6 +13,7 @@ #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.validate.h" #include "envoy/filter/http/filter_config_provider.h" #include "envoy/http/filter.h" +#include "envoy/http/original_ip_detection.h" #include "envoy/http/request_id_extension.h" #include "envoy/router/route_config_provider_manager.h" #include "envoy/tracing/http_tracer_manager.h" @@ -186,6 +187,10 @@ class HttpConnectionManagerConfig : Logger::Loggable, pathWithEscapedSlashesAction() const override { return path_with_escaped_slashes_action_; } + const std::vector& + originalIpDetectionExtensions() const override { + return original_ip_detection_extensions_; + } private: enum class CodecType { HTTP1, HTTP2, HTTP3, AUTO }; @@ -268,6 +273,7 @@ class HttpConnectionManagerConfig : Logger::Loggable, const envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action_; const LocalReply::LocalReplyPtr local_reply_; + std::vector original_ip_detection_extensions_{}; // Default idle timeout is 5 minutes if nothing is specified in the HCM config. static const uint64_t StreamIdleTimeoutMs = 5 * 60 * 1000; diff --git a/source/extensions/http/original_ip_detection/custom_header/BUILD b/source/extensions/http/original_ip_detection/custom_header/BUILD new file mode 100644 index 000000000000..13f1a2000999 --- /dev/null +++ b/source/extensions/http/original_ip_detection/custom_header/BUILD @@ -0,0 +1,40 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "custom_header_lib", + srcs = ["custom_header.cc"], + hdrs = ["custom_header.h"], + # This extension is used from core tests. + visibility = ["//visibility:public"], + deps = [ + "//include/envoy/http:original_ip_detection_interface", + "//source/common/network:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + category = "envoy.http.original_ip_detection", + security_posture = "robust_to_untrusted_downstream", + # This extension is used from core tests. + visibility = ["//visibility:public"], + deps = [ + ":custom_header_lib", + "//include/envoy/http:original_ip_detection_interface", + "//include/envoy/registry", + "//source/common/config:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/http/original_ip_detection/custom_header/config.cc b/source/extensions/http/original_ip_detection/custom_header/config.cc new file mode 100644 index 000000000000..65defdccb738 --- /dev/null +++ b/source/extensions/http/original_ip_detection/custom_header/config.cc @@ -0,0 +1,35 @@ +#include "extensions/http/original_ip_detection/custom_header/config.h" + +#include "envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.pb.h" +#include "envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.pb.validate.h" +#include "envoy/http/original_ip_detection.h" +#include "envoy/registry/registry.h" + +#include "common/config/utility.h" + +#include "extensions/http/original_ip_detection/custom_header/custom_header.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace CustomHeader { + +Envoy::Http::OriginalIPDetectionSharedPtr +CustomHeaderIPDetectionFactory::createExtension(const Protobuf::Message& message, + Server::Configuration::FactoryContext& context) { + auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( + dynamic_cast(message), context.messageValidationVisitor(), *this); + const auto& proto_config = MessageUtil::downcastAndValidate< + const envoy::extensions::http::original_ip_detection::custom_header::v3::CustomHeaderConfig&>( + *mptr, context.messageValidationVisitor()); + return std::make_shared(proto_config); +} + +REGISTER_FACTORY(CustomHeaderIPDetectionFactory, Envoy::Http::OriginalIPDetectionFactory); + +} // namespace CustomHeader +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/original_ip_detection/custom_header/config.h b/source/extensions/http/original_ip_detection/custom_header/config.h new file mode 100644 index 000000000000..05e28c07b30e --- /dev/null +++ b/source/extensions/http/original_ip_detection/custom_header/config.h @@ -0,0 +1,36 @@ +#pragma once + +#include "envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.pb.h" +#include "envoy/http/original_ip_detection.h" + +#include "common/protobuf/protobuf.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace CustomHeader { + +/** + * Config registration for the custom header IP detection extension. + * @see OriginalIPDetectionFactory. + */ +class CustomHeaderIPDetectionFactory : public Envoy::Http::OriginalIPDetectionFactory { +public: + Envoy::Http::OriginalIPDetectionSharedPtr + createExtension(const Protobuf::Message& message, + Server::Configuration::FactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique< + envoy::extensions::http::original_ip_detection::custom_header::v3::CustomHeaderConfig>(); + } + + std::string name() const override { return "envoy.http.original_ip_detection.custom_header"; } +}; + +} // namespace CustomHeader +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/original_ip_detection/custom_header/custom_header.cc b/source/extensions/http/original_ip_detection/custom_header/custom_header.cc new file mode 100644 index 000000000000..9e7d9d7e8bcb --- /dev/null +++ b/source/extensions/http/original_ip_detection/custom_header/custom_header.cc @@ -0,0 +1,47 @@ +#include "extensions/http/original_ip_detection/custom_header/custom_header.h" + +#include "common/network/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace CustomHeader { + +CustomHeaderIPDetection::CustomHeaderIPDetection( + const envoy::extensions::http::original_ip_detection::custom_header::v3::CustomHeaderConfig& + config) + : header_name_(config.header_name()), + allow_trusted_address_checks_(config.allow_extension_to_set_address_as_trusted()) { + if (config.has_reject_with_status()) { + const auto reject_code = toErrorCode(config.reject_with_status().code()); + reject_options_ = {reject_code, ""}; + } +} + +CustomHeaderIPDetection::CustomHeaderIPDetection( + const std::string& header_name, + absl::optional reject_options) + : header_name_(header_name), reject_options_(reject_options) {} + +Envoy::Http::OriginalIPDetectionResult +CustomHeaderIPDetection::detect(Envoy::Http::OriginalIPDetectionParams& params) { + auto hdr = params.request_headers.get(header_name_); + if (hdr.empty()) { + return {nullptr, false, reject_options_}; + } + + auto header_value = hdr[0]->value().getStringView(); + auto addr = Network::Utility::parseInternetAddressNoThrow(std::string(header_value)); + if (addr) { + return {addr, allow_trusted_address_checks_, absl::nullopt}; + } + + return {nullptr, false, reject_options_}; +} + +} // namespace CustomHeader +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/original_ip_detection/custom_header/custom_header.h b/source/extensions/http/original_ip_detection/custom_header/custom_header.h new file mode 100644 index 000000000000..4a820b2c5beb --- /dev/null +++ b/source/extensions/http/original_ip_detection/custom_header/custom_header.h @@ -0,0 +1,47 @@ +#pragma once + +#include "envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.pb.h" +#include "envoy/http/codes.h" +#include "envoy/http/original_ip_detection.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace CustomHeader { + +/** + * Custom header IP detection extension. + */ +class CustomHeaderIPDetection : public Envoy::Http::OriginalIPDetection { +public: + CustomHeaderIPDetection( + const envoy::extensions::http::original_ip_detection::custom_header::v3::CustomHeaderConfig& + config); + CustomHeaderIPDetection( + const std::string& header_name, + absl::optional reject_options = absl::nullopt); + + Envoy::Http::OriginalIPDetectionResult + detect(Envoy::Http::OriginalIPDetectionParams& params) override; + +private: + static Envoy::Http::Code toErrorCode(uint64_t status) { + const auto code = static_cast(status); + if (code >= Envoy::Http::Code::BadRequest && + code <= Envoy::Http::Code::NetworkAuthenticationRequired) { + return code; + } + return Envoy::Http::Code::Forbidden; + } + + Envoy::Http::LowerCaseString header_name_; + bool allow_trusted_address_checks_{false}; + absl::optional reject_options_{absl::nullopt}; +}; + +} // namespace CustomHeader +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/original_ip_detection/xff/BUILD b/source/extensions/http/original_ip_detection/xff/BUILD new file mode 100644 index 000000000000..a247f485a1f5 --- /dev/null +++ b/source/extensions/http/original_ip_detection/xff/BUILD @@ -0,0 +1,40 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "xff_lib", + srcs = ["xff.cc"], + hdrs = ["xff.h"], + # This extension is core code. + visibility = ["//visibility:public"], + deps = [ + "//include/envoy/http:original_ip_detection_interface", + "//source/common/http:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/xff/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + category = "envoy.http.original_ip_detection", + security_posture = "robust_to_untrusted_downstream", + # This extension is core code. + visibility = ["//visibility:public"], + deps = [ + ":xff_lib", + "//include/envoy/http:original_ip_detection_interface", + "//include/envoy/registry", + "//source/common/config:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/xff/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/http/original_ip_detection/xff/config.cc b/source/extensions/http/original_ip_detection/xff/config.cc new file mode 100644 index 000000000000..57179a6a077f --- /dev/null +++ b/source/extensions/http/original_ip_detection/xff/config.cc @@ -0,0 +1,35 @@ +#include "extensions/http/original_ip_detection/xff/config.h" + +#include "envoy/extensions/http/original_ip_detection/xff/v3/xff.pb.h" +#include "envoy/extensions/http/original_ip_detection/xff/v3/xff.pb.validate.h" +#include "envoy/http/original_ip_detection.h" +#include "envoy/registry/registry.h" + +#include "common/config/utility.h" + +#include "extensions/http/original_ip_detection/xff/xff.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace Xff { + +Envoy::Http::OriginalIPDetectionSharedPtr +XffIPDetectionFactory::createExtension(const Protobuf::Message& message, + Server::Configuration::FactoryContext& context) { + auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( + dynamic_cast(message), context.messageValidationVisitor(), *this); + const auto& proto_config = MessageUtil::downcastAndValidate< + const envoy::extensions::http::original_ip_detection::xff::v3::XffConfig&>( + *mptr, context.messageValidationVisitor()); + return std::make_shared(proto_config); +} + +REGISTER_FACTORY(XffIPDetectionFactory, Envoy::Http::OriginalIPDetectionFactory); + +} // namespace Xff +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/original_ip_detection/xff/config.h b/source/extensions/http/original_ip_detection/xff/config.h new file mode 100644 index 000000000000..df3872adbab0 --- /dev/null +++ b/source/extensions/http/original_ip_detection/xff/config.h @@ -0,0 +1,35 @@ +#pragma once + +#include "envoy/extensions/http/original_ip_detection/xff/v3/xff.pb.h" +#include "envoy/http/original_ip_detection.h" + +#include "common/protobuf/protobuf.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace Xff { + +/** + * Config registration for the x-forwarded-for IP detection extension. + * @see OriginalIPDetectionFactory. + */ +class XffIPDetectionFactory : public Envoy::Http::OriginalIPDetectionFactory { +public: + Envoy::Http::OriginalIPDetectionSharedPtr + createExtension(const Protobuf::Message& message, + Server::Configuration::FactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() const override { return "envoy.http.original_ip_detection.xff"; } +}; + +} // namespace Xff +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/original_ip_detection/xff/xff.cc b/source/extensions/http/original_ip_detection/xff/xff.cc new file mode 100644 index 000000000000..70f142bb52df --- /dev/null +++ b/source/extensions/http/original_ip_detection/xff/xff.cc @@ -0,0 +1,29 @@ +#include "extensions/http/original_ip_detection/xff/xff.h" + +#include "common/http/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace Xff { + +XffIPDetection::XffIPDetection( + const envoy::extensions::http::original_ip_detection::xff::v3::XffConfig& config) + : xff_num_trusted_hops_(config.xff_num_trusted_hops()) {} + +XffIPDetection::XffIPDetection(uint32_t xff_num_trusted_hops) + : xff_num_trusted_hops_(xff_num_trusted_hops) {} + +Envoy::Http::OriginalIPDetectionResult +XffIPDetection::detect(Envoy::Http::OriginalIPDetectionParams& params) { + auto ret = + Envoy::Http::Utility::getLastAddressFromXFF(params.request_headers, xff_num_trusted_hops_); + return {ret.address_, ret.allow_trusted_address_checks_, absl::nullopt}; +} + +} // namespace Xff +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/original_ip_detection/xff/xff.h b/source/extensions/http/original_ip_detection/xff/xff.h new file mode 100644 index 000000000000..82d58a1aeaa8 --- /dev/null +++ b/source/extensions/http/original_ip_detection/xff/xff.h @@ -0,0 +1,31 @@ +#pragma once + +#include "envoy/extensions/http/original_ip_detection/xff/v3/xff.pb.h" +#include "envoy/http/original_ip_detection.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace Xff { + +/** + * XFF (x-forwarded-for) IP detection extension. + */ +class XffIPDetection : public Envoy::Http::OriginalIPDetection { +public: + XffIPDetection(const envoy::extensions::http::original_ip_detection::xff::v3::XffConfig& config); + XffIPDetection(uint32_t xff_num_trusted_hops); + + Envoy::Http::OriginalIPDetectionResult + detect(Envoy::Http::OriginalIPDetectionParams& params) override; + +private: + const uint32_t xff_num_trusted_hops_; +}; + +} // namespace Xff +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/server/admin/admin.h b/source/server/admin/admin.h index c9339f6925e9..84d5236e212b 100644 --- a/source/server/admin/admin.h +++ b/source/server/admin/admin.h @@ -189,6 +189,10 @@ class AdminImpl : public Admin, return envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager:: KEEP_UNCHANGED; } + const std::vector& + originalIpDetectionExtensions() const override { + return detection_extensions_; + } Http::Code request(absl::string_view path_and_query, absl::string_view method, Http::ResponseHeaderMap& response_headers, std::string& body) override; void closeSocket(); @@ -447,6 +451,7 @@ class AdminImpl : public Admin, AdminListenerPtr listener_; const AdminInternalAddressConfig internal_address_config_; const LocalReply::LocalReplyPtr local_reply_; + const std::vector detection_extensions_{}; }; } // namespace Server diff --git a/test/common/http/BUILD b/test/common/http/BUILD index 6a4aae6d35d6..0063b157ced8 100644 --- a/test/common/http/BUILD +++ b/test/common/http/BUILD @@ -214,6 +214,7 @@ envoy_cc_test( ], shard_count = 3, deps = [ + ":ip_detection_extensions_lib", "//source/common/http:conn_manager_lib", "//source/common/http:context_lib", "//source/extensions/access_loggers/common:file_access_log_lib", @@ -238,6 +239,7 @@ envoy_cc_test( name = "conn_manager_utility_test", srcs = ["conn_manager_utility_test.cc"], deps = [ + ":ip_detection_extensions_lib", "//source/common/common:random_generator_lib", "//source/common/event:dispatcher_lib", "//source/common/http:conn_manager_lib", @@ -480,3 +482,13 @@ envoy_cc_fuzz_test( "//test/fuzz:utility_lib", ], ) + +envoy_cc_test_library( + name = "ip_detection_extensions_lib", + srcs = ["ip_detection_extensions.cc"], + hdrs = ["ip_detection_extensions.h"], + deps = [ + "//source/extensions/http/original_ip_detection/custom_header:custom_header_lib", + "//source/extensions/http/original_ip_detection/xff:xff_lib", + ], +) diff --git a/test/common/http/conn_manager_impl_fuzz_test.cc b/test/common/http/conn_manager_impl_fuzz_test.cc index 56873a2b14a0..a62a208315ae 100644 --- a/test/common/http/conn_manager_impl_fuzz_test.cc +++ b/test/common/http/conn_manager_impl_fuzz_test.cc @@ -210,6 +210,10 @@ class FuzzConfig : public ConnectionManagerConfig { return envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager:: KEEP_UNCHANGED; } + const std::vector& + originalIpDetectionExtensions() const override { + return ip_detection_extensions_; + } const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager config_; @@ -255,6 +259,7 @@ class FuzzConfig : public ConnectionManagerConfig { Http::DefaultInternalAddressConfig internal_address_config_; bool normalize_path_{true}; LocalReply::LocalReplyPtr local_reply_; + std::vector ip_detection_extensions_{}; }; // Internal representation of stream state. Encapsulates the stream state, mocks diff --git a/test/common/http/conn_manager_impl_test_2.cc b/test/common/http/conn_manager_impl_test_2.cc index 96adb828b74a..f66028601513 100644 --- a/test/common/http/conn_manager_impl_test_2.cc +++ b/test/common/http/conn_manager_impl_test_2.cc @@ -1,4 +1,5 @@ #include "test/common/http/conn_manager_impl_test_base.h" +#include "test/common/http/ip_detection_extensions.h" #include "test/test_common/logging.h" #include "test/test_common/test_runtime.h" @@ -3014,5 +3015,28 @@ TEST_F(HttpConnectionManagerImplDeathTest, InvalidConnectionManagerConfig) { filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); } +TEST_F(HttpConnectionManagerImplTest, RequestRejectedViaIPDetection) { + OriginalIPRejectRequestOptions reject_options = {Http::Code::Forbidden, "ip detection failed"}; + auto extension = getCustomHeaderExtension("x-ip", reject_options); + ip_detection_extensions_.push_back(extension); + + use_remote_address_ = false; + + setup(false, ""); + + // 403 direct response when IP detection fails. + EXPECT_CALL(response_encoder_, encodeHeaders(_, false)) + .WillOnce(Invoke([](const ResponseHeaderMap& headers, bool) -> void { + EXPECT_EQ("403", headers.getStatusValue()); + })); + std::string response_body; + EXPECT_CALL(response_encoder_, encodeData(_, true)).WillOnce(AddBufferToString(&response_body)); + + startRequest(); + + EXPECT_EQ("ip detection failed", response_body); + EXPECT_EQ(1U, stats_.named_.downstream_rq_rejected_via_ip_detection_.value()); +} + } // namespace Http } // namespace Envoy diff --git a/test/common/http/conn_manager_impl_test_base.cc b/test/common/http/conn_manager_impl_test_base.cc index a38d61306858..cd9fa25b98e6 100644 --- a/test/common/http/conn_manager_impl_test_base.cc +++ b/test/common/http/conn_manager_impl_test_base.cc @@ -2,6 +2,8 @@ #include "extensions/request_id/uuid/config.h" +#include "test/common/http/ip_detection_extensions.h" + using testing::AtLeast; using testing::InSequence; using testing::InvokeWithoutArgs; @@ -33,6 +35,8 @@ HttpConnectionManagerImplTest::HttpConnectionManagerImplTest() // response_encoder_ is not a NiceMock on purpose. This prevents complaining about this // method only. EXPECT_CALL(response_encoder_, getStream()).Times(AtLeast(0)); + + ip_detection_extensions_.push_back(getXFFExtension(0)); } HttpConnectionManagerImplTest::~HttpConnectionManagerImplTest() { diff --git a/test/common/http/conn_manager_impl_test_base.h b/test/common/http/conn_manager_impl_test_base.h index 7029b8414be1..c8e174853fb4 100644 --- a/test/common/http/conn_manager_impl_test_base.h +++ b/test/common/http/conn_manager_impl_test_base.h @@ -148,6 +148,10 @@ class HttpConnectionManagerImplTest : public testing::Test, public ConnectionMan pathWithEscapedSlashesAction() const override { return path_with_escaped_slashes_action_; } + const std::vector& + originalIpDetectionExtensions() const override { + return ip_detection_extensions_; + } Envoy::Event::SimulatedTimeSystem test_time_; NiceMock route_config_provider_; @@ -213,6 +217,8 @@ class HttpConnectionManagerImplTest : public testing::Test, public ConnectionMan NiceMock upstream_conn_; // for websocket tests NiceMock conn_pool_; // for websocket tests RequestIDExtensionSharedPtr request_id_extension_; + std::vector ip_detection_extensions_{}; + const LocalReply::LocalReplyPtr local_reply_; // TODO(mattklein123): Not all tests have been converted over to better setup. Convert the rest. diff --git a/test/common/http/conn_manager_utility_test.cc b/test/common/http/conn_manager_utility_test.cc index 7226c2a00d7f..7dc2a957ebad 100644 --- a/test/common/http/conn_manager_utility_test.cc +++ b/test/common/http/conn_manager_utility_test.cc @@ -14,6 +14,7 @@ #include "extensions/request_id/uuid/config.h" +#include "test/common/http/ip_detection_extensions.h" #include "test/mocks/http/mocks.h" #include "test/mocks/local_info/mocks.h" #include "test/mocks/network/mocks.h" @@ -108,9 +109,18 @@ class ConnectionManagerUtilityTest : public testing::Test { ON_CALL(config_, pathWithEscapedSlashesAction()) .WillByDefault(Return(envoy::extensions::filters::network::http_connection_manager::v3:: HttpConnectionManager::KEEP_UNCHANGED)); + + detection_extensions_.push_back(getXFFExtension(0)); + ON_CALL(config_, originalIpDetectionExtensions()) + .WillByDefault(ReturnRef(detection_extensions_)); } struct MutateRequestRet { + MutateRequestRet() = default; + MutateRequestRet(const std::string& downstream_address, bool internal, + Tracing::Reason trace_reason) + : downstream_address_(downstream_address), internal_(internal), + trace_reason_(trace_reason) {} bool operator==(const MutateRequestRet& rhs) const { return downstream_address_ == rhs.downstream_address_ && internal_ == rhs.internal_ && trace_reason_ == rhs.trace_reason_; @@ -119,6 +129,7 @@ class ConnectionManagerUtilityTest : public testing::Test { std::string downstream_address_; bool internal_; Tracing::Reason trace_reason_; + absl::optional reject_request_{absl::nullopt}; }; // This is a convenience method used to call mutateRequestHeaders(). It is done in this @@ -126,9 +137,10 @@ class ConnectionManagerUtilityTest : public testing::Test { // the request is internal/external, given the importance of these two pieces of data. MutateRequestRet callMutateRequestHeaders(RequestHeaderMap& headers, Protocol) { MutateRequestRet ret; - ret.downstream_address_ = ConnectionManagerUtility::mutateRequestHeaders( - headers, connection_, config_, route_config_, local_info_) - ->asString(); + const auto result = ConnectionManagerUtility::mutateRequestHeaders( + headers, connection_, config_, route_config_, local_info_); + ret.downstream_address_ = result.final_remote_address->asString(); + ret.reject_request_ = result.reject_request; ret.trace_reason_ = ConnectionManagerUtility::mutateTracingRequestHeader(headers, runtime_, config_, &route_); ret.internal_ = HeaderUtility::isEnvoyInternalRequest(headers); @@ -139,6 +151,7 @@ class ConnectionManagerUtilityTest : public testing::Test { NiceMock random_; const std::shared_ptr request_id_extension_; const std::shared_ptr request_id_extension_to_return_; + std::vector detection_extensions_{}; NiceMock config_; NiceMock route_config_; NiceMock route_; @@ -367,6 +380,11 @@ TEST_F(ConnectionManagerUtilityTest, UseRemoteAddressWithXFFTrustedHops) { // Verify that xff_num_trusted_hops works when not using remote address. TEST_F(ConnectionManagerUtilityTest, UseXFFTrustedHopsWithoutRemoteAddress) { + // Reconfigure XFF detection. + detection_extensions_.clear(); + detection_extensions_.push_back(getXFFExtension(1)); + ON_CALL(config_, originalIpDetectionExtensions()).WillByDefault(ReturnRef(detection_extensions_)); + connection_.stream_info_.downstream_address_provider_->setRemoteAddress( std::make_shared("127.0.0.1")); ON_CALL(config_, useRemoteAddress()).WillByDefault(Return(false)); @@ -1758,5 +1776,32 @@ TEST_F(ConnectionManagerUtilityTest, NoPreserveExternalRequestIdNoEdgeRequest) { EXPECT_EQ("my-request-id", headers.get_(Headers::get().RequestId)); } } + +// Test detecting the original IP via a header (no rejection if it fails). +TEST_F(ConnectionManagerUtilityTest, OriginalIPDetectionExtension) { + const std::string header_name = "x-cdn-detected-ip"; + auto detection_extension = getCustomHeaderExtension(header_name); + const std::vector extensions = {detection_extension}; + + ON_CALL(config_, originalIpDetectionExtensions()).WillByDefault(ReturnRef(extensions)); + ON_CALL(config_, useRemoteAddress()).WillByDefault(Return(false)); + + // Header is present. + { + TestRequestHeaderMapImpl headers{{header_name, "2.1.3.4"}}; + auto ret = callMutateRequestHeaders(headers, Protocol::Http11); + EXPECT_EQ(ret.downstream_address_, "2.1.3.4:0"); + EXPECT_EQ(ret.reject_request_, absl::nullopt); + } + + // Header missing -- fallbacks to default behavior. + { + TestRequestHeaderMapImpl headers; + auto ret = callMutateRequestHeaders(headers, Protocol::Http11); + EXPECT_EQ(ret.downstream_address_, "10.0.0.3:50000"); + EXPECT_EQ(ret.reject_request_, absl::nullopt); + } +} + } // namespace Http } // namespace Envoy diff --git a/test/common/http/ip_detection_extensions.cc b/test/common/http/ip_detection_extensions.cc new file mode 100644 index 000000000000..95c2abec8488 --- /dev/null +++ b/test/common/http/ip_detection_extensions.cc @@ -0,0 +1,25 @@ +#include "ip_detection_extensions.h" + +#include "extensions/http/original_ip_detection/custom_header/custom_header.h" +#include "extensions/http/original_ip_detection/xff/xff.h" + +namespace Envoy { + +Http::OriginalIPDetectionSharedPtr getXFFExtension(uint32_t hops) { + return std::make_shared(hops); +} + +Http::OriginalIPDetectionSharedPtr getCustomHeaderExtension(const std::string& header_name) { + return std::make_shared< + Extensions::Http::OriginalIPDetection::CustomHeader::CustomHeaderIPDetection>(header_name); +} + +Http::OriginalIPDetectionSharedPtr +getCustomHeaderExtension(const std::string& header_name, + Http::OriginalIPRejectRequestOptions reject_options) { + return std::make_shared< + Extensions::Http::OriginalIPDetection::CustomHeader::CustomHeaderIPDetection>(header_name, + reject_options); +} + +} // namespace Envoy diff --git a/test/common/http/ip_detection_extensions.h b/test/common/http/ip_detection_extensions.h new file mode 100644 index 000000000000..32863f20a1a1 --- /dev/null +++ b/test/common/http/ip_detection_extensions.h @@ -0,0 +1,14 @@ +#pragma once + +#include "envoy/http/original_ip_detection.h" + +// This helper is used to escape namespace pollution issues. +namespace Envoy { + +Http::OriginalIPDetectionSharedPtr getXFFExtension(uint32_t hops); +Http::OriginalIPDetectionSharedPtr getCustomHeaderExtension(const std::string& header_name); +Http::OriginalIPDetectionSharedPtr +getCustomHeaderExtension(const std::string& header_name, + Http::OriginalIPRejectRequestOptions reject_options); + +} // namespace Envoy diff --git a/test/common/http/utility_test.cc b/test/common/http/utility_test.cc index fcf26b70e9c1..c8c9d311af5f 100644 --- a/test/common/http/utility_test.cc +++ b/test/common/http/utility_test.cc @@ -454,16 +454,16 @@ TEST(HttpUtility, getLastAddressFromXFF) { {"x-forwarded-for", "192.0.2.10, 192.0.2.1, 10.0.0.1"}}; auto ret = Utility::getLastAddressFromXFF(request_headers); EXPECT_EQ(third_address, ret.address_->ip()->addressAsString()); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); ret = Utility::getLastAddressFromXFF(request_headers, 1); EXPECT_EQ(second_address, ret.address_->ip()->addressAsString()); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); ret = Utility::getLastAddressFromXFF(request_headers, 2); EXPECT_EQ(first_address, ret.address_->ip()->addressAsString()); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); ret = Utility::getLastAddressFromXFF(request_headers, 3); EXPECT_EQ(nullptr, ret.address_); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); } { const std::string first_address = "192.0.2.10"; @@ -476,64 +476,64 @@ TEST(HttpUtility, getLastAddressFromXFF) { // No space on the left. auto ret = Utility::getLastAddressFromXFF(request_headers); EXPECT_EQ(fourth_address, ret.address_->ip()->addressAsString()); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); // No space on either side. ret = Utility::getLastAddressFromXFF(request_headers, 1); EXPECT_EQ(third_address, ret.address_->ip()->addressAsString()); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); // Exercise rtrim() and ltrim(). ret = Utility::getLastAddressFromXFF(request_headers, 2); EXPECT_EQ(second_address, ret.address_->ip()->addressAsString()); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); // No space trimming. ret = Utility::getLastAddressFromXFF(request_headers, 3); EXPECT_EQ(first_address, ret.address_->ip()->addressAsString()); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); // No address found. ret = Utility::getLastAddressFromXFF(request_headers, 4); EXPECT_EQ(nullptr, ret.address_); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); } { TestRequestHeaderMapImpl request_headers{{"x-forwarded-for", ""}}; auto ret = Utility::getLastAddressFromXFF(request_headers); EXPECT_EQ(nullptr, ret.address_); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); } { TestRequestHeaderMapImpl request_headers{{"x-forwarded-for", ","}}; auto ret = Utility::getLastAddressFromXFF(request_headers); EXPECT_EQ(nullptr, ret.address_); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); } { TestRequestHeaderMapImpl request_headers{{"x-forwarded-for", ", "}}; auto ret = Utility::getLastAddressFromXFF(request_headers); EXPECT_EQ(nullptr, ret.address_); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); } { TestRequestHeaderMapImpl request_headers{{"x-forwarded-for", ", bad"}}; auto ret = Utility::getLastAddressFromXFF(request_headers); EXPECT_EQ(nullptr, ret.address_); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); } { TestRequestHeaderMapImpl request_headers; auto ret = Utility::getLastAddressFromXFF(request_headers); EXPECT_EQ(nullptr, ret.address_); - EXPECT_FALSE(ret.single_address_); + EXPECT_FALSE(ret.allow_trusted_address_checks_); } { const std::string first_address = "34.0.0.1"; TestRequestHeaderMapImpl request_headers{{"x-forwarded-for", first_address}}; auto ret = Utility::getLastAddressFromXFF(request_headers); EXPECT_EQ(first_address, ret.address_->ip()->addressAsString()); - EXPECT_TRUE(ret.single_address_); + EXPECT_TRUE(ret.allow_trusted_address_checks_); } } diff --git a/test/extensions/filters/network/http_connection_manager/BUILD b/test/extensions/filters/network/http_connection_manager/BUILD index 5124dc2fa9bb..aa3feb45998f 100644 --- a/test/extensions/filters/network/http_connection_manager/BUILD +++ b/test/extensions/filters/network/http_connection_manager/BUILD @@ -50,6 +50,7 @@ envoy_extension_cc_test( "//source/common/buffer:buffer_lib", "//source/extensions/access_loggers/file:config", "//source/extensions/filters/network/http_connection_manager:config", + "//source/extensions/http/original_ip_detection/custom_header:config", "//test/integration/filters:encoder_decoder_buffer_filter_lib", "//test/mocks/network:network_mocks", "//test/mocks/server:factory_context_mocks", diff --git a/test/extensions/filters/network/http_connection_manager/config_test.cc b/test/extensions/filters/network/http_connection_manager/config_test.cc index 90bc34105da5..7f4b8daec07e 100644 --- a/test/extensions/filters/network/http_connection_manager/config_test.cc +++ b/test/extensions/filters/network/http_connection_manager/config_test.cc @@ -1782,6 +1782,89 @@ TEST_F(HttpConnectionManagerConfigTest, DefaultRequestIDExtensionWithParams) { EXPECT_FALSE(request_id_extension->packTraceReason()); } +TEST_F(HttpConnectionManagerConfigTest, UnknownOriginalIPDetectionExtension) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + route_config: + name: local_route + original_ip_detection_extensions: + - name: envoy.http.original_ip_detection.UnknownOriginalIPDetectionExtension + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + http_filters: + - name: envoy.filters.http.router + )EOF"; + + EXPECT_THROW_WITH_REGEX(createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "Original IP detection extension not found: " + "'envoy.http.original_ip_detection.UnknownOriginalIPDetectionExtension'"); +} + +namespace { + +class OriginalIPDetectionExtensionNotCreatedFactory : public Http::OriginalIPDetectionFactory { +public: + Http::OriginalIPDetectionSharedPtr + createExtension(const Protobuf::Message&, Server::Configuration::FactoryContext&) override { + return nullptr; + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() const override { + return "envoy.http.original_ip_detection.OriginalIPDetectionExtensionNotCreated"; + } +}; + +} // namespace + +TEST_F(HttpConnectionManagerConfigTest, OriginalIPDetectionExtensionNotCreated) { + OriginalIPDetectionExtensionNotCreatedFactory factory; + Registry::InjectFactory registration(factory); + + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + route_config: + name: local_route + original_ip_detection_extensions: + - name: envoy.http.original_ip_detection.OriginalIPDetectionExtensionNotCreated + typed_config: + "@type": type.googleapis.com/google.protobuf.UInt32Value + http_filters: + - name: envoy.filters.http.router + )EOF"; + + EXPECT_THROW_WITH_REGEX( + createHttpConnectionManagerConfig(yaml_string), EnvoyException, + "Original IP detection extension could not be created: " + "'envoy.http.original_ip_detection.OriginalIPDetectionExtensionNotCreated'"); +} + +TEST_F(HttpConnectionManagerConfigTest, OriginalIPDetectionExtension) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + route_config: + name: local_route + original_ip_detection_extensions: + - name: envoy.http.original_ip_detection.custom_header + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.original_ip_detection.custom_header.v3.CustomHeaderConfig + header_name: x-ip-header + http_filters: + - name: envoy.filters.http.router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); + + const auto& original_ip_detection_extensions = config.originalIpDetectionExtensions(); + EXPECT_EQ(1, original_ip_detection_extensions.size()); +} + TEST_F(HttpConnectionManagerConfigTest, DynamicFilterWarmingNoDefault) { const std::string yaml_string = R"EOF( codec_type: http1 diff --git a/test/extensions/http/original_ip_detection/custom_header/BUILD b/test/extensions/http/original_ip_detection/custom_header/BUILD new file mode 100644 index 000000000000..7a02e5ec778f --- /dev/null +++ b/test/extensions/http/original_ip_detection/custom_header/BUILD @@ -0,0 +1,38 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "custom_header_detection_test", + srcs = ["custom_header_test.cc"], + extension_name = "envoy.http.original_ip_detection.custom_header", + deps = [ + "//source/common/network:utility_lib", + "//source/extensions/http/original_ip_detection/custom_header:custom_header_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.http.original_ip_detection.custom_header", + deps = [ + "//include/envoy/registry", + "//source/extensions/http/original_ip_detection/custom_header:config", + "//source/extensions/http/original_ip_detection/custom_header:custom_header_lib", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/http/original_ip_detection/custom_header/config_test.cc b/test/extensions/http/original_ip_detection/custom_header/config_test.cc new file mode 100644 index 000000000000..54e42644c053 --- /dev/null +++ b/test/extensions/http/original_ip_detection/custom_header/config_test.cc @@ -0,0 +1,59 @@ +#include "envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.pb.h" +#include "envoy/registry/registry.h" + +#include "extensions/http/original_ip_detection/custom_header/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace CustomHeader { + +TEST(CustomHeaderFactoryTest, Basic) { + auto* factory = Registry::FactoryRegistry::getFactory( + "envoy.http.original_ip_detection.custom_header"); + ASSERT_NE(factory, nullptr); + + envoy::config::core::v3::TypedExtensionConfig typed_config; + const std::string yaml = R"EOF( + name: envoy.formatter.TestFormatter + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.original_ip_detection.custom_header.v3.CustomHeaderConfig + header_name: x-real-ip +)EOF"; + TestUtility::loadFromYaml(yaml, typed_config); + + NiceMock context; + EXPECT_NE(factory->createExtension(typed_config.typed_config(), context), nullptr); +} + +TEST(CustomHeaderFactoryTest, InvalidHeaderName) { + auto* factory = Registry::FactoryRegistry::getFactory( + "envoy.http.original_ip_detection.custom_header"); + ASSERT_NE(factory, nullptr); + + envoy::config::core::v3::TypedExtensionConfig typed_config; + const std::string yaml = R"EOF( + name: envoy.formatter.TestFormatter + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.original_ip_detection.custom_header.v3.CustomHeaderConfig + header_name: " " +)EOF"; + TestUtility::loadFromYaml(yaml, typed_config); + + NiceMock context; + EXPECT_THROW_WITH_REGEX(factory->createExtension(typed_config.typed_config(), context), + EnvoyException, + "Proto constraint validation failed.*does not match regex pattern.*"); +} + +} // namespace CustomHeader +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/http/original_ip_detection/custom_header/custom_header_test.cc b/test/extensions/http/original_ip_detection/custom_header/custom_header_test.cc new file mode 100644 index 000000000000..e1a14c133d73 --- /dev/null +++ b/test/extensions/http/original_ip_detection/custom_header/custom_header_test.cc @@ -0,0 +1,107 @@ +#include "envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.pb.h" + +#include "common/network/utility.h" + +#include "extensions/http/original_ip_detection/custom_header/custom_header.h" + +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace CustomHeader { + +class CustomHeaderTest : public testing::Test { +protected: + CustomHeaderTest() { configure(); } + + void configure(envoy::type::v3::StatusCode code = envoy::type::v3::StatusCode::Unauthorized) { + envoy::extensions::http::original_ip_detection::custom_header::v3::CustomHeaderConfig config; + config.set_header_name("x-real-ip"); + config.set_allow_extension_to_set_address_as_trusted(true); + auto* reject_with_status = config.mutable_reject_with_status(); + reject_with_status->set_code(code); + custom_header_extension_ = std::make_shared(config); + } + + std::shared_ptr custom_header_extension_; +}; + +TEST_F(CustomHeaderTest, Detection) { + // Header missing. + { + Envoy::Http::TestRequestHeaderMapImpl headers{{"x-other", "abc"}}; + Envoy::Http::OriginalIPDetectionParams params = {headers, nullptr}; + auto result = custom_header_extension_->detect(params); + + EXPECT_EQ(nullptr, result.detected_remote_address); + EXPECT_FALSE(result.allow_trusted_address_checks); + EXPECT_TRUE(result.reject_options.has_value()); + + const auto& reject_options = result.reject_options.value(); + EXPECT_EQ(reject_options.response_code, Envoy::Http::Code::Unauthorized); + EXPECT_EQ(reject_options.body, ""); + } + + // Bad IP in the header. + { + Envoy::Http::TestRequestHeaderMapImpl headers{{"x-real-ip", "not-a-real-ip"}}; + Envoy::Http::OriginalIPDetectionParams params = {headers, nullptr}; + auto result = custom_header_extension_->detect(params); + + EXPECT_EQ(nullptr, result.detected_remote_address); + EXPECT_FALSE(result.allow_trusted_address_checks); + EXPECT_TRUE(result.reject_options.has_value()); + + const auto& reject_options = result.reject_options.value(); + EXPECT_EQ(reject_options.response_code, Envoy::Http::Code::Unauthorized); + EXPECT_EQ(reject_options.body, ""); + } + + // Good IPv4. + { + Envoy::Http::TestRequestHeaderMapImpl headers{{"x-real-ip", "1.2.3.4"}}; + Envoy::Http::OriginalIPDetectionParams params = {headers, nullptr}; + auto result = custom_header_extension_->detect(params); + + EXPECT_EQ("1.2.3.4:0", result.detected_remote_address->asString()); + EXPECT_TRUE(result.allow_trusted_address_checks); + EXPECT_FALSE(result.reject_options.has_value()); + } + + // Good IPv6. + { + Envoy::Http::TestRequestHeaderMapImpl headers{{"x-real-ip", "fc00::1"}}; + Envoy::Http::OriginalIPDetectionParams params = {headers, nullptr}; + auto result = custom_header_extension_->detect(params); + + EXPECT_EQ("[fc00::1]:0", result.detected_remote_address->asString()); + EXPECT_TRUE(result.allow_trusted_address_checks); + EXPECT_FALSE(result.reject_options.has_value()); + } +} + +TEST_F(CustomHeaderTest, FallbacksToDefaultResponseCode) { + configure(envoy::type::v3::StatusCode::OK); + + Envoy::Http::TestRequestHeaderMapImpl headers{{"x-other", "abc"}}; + Envoy::Http::OriginalIPDetectionParams params = {headers, nullptr}; + auto result = custom_header_extension_->detect(params); + + EXPECT_EQ(nullptr, result.detected_remote_address); + EXPECT_FALSE(result.allow_trusted_address_checks); + EXPECT_TRUE(result.reject_options.has_value()); + + const auto& reject_options = result.reject_options.value(); + EXPECT_EQ(reject_options.response_code, Envoy::Http::Code::Forbidden); + EXPECT_EQ(reject_options.body, ""); +} + +} // namespace CustomHeader +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/http/original_ip_detection/xff/BUILD b/test/extensions/http/original_ip_detection/xff/BUILD new file mode 100644 index 000000000000..14d79ac5cb50 --- /dev/null +++ b/test/extensions/http/original_ip_detection/xff/BUILD @@ -0,0 +1,38 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "xff_detection_test", + srcs = ["xff_test.cc"], + extension_name = "envoy.http.original_ip_detection.xff", + deps = [ + "//source/common/http:utility_lib", + "//source/extensions/http/original_ip_detection/xff:xff_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/xff/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.http.original_ip_detection.xff", + deps = [ + "//include/envoy/registry", + "//source/extensions/http/original_ip_detection/xff:config", + "//source/extensions/http/original_ip_detection/xff:xff_lib", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/xff/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/http/original_ip_detection/xff/config_test.cc b/test/extensions/http/original_ip_detection/xff/config_test.cc new file mode 100644 index 000000000000..0f7f1ace9632 --- /dev/null +++ b/test/extensions/http/original_ip_detection/xff/config_test.cc @@ -0,0 +1,39 @@ +#include "envoy/extensions/http/original_ip_detection/xff/v3/xff.pb.h" +#include "envoy/registry/registry.h" + +#include "extensions/http/original_ip_detection/xff/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace Xff { + +TEST(CustomHeaderFactoryTest, Basic) { + auto* factory = Registry::FactoryRegistry::getFactory( + "envoy.http.original_ip_detection.xff"); + ASSERT_NE(factory, nullptr); + + envoy::config::core::v3::TypedExtensionConfig typed_config; + const std::string yaml = R"EOF( + name: envoy.formatter.TestFormatter + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig + xff_num_trusted_hops: 1 +)EOF"; + TestUtility::loadFromYaml(yaml, typed_config); + + NiceMock context; + EXPECT_NE(factory->createExtension(typed_config.typed_config(), context), nullptr); +} + +} // namespace Xff +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/http/original_ip_detection/xff/xff_test.cc b/test/extensions/http/original_ip_detection/xff/xff_test.cc new file mode 100644 index 000000000000..f03ac1a307ca --- /dev/null +++ b/test/extensions/http/original_ip_detection/xff/xff_test.cc @@ -0,0 +1,52 @@ +#include "envoy/extensions/http/original_ip_detection/xff/v3/xff.pb.h" + +#include "extensions/http/original_ip_detection/xff/xff.h" + +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace OriginalIPDetection { +namespace Xff { + +class XffTest : public testing::Test { +protected: + XffTest() { + envoy::extensions::http::original_ip_detection::xff::v3::XffConfig config; + config.set_xff_num_trusted_hops(1); + xff_extension_ = std::make_shared(config); + } + + std::shared_ptr xff_extension_; +}; + +TEST_F(XffTest, Detection) { + // Header missing. + { + Envoy::Http::TestRequestHeaderMapImpl headers{{"x-other", "abc"}}; + Envoy::Http::OriginalIPDetectionParams params = {headers, nullptr}; + auto result = xff_extension_->detect(params); + + EXPECT_EQ(nullptr, result.detected_remote_address); + EXPECT_FALSE(result.allow_trusted_address_checks); + } + + // Good request. + { + Envoy::Http::TestRequestHeaderMapImpl headers{{"x-forwarded-for", "1.2.3.4,2.2.2.2"}}; + Envoy::Http::OriginalIPDetectionParams params = {headers, nullptr}; + auto result = xff_extension_->detect(params); + + EXPECT_EQ("1.2.3.4:0", result.detected_remote_address->asString()); + EXPECT_FALSE(result.allow_trusted_address_checks); + } +} + +} // namespace Xff +} // namespace OriginalIPDetection +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/test/integration/BUILD b/test/integration/BUILD index 014e08b26d0e..3648cea48ffa 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -299,6 +299,7 @@ envoy_cc_test( ":http_integration_lib", "//source/common/config:api_version_lib", "//source/common/protobuf", + "//source/extensions/http/original_ip_detection/xff:config", "//test/test_common:utility_lib", "@envoy_api//envoy/api/v2:pkg_cc_proto", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", @@ -1730,3 +1731,16 @@ envoy_cc_test( "@com_googlesource_quiche//:quic_test_tools_session_peer_lib", ]), ) + +envoy_cc_test( + name = "original_ip_detection_integration_test", + srcs = [ + "original_ip_detection_integration_test.cc", + ], + deps = [ + ":http_integration_lib", + "//source/extensions/http/original_ip_detection/custom_header:config", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/http/original_ip_detection/custom_header/v3:pkg_cc_proto", + ], +) diff --git a/test/integration/header_integration_test.cc b/test/integration/header_integration_test.cc index 004542631490..073c05bf097b 100644 --- a/test/integration/header_integration_test.cc +++ b/test/integration/header_integration_test.cc @@ -42,7 +42,11 @@ const std::string http_connection_mgr_config = R"EOF( - name: envoy.filters.http.router codec_type: HTTP1 use_remote_address: false -xff_num_trusted_hops: 1 +original_ip_detection_extensions: +- name: envoy.http.original_ip_detection.xff + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig + xff_num_trusted_hops: 1 stat_prefix: header_test route_config: virtual_hosts: diff --git a/test/integration/original_ip_detection_integration_test.cc b/test/integration/original_ip_detection_integration_test.cc new file mode 100644 index 000000000000..9fa25edce17e --- /dev/null +++ b/test/integration/original_ip_detection_integration_test.cc @@ -0,0 +1,55 @@ +#include "envoy/extensions/http/original_ip_detection/custom_header/v3/custom_header.pb.h" + +#include "test/integration/http_integration.h" +#include "test/test_common/registry.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +using testing::HasSubstr; + +namespace Envoy { +namespace Formatter { + +class OriginalIPDetectionIntegrationTest + : public testing::TestWithParam, + public HttpIntegrationTest { +public: + OriginalIPDetectionIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam()) {} + + void runTest(const std::string& ip) { + useAccessLog("%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"); + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { + envoy::extensions::http::original_ip_detection::custom_header::v3::CustomHeaderConfig + config; + config.set_header_name("x-cdn-detected-ip"); + + auto* extension = hcm.add_original_ip_detection_extensions(); + extension->set_name("envoy.http.original_ip_detection.custom_header"); + extension->mutable_typed_config()->PackFrom(config); + + hcm.mutable_use_remote_address()->set_value(false); + }); + initialize(); + auto raw_http = + fmt::format("GET / HTTP/1.1\r\nHost: host\r\nx-cdn-detected-ip: {}\r\n\r\n", ip); + std::string response; + sendRawHttpAndWaitForResponse(lookupPort("http"), raw_http.c_str(), &response, true); + std::string log = waitForAccessLog(access_log_name_); + EXPECT_THAT(log, HasSubstr(ip)); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, OriginalIPDetectionIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(OriginalIPDetectionIntegrationTest, HeaderBasedDetectionIPv4) { runTest("9.9.9.9"); } + +TEST_P(OriginalIPDetectionIntegrationTest, HeaderBasedDetectionIPv6) { runTest("fc00::1"); } + +} // namespace Formatter +} // namespace Envoy diff --git a/test/mocks/http/mocks.h b/test/mocks/http/mocks.h index 31a40da3a354..da5cb0b8a497 100644 --- a/test/mocks/http/mocks.h +++ b/test/mocks/http/mocks.h @@ -589,6 +589,8 @@ class MockConnectionManagerConfig : public ConnectionManagerConfig { MOCK_METHOD(envoy::extensions::filters::network::http_connection_manager::v3:: HttpConnectionManager::PathWithEscapedSlashesAction, pathWithEscapedSlashesAction, (), (const)); + MOCK_METHOD(const std::vector&, originalIpDetectionExtensions, + (), (const)); std::unique_ptr internal_address_config_ = std::make_unique();