获取客户端真实信息

更新时间:
复制为 MD 格式

当服务资源为 NLB 时,支持通过 ProxyProtocol v2 协议向后端服务器传递服务使用方的真实来源信息,包括客户端真实 IP 和阿里云扩展信息(终端节点 ID、VPC ID和终端节点服务 ID),可用于追溯请求来源。

工作原理

获取客户端真实 IP

ProxyProtocol v2 用于在 NLB 与后端服务器之间透传客户端连接信息。配置 NLB 监听开启 ProxyProtocol 功能后,NLB 在转发客户端请求时,会在 TCP 三次握手成功后,将客户端的真实源信息(如 IP 地址、端口等)附加到发送给后端服务器的第一个 TCP 数据包的 Payload 开头。

Proxy Protocol v2报文参考

携带客户端IPv4地址的Proxy Protocol v2二进制头格式

携带客户端IPv6地址的Proxy Protocol v2二进制头格式

IPv4

IPv6

获取阿里云扩展信息

  • NLB 支持通过 ProxyProtocol v2 的 TLV(Type-Length-Value)扩展机制传递 PrivateLink 特有的标识信息。

    • 根据 ProxyProtocol v2 协议规范,Type 值 0xE0 ~ 0xEF 为用户自定义扩展的保留范围。阿里云使用0xE1 作为自定义 Type 标识。

    • 阿里云扩展信息格式:

      • Type:0xE1,标识阿里云自定义扩展。

      • Length:后续数据的总长度(包含 SubType + Custom Data)。

      • SubType:扩展字段类型。

        • 0x01:VPC ID。

        • 0x02:终端节点 ID。

        • 0x03:终端节点服务 ID。

      • Custom Data:扩展信息的具体 ID。

       * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
       *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       *|  Type = 0xE1  |            Length             | SubType       |
       *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       *|                                                               |
       *|                                                               |
       *|                        Custom Data                            |
       *|                                                               |
       *|                                                               |
       *+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

适用范围

  • 仅 NLB 类型的服务资源支持 ProxyProtocol v2 功能,ALB 和 CLB 不支持。

  • Proxy Protocol 需要后端服务器支持该协议才能正常使用。如果后端服务器不具备解析Proxy Protocol协议能力,直接开启 ProxyProtocol 功能,很可能会导致后端服务解析异常,从而影响服务可用性。

  • NLB 监听支持通过 Proxy Protocol 携带原始连接信息(源IP、目的IP、源端口、目的端口等)并添加到 TCP 或 UDP 数据头中,且不会丢弃或覆盖任何原有数据。

  • NLB 仅支持Proxy Protocol v2版本。Proxy Protocol v2版本支持多种传输协议,如TCPUDP,更多信息,请参见The PROXY protocol

获取客户端真实 IP

本文仅介绍服务提供方如何获取客户端真实 IP,服务私网访问的完整流程可参考共享用户自建服务
  1. 配置 NLB 监听,开启 ProxyProtocol:前往NLB 控制台 - 实例页面,单击目标 NLB 实例 ID。在实例属性区域,确保配置修改保护未开启。

    • 未配置监听:选择监听页签,单击创建监听,修改高级配置,启用开启ProxyProtocol开关。

    • 已配置监听:单击监听 ID,编辑监听,修改高级配置,启用开启ProxyProtocol开关。

  2. 配置后端应用解析协议:

    此处以CentOS 7.9操作系统、Nginx 1.20.1 版本配置为例。具体请以您实际使用的环境为准。

    1. 登录后端服务器,执行nginx -t命令查看配置文件所在路径。默认通常为 /etc/nginx/nginx.conf

    2. 修改 Nginx 配置文件,在 listen 指令中添加 proxy_protocol 参数。

      http {
          # 配置日志格式,使用$proxy_protocol_addr变量记录客户端真实IP
          log_format  main  '$proxy_protocol_addr - $remote_addr - $remote_user [$time_local] "$request" '
                            '$status $body_bytes_sent "$http_referer" '
                            '"$http_user_agent" "$http_x_forwarded_for"';
      
          server {
              # 在监听端口上添加proxy_protocol参数
              listen       80 proxy_protocol;
              #...
          }
      }
    3. 执行sudo nginx -s reload命令,重新加载Nginx配置文件。

  3. 验证后端服务器可获取客户端真实IP:

    1. 服务使用方登录终端节点所属 VPC 的 ECS 实例,执行curl http://<终端节点域名>

    2. 服务提供方登录后端服务器执行tail -f /var/log/nginx/access.log 查看 Nginx 访问日志:

      Nginx日志文件默认路径为:/var/log/nginx/access.log
      每行日志中,$proxy_protocol_addr变量对应的IP地址即为客户端真实IP地址。

      image

