如何通过 Envoy Gateway 得到客户端的真实 IP 地址?

Posted by Huabing Blog on Monday, May 20, 2024

前言

河水出昆仑,东流经玉门,环绝壁,历五山,南至积石,东流入海。其流也,或曲或直,时急时缓,遇山则环,逢谷则奔。渐行渐远,百折千回,至于中原,汇百川,泽九州。其道也,蜿蜒盘旋,绵延万里,波澜壮阔,历千古而不息。——《水经注》

河水从源头出发,经过千折百回,才能流入大海。在网络世界中,一个 HTTP 请求从客户端发出,也要经过多个网络节点,最终才能到达服务器。在这个过程中,请求的源 IP 地址在经过代理服务器、负载均衡器等中间节点时会发生变化,导致服务器只能看到和其直接相连的节点的源地址,无法获取到客户端的真实 IP 地址。

一个 HTTP 请求在经过多次网络节点后丢失了客户端 IP 地址

上图是一个简化的示意图,展示了 HTTP 请求从客户端到服务器的路径。图中省略了一些细节,例如服务器可能有多个 IP 地址,或者 IP 地址可能会被网络地址转换(NAT)改变,以便我们能专注于主要概念。 而在对请求进行处理时,后端经常需要得到客户端的真实 IP 地址,主要有下面这些原因:

  1. 欺诈预防:客户端 IP 地址可以帮助识别恶意行为者,并阻止与滥用行为、黑客攻击或拒绝服务攻击相关的特定 IP 地址。

  2. 访问控制:一些系统基于 IP 地址限制对某些资源的访问。了解客户端 IP 地址可以让您实施白名单策略。

  3. 用户体验:从客户端 IP 地址获取的地理位置信息可以用于根据用户所在位置定制内容,例如显示本地化内容或语言。

  4. 应用性能:客户端 IP 地址用于实施速率限制,防止滥用并确保资源的公平使用。它还可以用于有效分配流量和保持会话一致性。

Envoy 提供了多种方法来获取客户端的 IP 地址,包括使用 X-Forwarded-For HTTP 头、自定义 HTTP 头以及代理协议。

本文将探讨这些方法,详细介绍如何在 Envoy 中配置每一种方法。此外,我们还将演示如何使用 Envoy Gateway 来简化 Envoy 的配置,并讨论如何利用客户端的 IP 地址进行流量管理,例如访问控制和速率限制。

X-Forwarded-For HTTP Header

什么是 X-Forwarded-For?

X-Forwarded-For 是一个 HTTP 请求Header,常用于代理和负载均衡器环境中,以标识发出请求的客户端的原始 IP 地址。当一个请求经过代理或负载均衡器时,该节点可以在 HTTP 请求中添加或更新 X-Forwarded-For Header,通过这种方式,原始客户端的 IP 可以被保留下来。

这个 Header 可以包含单个 IP 地址(最初的客户端),也可以包含一个 IP 地址链,反映了请求路径中的每一个代理。格式通常是一个逗号分隔的 IP 地址列表,如下所示:

X-Forwarded-For: client, proxy1, proxy2, ...

这里,client 是原始发起请求的客户端的 IP,proxy1 和 proxy2 是该请求经过的第一个和第二个代理服务器的 IP 地址。请求途径的每个代理会将和自己直接通信的上一个节点的 IP 地址添加到 X-Forwarded-For Header 中,这样服务器就可以通过解析这个 Header 来获取客户端的真实 IP 地址。

假设客户端发出的 HTTP 请求需要经过两个代理服务器后才能到达服务器端,请求的路径如下:

一个途经 CDN 服务器和负载均衡器两个网络节点的 HTTP 请求

在这个过程中,HTTP 请求通过两个代理,每个代理都会发起一个新的 TCP 连接。当请求经过每个代理时,代理会将转发的 TCP 连接的源 IP 地址附加到 X-Forwarded-For 头中。

以下是每个 TCP 连接的源地址和目标地址,以及相应的 HTTP X-Forwarded-For 头的内容:

