在envoy作為前端代理時,用戶ip的獲取很重要,一般獲取ip的方式。都是通過Header中的 X-Forward-For、 X-Real-IP或 Remote addr 等屬性獲取,但是如果確保Envoy可以獲取到的ip是真實的用戶ip呢?本篇繼續解密!
概念說明
-
Remote Address
是nginx與客戶端進行TCP連接過程中,獲得的客戶端真實地址。Remote Address 無法偽造,因為建立 TCP 連接需要三次握手,如果偽造了源 IP,無法建立 TCP 連接,更不會有后面的 HTTP 請求。
一般情況下,在Envoy作為最外層代理時,此IP為真實的IP客戶端IP -
X-Real-IP
是一個自定義頭。X-Real-Ip 通常被 HTTP 代理用來表示與它產生 TCP 連接的設備 IP,這個設備可能是其他代理,也可能是真正的請求端。X-Real-Ip 目前並不屬於任何標准,代理和 Web 應用之間可以約定用任何自定義頭來傳遞這個信息。 -
X-Forwarded-For
X-Forwarded-For 是一個擴展頭。HTTP/1.1(RFC 2616)協議並沒有對它的定義,它最開始是由 Squid 這個緩存代理軟件引入,用來表示 HTTP 請求端真實 IP,現在已經成為事實上的標准,被各大 HTTP 代理、負載均衡等轉發服務廣泛使用,並被寫入 RFC 7239(Forwarded HTTP Extension)標准之中。通常,X-Forwarded-For可被偽造,並且使用CDN會被重寫
Envoy中如何獲取真實IP
在Envoy中,涉及到客戶端IP的配置如下:
use_remote_address: 默認值false,設置為true,使用客戶端連接的真實遠程地址,false是使用x-forwarded-for
skip_xff_append: 設置為true,則不會將遠程地址附加到x-forwarded-for中
request_headers_to_add 添加請求頭
request_headers_to_remove 刪除一個請求頭
實驗環境配置准備
admin:
access_log_path: /dev/null
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_80
address:
socket_address: { address: 0.0.0.0, port_value: 80 }
access_log:
filter_chains:
- filters:
- name: envoy_http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
access_log:
- name: envoy.listener.accesslog
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /var/log/envoy.log
log_format:
text_format: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\"\n"
http_filters:
- name: envoy.filters.http.router
use_remote_address: true
skip_xff_append: false
xff_num_trusted_hops: 0
stat_prefix: local_route
codec_type: AUTO
route_config:
name: local_route
#request_headers_to_remove: "X-Forwarded-For"
request_headers_to_add:
header:
key: "X-Forwarded-For"
value: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"
#value: "%REQ(REMOTE_ADDR)%"
append: true
virtual_hosts:
- name: split_traffic
domains: [ "*" ]
routes:
- match:
prefix: "/"
route:
cluster: version_v1
request_mirror_policies:
cluster: version_v2
runtime_fraction:
default_value:
numerator: 10
denominator: HUNDRED
runtime_key: routing.request_mirror.version
clusters:
- name: version_v1
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: version_v1
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: version1, port_value: 90 }
health_checks:
timeout: 3s
interval: 30s
unhealthy_threshold: 2
healthy_threshold: 2
http_health_check:
path: /ping
expected_statuses: { start: 200, end: 201 }
- name: version_v2
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: version_v2
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: version2, port_value: 90 }
health_checks:
timeout: 3s
interval: 30s
unhealthy_threshold: 2
healthy_threshold: 2
http_health_check:
path: /ping
expected_statuses: { start: 200, end: 201 }
docker-compose
version: '3'
services:
envoy:
image: envoyproxy/envoy-alpine:v1.15-latest
environment:
- ENVOY_UID=0
ports:
- 80:80
- 443:443
- 82:9901
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
networks:
envoymesh:
aliases:
- envoy
depends_on:
- webserver1
- webserver2
- webserver3
- webserver4
webserver1:
image: sealloong/envoy-end:latest
networks:
envoymesh:
aliases:
- version1
environment:
- VERSION=v1
- COLORFUL=blue
expose:
- 90
webserver2:
image: sealloong/envoy-end:latest
networks:
envoymesh:
aliases:
- version1
environment:
- VERSION=v1
- COLORFUL=blue
expose:
- 90
webserver3:
image: sealloong/envoy-end:latest
networks:
envoymesh:
aliases:
- version2
environment:
- VERSION=v2
- COLORFUL=red
expose:
- 90
webserver4:
image: sealloong/envoy-end:latest
environment:
- VERSION=v2
- COLORFUL=red
networks:
envoymesh:
aliases:
- version2
expose:
- 90
networks:
envoymesh: {}
實際使用Envoy作為代理時的外在環境
環境1:客戶端直接和Envoy通信
當一個正常請求時,此處可以正常獲得客戶端IP,實際上envoy拿的值是 X-Forwarded-For

后端日志

在偽造或者重寫X-Forwarded-For后實際上是獲取的偽造的值。



在Envoy直接作為外層代理時,可以使用如下參數,在不管如何偽造,都可以拿到對應的參數。
name: local_route
request_headers_to_remove: "X-Forwarded-For" # 怕X-Forwarded-For為偽造值,可以刪除此值,
request_headers_to_add: # 刪除后還需要向后端傳遞,故還需要添加上此值
header:
key: "X-Forwarded-For"
value: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" # 獲取 remote_addr,此值無法偽造,為Envoy變量,表示 下游主機真實IP不加端口,即remote_addr 無端口
append: true # 表面值是追加還是重寫



可以看到envoy獲取的為真實的ip並非偽造的請求
環境2:Envoy前段存在代理(無CDN)
此環境下,前端存在代理,如f5、nginx等。這種情況下不能使用remote_addr 這樣獲取的為前端代理的IP並非真實IP
前端存在f5或nginx,可以在f5中配置irule傳遞真實的remote_addr,替換為真實的客戶端IP,又前端代理重寫配置,可自定義值。
request_headers_to_remove: "X-Forwarded-For"
request_headers_to_add:
header:
key: "X-Forwarded-For"
value: "%REQ(custom_header)%"
環境3:Envoy前段存在代理(單CDN)
此環境下,前端存在代理,並且使用了CDN,應為每個CDN廠商獲取客戶真實IP的方式並不一致,這里需要找到cdn廠商找到獲取真實IP的方法,在按照步驟2進行。
舉例:
阿里雲cdn獲取真實IP方法
加速樂獲取真實IP方法
環境4:Envoy前段存在代理(多CDN)
由於各CDN的帶寬、價格、使用場景等因素,在實際情況下,可能使用多種CDN;如:正常情況下使用cdn加速,遇到攻擊時切換安全防御高的CDN。一般僅加速的CDN價格比帶防御的要便宜很多。
此處Enovy待更新,后端應用可根據CDN的http頭正常獲取IP
環境5:內部代理
無特殊需求可無需配置
