Envoy:经过envoy代理后获取客户端真实IP

时间:2024-03-03 21:28:36

在envoy作为前端代理时,用户ip的获取很重要,一般获取ip的方式。都是通过Header中的 X-Forward-ForX-Real-IPRemote 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:内部代理

无特殊需求可无需配置