TCP 连接 源 IP 地址 目的 IP 地址 XFF Header
1 From Client to CDN 146.74.94.117 198.40.10.101
2 From CDN to Load Balancer 198.40.10.101 198.40.10.102 146.74.94.117
3 From Load Balancer to Server 198.40.10.102 Server IP 146.74.94.117,198.40.10.101

从上表可以看到,每段 TCP 连接的源地址都发生了变化,但是途径的两个代理服务器(CDN 和 Load Balancer)都会将和自己直接连接的节点源地址添加到在 HTTP 请求的 X-Forwarded-For Header。服务器随后可以从 X-Forwarded-For 头中提取客户端的 IP 地址。由于知道有两个中间网络节点,服务器选择从最右边开始数第二个值作为客户端的真实 IP。

通过 X-Forwarded-For (XFF) 头转发的客户端 IP 地址

采用 X-Forwarded-For Header 的优点是它是一个(事实)标准的 HTTP Header,易于实现和解析。大多数代理服务器和负载均衡器都支持添加 X-Forwarded-For。而缺点是它容易被伪造,请求途径的任何一个中间节点都可以添加或修改这个 Header,所以在使用 X-Forwarded-For 时需要确保其来源可信。

在 Envoy 中如何配置 X-Forwarded-For?

下面我们来看一下如何在 Envoy 中配置 X-Forwarded-For Header,以便获取客户端的真实 IP 地址。

Envoy 支持采用两种方式来从 X-Forwarded-For Header中提取客户端的真实 IP 地址,分别是通过 HCM(HTTP Connection Manager)和 IP Detection Extension。下面将介绍这两种方法的配置步骤。

在 HCM 中配置 X-Forwarded-For

在 Envoy 的 HCM 中配置 X-Forwarded-For Header的提取,可以通过设置 xffNumTrustedHops 参数来实现。此参数定义了 Envoy 在 X-Forwarded-For 头中应信任的 IP 地址数量。根据您的网络拓扑结构调整 xffNumTrustedHops 以进行正确配置。

例如,考虑这样的请求路径: client -> proxy1 -> proxy2 -> Envoy,proxy1 和 proxy2 位于受信任的网络中,它们都会修改 X-Forwarded-For Header,那么 Envoy 收到的一个请求的 X-Forwarded-For Header 可能是这样的:

X-Forwarded-For: client, proxy1

在这种情况下,我们需要将 xffNumTrustedHops 设置为 2,即 Envoy 信任的 IP 地址数量为 2。Envoy 会按从右到左的顺序从 X-Forwarded-For Header 中提取 xffNumTrustedHops 指定的那个 IP 地址,将其作为该请求的客户端地址。

下面是对应的 Envoy 配置示例:

"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
  "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
  // omitted for brevity
  // ...
   
  "xffNumTrustedHops": 2
}

在这个配置中,Envoy 会提取 X-Forwarded-For Header 中倒数的第二个 IP 地址(即 client 的 IP 地址)作为客户端的真实 IP 地址。

只要我们能够确保 xffNumTrustedHops 中设置的中间节点是可信的,就可以防止恶意用户伪造 X-Forwarded-For Header,从而保证服务器获取到的客户端 IP 地址是准确的。

假设一个攻击者试图通过伪造 X-Forwarded-For Header 来假冒一个合法的客户端。他在发送请求中添加一个虚假的 X-Forwarded-For Header,如下所示:

X-Forwarded-For: forged-client

在这种情况下,proxy1 和 proxy2 依然分别将 client 和 proxy1 的 IP 地址添加到 X-Forwarded-For Header 中。最后 Envoy 收到的请求的 X-Forwarded-For Header 是这样的:

X-Forwarded-For: forged-client, client, proxy1

由于我们设置了 xffNumTrustedHops 为 2,Envoy 只会提取 X-Forwarded-For Header 中倒数的第二个 IP 地址,即 client 的 IP 地址,而不会受到 forged-client 的影响,从而保证了客户端 IP 地址的准确性。

通过设置信任网络节点个数来防止 XFF 头伪造攻击

通过 IP Detection Extension 从 X-Forwarded-For 中提取 IP 地址

