前言
河水出昆仑,东流经玉门,环绝壁,历五山,南至积石,东流入海。其流也,或曲或直,时急时缓,遇山则环,逢谷则奔。渐行渐远,百折千回,至于中原,汇百川,泽九州。其道也,蜿蜒盘旋,绵延万里,波澜壮阔,历千古而不息。——《水经注》
河水从源头出发,经过千折百回,才能流入大海。在网络世界中,一个 HTTP 请求从客户端发出,也要经过多个网络节点,最终才能到达服务器。在这个过程中,由于客户端的 IP 地址在经过代理服务器、负载均衡器等中间节点时会丢失,导致服务器无法获取到客户端的真实 IP 地址。
而在对网络请求进行处理时,服务器经常需要获取客户端的真实 IP 地址,以用于访问控制、日志记录、地理位置识别等操作。
为了解决这个问题,Envoy 提供了多种方法来获取客户端的真实 IP 地址,包括使用 X-Forwarded-For Header、自定义 HTTP Header 和代理协议。本文将介绍这些方法,并说明如何在 Envoy 中配置它们。
X-Forwarded-For HTTP Header
什么是 X-Forwarded-For?
X-Forwarded-For 是一个 HTTP 请求Header,常用于代理和负载均衡器环境中,以标识发出请求的客户端的原始 IP 地址。当一个请求经过代理或负载均衡器时,该节点可以在 HTTP 请求中添加或更新 X-Forwarded-For Header,通过这种方式,原始客户端的 IP 可以被保留下来。
这个 Header 可以包含单个 IP 地址(最初的客户端),也可以包含一个 IP 地址链,反映了请求路径中的每一个代理。格式通常是一个逗号分隔的 IP 地址列表,如下所示:
代码语言:javascript复制X-Forwarded-For: client, proxy1, proxy2, ...
这里,client 是原始发起请求的客户端的 IP,proxy1 和 proxy2 是该请求经过的第一个和第二个代理服务器的 IP 地址。请求途径的每个代理会将和自己直接通信的上一个节点的 IP 地址添加到 X-Forwarded-For Header 中,这样服务器就可以通过解析这个 Header 来获取客户端的真实 IP 地址。
假设客户端发出的 HTTP 请求需要经过两个代理服务器后才能到达服务器端,请求的路径如下:
代码语言:javascript复制client -- 1 --> proxy1 -- 2 --> proxy2 -- 3 --> sever
如上图所示,在这个过程中,该 HTTP 请求经过了三段 TCP 连接。其中每一段 TCP 连接的源地址和目的地址,以及相应的 X-Forwarded-For Header的内容如下:
1. 请求从 client 发送到 proxy1: TCP 连接: source IP (client) -> destination IP (proxy1) X-Forwarded-For: 无。因为在这一段连接中,client 就是是请求最初的源头,所以不会有 X-Forwarded-For Header。
2. 请求从 proxy1 发送到 proxy2: TCP 连接: source IP (proxy1) -> destination IP (proxy2) X-Forwarded-For: client。在这一段连接中,proxy1 作为代理为 client 转发请求,所以它会在向外发出的 HTTP 请求中增加 X-Forwarded-For Header,并将 TCP 连接的对端 IP 地址,即 client 的 IP 地址设置到该 Header 中,以便后续节点可以获取到 client 的真实 IP 地址。
3. 请求从 proxy2 发送到 Server: TCP 连接: source IP (proxy2) -> destination IP (server) X-Forwarded-For: client, proxy1。在这一段连接中,proxy2 作为代理为 proxy1 转发请求,它会保留原有的 X-Forwarded-For Header,并在其基础上添加和 TCP 连接的对端 IP 地址,即 proxy1 的 IP 地址添加到 X-Forwarded-For Header 中。
最终,服务器端收到的请求的 X-Forwarded-For Header 如下:
代码语言:javascript复制X-Forwarded-For: client, proxy1
注意这里 proxy2 的地址并不会出现在 X-Forwarded-For Header 中。在请求的处理过程中,proxy2 最后一个转发请求的节点,Server 端可以直接通过 TCP 连接的源地址获取到 proxy2 的 IP 地址,因此并不需要在 X-Forwarded-For Header 中包含 proxy2 的地址。
采用 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的提取,可以通过设置 useRemoteAddress
和 xffNumTrustedHops
两个参数来实现。useRemoteAddress
参数用于指示 Envoy 是否从 X-Forwarded-For Header 中提取远程地址作为请求的源地址,而 xffNumTrustedHops
参数用于指定 Envoy 信任的 X-Forwarded-For Header中的 IP 地址数量。
我们需要将 useRemoteAddress
设置为 true
,并根据实际情况设置 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 配置示例:
代码语言:javascript复制"name": "envoy.filters.network.http_connection_manager",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager",
// omitted for brevity
// ...
"useRemoteAddress": true,
"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,如下所示:
代码语言:javascript复制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 地址的准确性。
通过 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 的示例:
代码语言:javascript复制"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 地址。如果采用了自定义的 Header,我们可以采用配置 Envoy 的 Custom Header IP Detection 插件来获取客户端的 IP 地址。
假设我们在请求中添加了一个名为 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 地址。由于 Proxy Protocol 是在 TCP 连接的建立阶段添加的,因此它对应用协议是透明的,可以在任何应用协议上使用,包括 HTTP、HTTPS、SMTP 等。
Proxy Protocol 有两个版本,分别是版本 1 和版本 2。版本 1 使用文本格式,易于人工阅读,而版本 2 使用二进制格式,更高效,但不易读。 在使用 Proxy Protocol 时,需要确保代理服务器和后端服务器都支持相同的版本。虽然格式不同,但这两个版本的工作原理是相同的。下面我们以版本 1 为例,来看一下 Proxy Protocol 的工作原理。
发送端:在 TCP 连接的握手阶段结束后,代理服务器向后端服务器发送一个包含客户端的 IP 地址和端口号的 Proxy Protocol Header,紧接着 Proxy Protocol Header 后,代理服务器会转发客户端的数据。
下面是一个包含 Proxy Protocol Header 的 HTTP 请求示例:
代码语言:javascript复制PROXY TCP4 162.231.246.188 192.168.0.11 56324 443rn
GET / HTTP/1.1rn
Host: www.example.comrn
rn
在这个示例中:
- PROXY 表明这是 Proxy Protocol 的Header。
- TCP4 表示使用的是 IPv4 和 TCP 协议。
- 162.231.246.188 是原始客户端的 IP 地址。
- 10.0.0.1 是服务端(代理服务器)的 IP 地址。
- 12345 是客户端的端口号。
- 443 是服务端(代理服务器)的端口号。
其中 Proxy Protocol Header 中的字段依次表示:协议类型(TCP4)、客户端 IP 地址()、服务器 IP 地址(192.168.0.11)、客户端端口号(56324)、服务器端口号(443)。
接收端:后端服务器在接收到代理服务器转发的请求时,会首先解析 Proxy Protocol Header,提取客户端的 IP 地址和端口号。这些信息可以用于进行访问控制、日志记录等操作。当 Proxy Protocol Header 被从 TCP 数据中剥离出来后,HTTP 请求就可以被正常处理了。
需要注意的是,后端服务器能够识别 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 Gateway 的主要目标之一就是简化 Envoy 的配置,提供更高级的抽象,使用户可以更方便地配置 Envoy。Envoy Gateway 提供了一个 ClientTrafficPolicy CRD,该 CRD 屏蔽了底层 Envoy 的配置细节,用户可以通过创建 ClientTrafficPolicy 资源来得到客户端的真实 IP 地址。
在 ClientTrafficPolicy 中,我们可以通过配置 clientIPDetection
来从 X-Forwarded-For Header 或自定义 Header 中提取客户端的 IP 地址。
从 X-Forwarded-For Header 中提取客户端的 IP 地址的 ClientTrafficPolicy 配置示例:
代码语言:javascript复制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
从自定义 X-Real-IP Header 中提取客户端的 IP 地址的 ClientTrafficPolicy 配置示例:
代码语言:javascript复制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
我们也可以通过配置 enableProxyProtocol
来启用代理协议,从而获取客户端的 IP 地址。
启用代理协议的 ClientTrafficPolicy 配置示例:
代码语言:javascript复制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 Gateway 的 SecurityPolicy,可以对客户端 IP 地址进行访问控制。下面的配置示例中,只允许来自 admin-region-useast 和 admin-region-uswest 两个 Region 的客户端 IP 地址访问 admin-route 这个 HTTPRoute,其余的请求都会被拒绝。
代码语言:javascript复制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 Gateway 的 BackendTrafficPolicy,可以对客户端 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 Gateway 对 Envoy 进行管理,用户可以很方便地获取到客户端的真实 IP 地址,并可以基于客户端 IP 地址进行对请求进行访问控制、限流等操作,提高了应用的安全性和可用性。