获取阿里云扩展信息

本文仅介绍服务提供方如何获取阿里云扩展信息,服务私网访问的完整流程可参考共享用户自建服务
  1. 配置 NLB 监听,开启 ProxyProtocol:前往NLB 控制台 - 实例页面,单击目标 NLB 实例 ID。在实例属性区域,确保配置修改保护未开启。

    • 未配置监听:选择监听页签,单击创建监听,修改高级配置,启用开启ProxyProtocol开关。

    • 已配置监听:单击监听 ID,编辑监听,修改高级配置,启用开启ProxyProtocol开关。

  2. 调用UpdateListenerAttribute,配置ProxyProtocolEnabledPpv2VpcIdEnabledPpv2PrivateLinkEpIdEnabledPpv2PrivateLinkEpsIdEnabled均为true,通过 Proxy Protocol 协议携带 PrivateLink 相关信息到后端服务器。

  3. 配置后端应用解析扩展字段:

    标准应用无法直接解析阿里云扩展字段。本文以scapy抓包库为例,解析并输出阿里云扩展信息。

    1. 服务提供方登录后端服务器,执行pip3 install scapy安装 scapy。

    2. 保存以下脚本为parse_ppv2_extensions.py

      #!/usr/bin/env python3
      from scapy.all import sniff
      import argparse
      import sys
      
      # 禁用输出缓冲
      sys.stdout = open(sys.stdout.fileno(), mode='w', buffering=1)
      sys.stderr = open(sys.stderr.fileno(), mode='w', buffering=1)
      
      ppv2_signature = [13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10]  # "\r\n\r\n\0\r\nQUIT\n"
      ep_id = "null"
      vpc_id = "null"
      eps_id = "null"
      client_ip = ""
      client_port = 0
      endpoint_ip = ""
      endpoint_id = ""
      parser = argparse.ArgumentParser(description="input parameter")
      parser.add_argument('--dstport', default='destination port', required=True)
      args = parser.parse_args()
      rs_port = args.dstport
      ppv2_cnt = 0
      
      def convert_list_to_string(l):
          tmp = ""
          for c in l:
              tmp += chr(c)
          return tmp
      
      def convert_list_to_ip(l):
          return ".".join(map(str, l))
      
      
      def pack_callback(packet):
          global endpoint_id, client_port, client_ip, vpc_id, ppv2_cnt, endpoint_ip, eps_id
          if ('Raw' in packet):
              byte_list = list(packet['Raw'].load)
              if (byte_list[0:12] == ppv2_signature):
                  ppv2_cnt += 1
                  client_ip = convert_list_to_ip(byte_list[16:20])
                  endpoint_ip = convert_list_to_ip(byte_list[20:24])
                  client_port = byte_list[24] * 256 + byte_list[25]
                  tlv = byte_list[28:]
                  i = 0
                  while i < len(tlv):
                      tlv_type = tlv[i]
                      tlv_length = tlv[i + 1] * 256 + tlv[i + 2]
                      tlv_value_first_byte = tlv[i + 3]
                      tlv_value_true_value = tlv[i + 4: i + 4 + tlv_length - 1]
                      if tlv_type == 225:  # 0xE1
                          if tlv_value_first_byte == 1:   # vpc_id
                              vpc_id = convert_list_to_string(tlv_value_true_value)
                          if tlv_value_first_byte == 2:   # endpoint_id
                              endpoint_id = convert_list_to_string(tlv_value_true_value)
                          if tlv_value_first_byte == 3:   # eps_id
                              eps_id = convert_list_to_string(tlv_value_true_value)
      
                      i += 1 + 2 + tlv_length
                  print("receive total %d ppv2 packet, ClientIp: %s, ClientPort: %d, EndpointIp: %s, EndpointId: %s, VpcId: %s, EndpointServiceId: %s" % ( ppv2_cnt, client_ip, client_port, endpoint_ip, endpoint_id, vpc_id, eps_id))
                  sys.stdout.flush()  # 强制刷新输出
      
      
      filterstr = "ip and tcp and dst port " + rs_port
      print("start to capture ppv2 packet on port " + rs_port)
      sys.stdout.flush()
      sniff(iface="eth0", filter=filterstr, prn=pack_callback)
      
    3. 执行sudo python3 parse_ppv2_extensions.py --dstport < NLB服务器组的后端服务端口>

    4. 服务使用方登录终端节点所属 VPC 的 ECS 实例,执行curl http://<终端节点域名>访问服务后,可在后端服务器查看到阿里云扩展信息。

      image