除了在 HCM 中配置 X-Forwarded-For,我们还可以通过 IP Detection Extension 来提取客户端的真实 IP 地址,其配置和 HCM 类似,只是配置不是直接在 HCM 中,而是通过一个 IP Detection Extension 扩展组件来实现。

下面是一个通过 IP Detection Extension 配置 X-Forwarded-For 的示例:

"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
  "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
  // omitted for brevity
  // ...

  "originalIpDetectionExtensions": [
    {
      "name": "envoy.extensions.http.original_ip_detection.xff",
      "typedConfig": {
        "@type": "type.googleapis.com/envoy.extensions.http.original_ip_detection.xff.v3.XffConfig",
        "xffNumTrustedHops": 1
      }
    }
  ]
}  

备注:IP Detection Extension 似乎有一个 bug,其 xffNumTrustedHops 参数的取值需要比实际的 IP 地址数量少 1,即如果需要提取倒数第二个 IP 地址,需要将 xffNumTrustedHops 设置为 1。

自定义 HTTP Header

除了采用标准的 X-Forwarded-For Header,我们还可以通过自定义 HTTP Header 来传递客户端的真实 IP 地址。这种方法一般用于下述场景:

  • 遗留系统不支持 X-Forwarded-For Header,而是使用了自定义的 Header 来传递客户端的 IP 地址。
  • 出于安全原因,不希望将客户端的 IP 地址暴露在 X-Forwarded-For Header 中,而是采用加密的方式保存在自定义 Header 中。
  • 除了 IP 地址以为,还希望通过自定义 Header 传递一些额外的信息,例如客户端的地理位置、用户 ID 等。

如何在 Envoy 中配置自定义 HTTP Header?

假设我们在请求中添加了一个名为 X-Real-IP 的自定义 Header,用于传递客户端的真实 IP 地址。我们可以通过配置 Envoy 的 Custom Header IP Detection 插件来提取这个 Header 中的 IP 地址。

"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
  "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
  // omitted for brevity
  // ...

  "originalIpDetectionExtensions": [
    {
      "name": "envoy.extensions.http.original_ip_detection.custom_header",
      "typedConfig": {
        "@type": "type.googleapis.com/envoy.extensions.http.original_ip_detection.custom_header.v3.CustomHeaderConfig",
        "allowExtensionToSetAddressAsTrusted": true,
        "headerName": "X-Real-IP"
      }
    }
  ]
}  

代理协议

采用 HTTP Header 的方式可以很好地传递客户端的 IP 地址,但是这种方式有一个很大的局限性,它只能在 HTTP 协议中使用。如果我们的服务需要支持 HTTP 之外的其他协议,则可以考虑使用代理协议(Proxy Protocol)来传递客户端的 IP 地址。

什么是代理协议?

Proxy Protocol 是一个工作在传输层(TCP)上的协议,用于在代理服务器和后端服务器之间传递客户端的真实 IP 地址。

代理协议的工作原理是,在 TCP 连接开始时加入一个包含客户端 IP 地址的头。这个头在 TCP 握手之后立即插入,并在传输任何应用数据之前完成。因此,它对应用协议是透明的,可以与任何应用协议一起使用,包括 HTTP、HTTPS、SMTP 等。

支持 Proxy Protocol 的 TCP 握手过程

Proxy Protocol 有两个版本,分别是版本 1 和版本 2。版本 1 使用文本格式,易于人工阅读,而版本 2 使用二进制格式,更高效,但不易读。 在使用 Proxy Protocol 时,需要确保代理服务器和后端服务器都支持相同的版本。虽然格式不同,但这两个版本的工作原理是相同的。由于版本 1 的可读性更好,下面我们以版本 1 为例,来看一下 Proxy Protocol 的工作原理。

代理协议版本 1 的头是一行文本,以字符串 “PROXY” 开头,后跟几个用空格分隔的字段。格式如下:

PROXY <INET_PROTOCOL> <CLIENT_IP> <SERVER_IP> <CLIENT_PORT> <SERVER_PORT>\r\n

在 TCP 连接握手完成后,发送方会向接收方发送一个代理协议头。这个头包含多个字段,我们主要关注的是客户端的 IP 地址。然后,代理服务器在代理协议头之后立即转发客户端的数据。

以下是包含代理协议头的 HTTP 请求示例:

PROXY TCP4 162.231.246.188 192.168.0.11 56324 443\r\n
GET / HTTP/1.1\r\n
Host: www.example.com\r\n
\r\n

在这个示例中:

  • PROXY 表明这是 Proxy Protocol 的Header。
  • TCP4 表示使用的是 IPv4 和 TCP 协议。
  • 162.231.246.188 是原始客户端的 IP 地址。
  • 192.168.0.11 是服务端(代理服务器)的 IP 地址。
  • 56324 是客户端的端口号。
  • 443 是服务端(代理服务器)的端口号。

其中 Proxy Protocol Header 中的字段依次表示:协议类型(TCP4)、客户端 IP 地址()、服务器 IP 地址(192.168.0.11)、客户端端口号(56324)、服务器端口号(443)。

当接收方收到带有代理协议头的 TCP 连接时,它首先解析该头以提取客户端的 IP 地址和其他信息。然后,它从 TCP 数据中去除代理协议头,确保实际的 HTTP 请求可以正常处理。如果接收方也是支持代理协议的中间节点,它可以将客户端的 IP 地址转发到网络中的下一个跳,从而在请求的整个传输过程中保留客户端的 IP 地址。

需要注意的是,后端服务器能够识别 Proxy Protocol 主要依赖于预设的配置。如果服务器没有被适当配置,它可能无法理解 Proxy Protocol Header,可能会将其误解为错误的请求数据。

如何在 Envoy 中配置代理协议?

下面我们来看一下如何在 Envoy 中配置代理协议。 由于 Proxy Protocol 是在 TCP 连接的握手阶段添加的,因此我们需要在 Listener 的配置中启用 Proxy Protocol。

Listener 的配置中需要添加一个 envoy.filters.listener.proxy_protocol 的 Listener Filter,该 Filter 会从 TCP 连接建立后的第一个数据包中解析 Proxy Protocol Header,提取客户端的 IP 地址。然后将去掉 Proxy Protocol Header 的 TCP 数据包转发给 HCM(HTTP Connection Manager)进行处理。

"listener": {
  "@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
  "address": {
    "socketAddress": {
      "address": "0.0.0.0",
      "portValue": 10080
    }
  },
  // omitted for brevity
  // ...

  "listenerFilters": [
    { 
      "name": "envoy.filters.listener.proxy_protocol",
      "typedConfig": {
        "@type": "type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol"
      }
    }
  ],
}

配置太复杂?试试 Envoy Gateway!

采用上诉的三种方式,我们可以在 Envoy 中获取客户端的真实 IP 地址。这些方式都需要在 Envoy 长达数千行的配置文件中手动进行配置

Envoy 配置语法主要是为控制面使用而设计的,其首要目标提供配置的灵活性和可定制性,并没有太多用户友好性的考虑。Envoy 配置语法中包含了大量繁琐的配置项,这些配置项往往需要用户对 Envoy 的内部工作原理非常了解,才能正确配置。因此,对于普通用户来说,直接操作 Envoy 的配置文件可能会有一定的难度。

Envoy Gateway 的主要目标之一就是简化 Envoy 的部署和配置。 Envoy Gateway 采用 Kubernetes CRD 的方式基于 Envoy 之上提供更高级的抽象,屏蔽了用户不需要关心的细节,使用户可以更方便地配置 Envoy

使用 Envoy Gateway 管理 Envoy

ClientTrafficPolicyEnvoy Gateway 扩展的一个 Gateway API Policy CRD,用于配置连接到 Envoy Proxy 的客户端的网络流量策略。用户可以通过创建 ClientTrafficPolicy 来对 Envoy 进行配置,得到客户端的真实 IP 地址。

ClientTrafficPolicy 中,我们可以通过配置 clientIPDetection 来从 X-Forwarded-For Header 或自定义 Header 中提取客户端的 IP 地址。

从 X-Forwarded-For Header 中提取客户端的 IP 地址的 ClientTrafficPolicy 配置示例如下。该配置从 X-Fowarded-For Header 中提取倒数第二个 IP 地址作为客户端的真实 IP 地址。

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
  name: enable-client-ip-detection-xff
spec:
  clientIPDetection:
    xForwardedFor:
      numTrustedHops: 2
  targetRef:
    group: gateway.networking.k8s.io
    kind: Gateway
    name: my-gateway

如果我们采用自定义 Header 来传递客户端的 IP 地址,我们也可以通过配置 customHeader 来提取这个 Header 中的 IP 地址。 下面的 ClientTrafficPolicy 配置示例从 X-Real-IP Header 中提取客户端的 IP 地址。

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
  name: enable-client-ip-detection-custom-header
spec:
  clientIPDetection:
    customHeader:
      name: X-Real-IP
  targetRef:
    group: gateway.networking.k8s.io
    kind: Gateway
    name: my-gateway    

如果请求路径上的中间节点支持代理协议,我们也可以通过 ClientTrafficPolicyenableProxyProtocol 字段 来启用代理协议,从而获取客户端的 IP 地址。下面的 ClientTrafficPolicy 配置 Envoy 从代理协议中提取客户端的 IP 地址。

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
  name: enable-proxy-protocol
spec:
  enableProxyProtocol: true
  targetRef:
    group: gateway.networking.k8s.io
    kind: Gateway
    name: my-gateway

利用客户端地址进行访问控制和限流

通过 Envoy Gateway,用户可以更方便地实现客户端 IP 地址的获取,而无需了解 Envoy 的配置细节。在获取到客户端真实 IP 地址后,Envoy Gateway 还可以基于客户端的 IP 地址进行访问控制、限流等操作。

通过 Envoy GatewaySecurityPolicy,可以对客户端 IP 地址进行访问控制。下面的配置示例中,只允许来自 admin-region-useast 和 admin-region-uswest 两个 Region 的客户端 IP 地址访问 admin-route 这个 HTTPRoute,其余的请求都会被拒绝。

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: authorization-client-ip
  namespace: gateway-conformance-infra
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: admin-route
  authorization:
    defaultAction: Deny
    rules:
    - name: admin-region-useast
      action: Allow
      principal:
        clientCIDRs:
        - 10.0.1.0/24
        - 10.0.2.0/24
    - name: admin-region-uswest
      action: Allow
      principal:
        clientCIDRs:
        - 10.0.11.0/24
        - 10.0.12.0/24    

通过 Envoy GatewayBackendTrafficPolicy,可以对客户端 IP 地址进行限流。下面的配置示例中,对于来自 192.168.0.0/16 的客户端 IP 地址进行限流,每个 IP 地址每秒最多只能发出 20 个请求,超过这个限制的请求会被拒绝。

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy 
metadata:
  name: policy-httproute
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: myapp-route
    namespace: default
  rateLimit:
    type: Global
    global:
      rules:
      - clientSelectors:
        - sourceCIDR:
          type: "Distinct"
          value: 192.168.0.0/16
        limit:
          requests: 20
          unit: Second

结语

一个客户端请求在到达服务器前,通常会经过多个网络节点,如代理服务器、负载均衡器等,这些节点可能会更改请求的来源 IP 地址,导致服务器无法准确识别客户端的真实位置。

为了解决这个问题,Envoy 提供了多种方法来获取客户端的真实 IP 地址,包括使用标准的 X-Forwarded-For Header、自定义 HTTP Header 和代理协议。每种方法都有其优缺点,Envoy 支持所有这些方法,允许用户根据具体的使用场景选择最合适的解决方案。

Envoy 的配置语法相对复杂,对于普通用户来说可能会有一定的难度。通过采用 Envoy Gateway 对 Envoy 进行管理,可以显著简化从请求中获取客户端 IP 地址的过程。此外, Envoy Gateway 还支持高级用例,例如访问控制、速率限制和其他基于客户端 IP 的流量管理。

👇👇👇 您在网关配置中使用客户端 IP 吗?我很想听听您的使用案例!请在下方留言,分享您的见解。👇👇👇