探讨4层代理和7层代理行为以及如何获取真实客户端IP

avatar
作者
猴君
阅读量:0

准备工作

实验环境

IP角色
192.168.1.100客户端请求IP
192.168.1.100python 启动的HTTP服务
192.168.1.102nginx服务
192.168.1.103haproxy 服务

HTTP服务

这是一个简单的HTTP服务,主要打印HTTP报文用于分析客户端IP

#!/usr/bin/env python # coding: utf-8  import socket from threading import Thread  # 创建socket对象 sock_srv = socket.socket()  # 绑定IP和port sock_srv.bind(('0.0.0.0', 5001))  # 开启服务 sock_srv.listen()  # 定义一个函数, 处理来自客户端链接的处理 def socket_deal(conn: socket.socket, address: tuple): 	# 通过socket获取客户端的IP; 这里的客户端IP其实指的是TCP报文中的原始IP和原始Port 	# 就是上一个发起TCP发起的地址      print(address)      # 打印HTTP的报文     print(conn.recv(1024).decode())      # 不做特殊处理,所有的请求均返回Hello Word     template = """ HTTP/1.1 200 OK Service: HTTP Version: 1.1.2.2  <h1>hello word</h1>     """     conn.send(template.encode())      # 关闭此次HTTP的请求     conn.close()   while True:     # 接受Client的数据请求     conn, address = sock_srv.accept()     Thread(target=socket_deal, args=(conn, address)).start()  

Nginx报文分析

nginx 4层代理

  • 配置启动4层代理, 并请求 http://192.168.1.102 并观察 101 的请求信息
stream { 	server { 	  listen 80 ; 	  proxy_pass 192.168.1.100:5001;  		# Python的HTTP服务 	  # proxy_protocol on;					# 可选性, 4层携带真实IP 	} } 
  • 客户端请求4层转发,默认不传递客户端IP。
# print(address), 可以从socket得到客户端IP ('192.168.1.102', 52842)  # 得到HTTP的报文信息如下 GET / HTTP/1.1 Host: 192.168.1.102 Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 
  • 客户端请求4层转发,要求传递客户端IP。
# print(address), 可以从socket得到客户端IP ('192.168.1.102', 52848)  # 得到HTTP的报文信息如下,多了一行PROXY。其余信息不变 PROXY TCP4 192.168.1.100 192.168.1.102 55360 80 GET / HTTP/1.1 Host: 192.168.1.102 Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 

nginx 7层代理

  • 配置文件
server {                                                                                listen       80;                                                                    server_name  localhost;                                                                                                                                                                                                                                     location / {                                                                            proxy_pass http://192.168.1.100:8000/;                                                                                  # proxy_set_header X-Forwarded-For $remote_addr;   # nginx 去掉注释请求携带客户端真实IP                                     }                                                                                                                                                                   } 
  • 客户端请求7层代理,默认不传递客户端IP真实IP。
# print(address), 可以从socket得到客户端IP ('192.168.1.102', 52854)  # 得到HTTP的报文信息如下. GET / HTTP/1.0 Host: 192.168.1.100:5001 Connection: close Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 
  • 客户端请求7层,要求传递客户端IP真实IP。
# print(address), 可以从socket得到客户端IP ('192.168.1.102', 52858)  # 得到HTTP的报文信息如下,多了一行PROXY。其余信息不变 GET / HTTP/1.0 Host: 192.168.1.100:5001 Connection: close Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 X-Forwarded-For: 192.168.1.100 

分析NGINX获取客户端IP方法

方式修改header信息是否修改原始报文获取客户端IP方式
4层转发不带客户端IP不修改不修改服务端可以获取上一层发起请求的socket 源IP, 但不是客户端真实IP
4层转发携带客户端IP不修改原始报文前增加PROXY格式内容通过PROXY内容可以获取客户端IP
7层转发不带客户端IP不修改nginx重新封装HTTP报文服务端 socket 获取
7层转发携带客户端IPnginx 通过增加header信息传递客户端IPnginx重新封装HTTP报文通过nginx封装的报文获取XFF
  1. 7层代理会对报文进行重新封装,封装过程中可以通过增加XFF的header传递客户端IP。

  2. 4层转发不会修改报文。在不修改HTTP报文前提下,前置补充代理信息, 格式: PROXY TCP 客户端IP 代理端IP 客户端端口 代理端端口

Haproxy 代理分析

Haproxy 4层代理

  • 配置
defaults     mode                    tcp  frontend  main *:80     default_backend             app  backend app     balance     roundrobin     server  app1 192.168.1.100:5001 check # 不携带真实IP     # server  app1 192.168.1.100:5001 send-proxy check  # 携带真实IP 
  • 客户端请求4层转发,默认不要求传递客户端IP
# print(address) 获取客户端的address信息 ('192.168.1.103', 56790)  # 这个信息和NGINX 4层信息一样 GET / HTTP/1.1 Host: 192.168.1.103 Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 
  • 客户端请求4层转发,要求传递客户端IP
# print(address) 获取客户端的address信息 ('192.168.1.103', 58410)  # 与Nginx 4层带真实IP一样, 报文之前增加了PROXY信息 PROXY TCP4 192.168.1.100 192.168.1.103 57871 80 GET / HTTP/1.1 Host: 192.168.1.103 Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9  

Haproxy 7层代理

  • 配置
defaults     mode                    http     option forwardfor       except 127.0.0.0/8   # 默认携带客户端IP,  frontend  main *:80     default_backend             app  backend app     balance     roundrobin     server  app1 192.168.1.100:5001 check  
  • 客户端请求7层代理,默认传递客户端IP
# print(address) 获取客户端的address信息 ('192.168.1.103', 53178)  # 与Nginx 7层带客户端IP一样, 报文包含X-Forwarded-For GET /favicon.ico HTTP/1.1 Host: 192.168.1.103 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 Referer: http://192.168.1.103/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 X-Forwarded-For: 192.168.1.100 Connection: close 
  • 客户端请求7层代理,注释不传递客户端IP
# print(address) 获取客户端的address信息 ('192.168.1.103', 53576)  # 与Nginx 7层不传递客户端IP一样, 报文包含没有X-Forwarded-For GET / HTTP/1.1 Host: 192.168.1.103 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close 

分析Haproxy获取客户端IP方法

其实和nginx一样

方式修改header信息是否修改原始报文获取客户端IP方式
4层转发不带客户端IP不修改不修改服务端可以获取上一层发起请求的socket 源IP, 但不是客户端真实IP
4层转发携带客户端IP不修改原始报文前增加PROXY格式内容通过PROXY内容可以获取客户端IP
7层转发不带客户端IP不修改nginx重新封装HTTP报文服务端 socket 获取
7层转发携带客户端IPnginx 通过增加header信息传递客户端IPnginx重新封装HTTP报文通过nginx封装的报文获取XFF

案例小结

上述操作主要是完成: Nginx和Haproxy两款服务分别完成: 4层转发和7层代理。 携带IP与不携带客户端IP配置上的区别和报文展示

转发方式传递方式特点
7层代理增加HTTP报文的header信息X-Forwarded-For, 进行传递客户端IP在原始报文进行修改
4层代理在HTTP报文前方附加一层PROXY信息, 进行传递客户端IP不修改原始报文,在HTTP报文前方附加数据

提前了解一下: NGINX中参数设定protocol。开启后可以处理 “在HTTP报文前方附加数据” 混淆的HTTP报文结构

在这里插入图片描述

附伪代码获取客户端IP

#!/usr/bin/env python # coding: utf-8 import socket from threading import Thread  # 创建socket对象 sock_srv = socket.socket()  # 绑定IP和port sock_srv.bind(('0.0.0.0', 5001))  # 开启服务 sock_srv.listen()   # 简单的Response结构 def response(content, status, msg, conn):     template = """ HTTP/1.1 %d %s Service: HTTP Version: 1.1.2.2 Content-Type: text/html; charset=UTF-8 Connection: close  %s       """ % (status, msg, content)     return conn.send(template.encode())   # 定义一个函数, 处理来自客户端链接的处理 def socket_deal(conn: socket.socket, address: tuple):     try:         # 通过socket获取客户端的IP; 这里的客户端IP其实指的是TCP报文中的原始IP和原始Port         # 就是上一个发起TCP发起的地址         _client_ip, _client_port = address         data = conn.recv(1024).decode()          # 打印HTTP的报文         _data_lines = data.splitlines()          # 代理模式         _proxy_type = "可能HTTP代理"          # header 信息收集         extend_data = ["header信息", ]         for line in _data_lines:              # 如果4层传递客户端IP,会得到如下信息。             if line.startswith('PROXY'):                 # PROXY TCP4 192.168.1.100 192.168.1.102 55360 80                 _, protocol, _c_ip, _p_ip, _c_port, _p_port = line.split()                 if protocol != 'TCP4':                     response("PROXY ERROR", 500, "PROXY_ERROR", conn)                 else:                     _proxy_type = "TCP代理"                     _client_ip = _c_ip                     # 有时候报文只收到PROXY信息, 就需要第二次接收报文信息                     if len(_data_lines) == 1:                         socket_deal(conn, (_c_ip, _c_port))                         return                     # 如果一次性收完报文信息则继续处理                     else:                         continue              if ":" not in line:                 # 不是K:V 形式,那不是header。 可能是post数据, 也可能是HTTP协议。 此处忽略                 continue              # 拿到header信息             header_key, header_value = [item.strip() for item in line.split(":", 1)]             if header_key == 'X-Forwarded-For':                 _client_ip = header_value              # header 信息入库             extend_data.append(":".join((header_key, header_value)))          content = ["真实的客户端IP可能是" + _client_ip, "<br />"]         content.extend(extend_data)         response("<br />".join(content), 200, 'ok', conn)              except Exception as e:         print(e)     finally:         # 关闭此次HTTP的请求         conn.close()   while True:     # 接受Client的数据请求     conn, address = sock_srv.accept()     Thread(target=socket_deal, args=(conn, address)).start()  

实践: NGINX与HAProxy结合使用。

准备环境

IP角色标记说明
192.168.1.100客户端请求IPclient浏览器请求4层转发到后端HTTP服务
192.168.1.102NGINX实现提供4层转发NGINX-PROXY转发到104服务器
192.168.1.103Haproxy提供4层转发HAProxy4层转发到102的7层
192.168.1.104NGINX 7层服务NGINX-HTTP代表HTTP服务器。直接返回,并观察日志
  1. 启动一台NGINX HTTP服务器,扮演正常访问的网站。 代号NGINX-HTTP
  2. 另启动一台NGINX服务器,实现四层转发功能到NGINX HTTP服务器,代号NGINX-PROXY
  3. 启动一台HAProxy服务器,实现四层转发功能到NGINX HTTP服务器,代号HAProxy
  4. 在客户端分别访问NGINX-PROXY和HAProxy, 并采集NGINX-HTTP的日志
  5. 在NGINX-PROXY和HAProxy交叉请求CURL,并携带Header信息: X-FORWARDED-FOR, 并采集NGINX-HTTP的日志
  6. PROXY-PROTOCOL, 是四层转发开启携带真实IP补充协议,即: 在HTTP报文前方附加的数据。 参考文档

NGINX-HTTP服务器

  • HTTP服务主要配置
  • 因为4层转发携带了真实IP后, NGINX收到的不再是一个纯粹的HTTP报文。 所以需要在NGINX服务开启如下设置。 参考文档
 http { 	# r:访问IP, 也就是代理端IP。  	# P: PROXY-PROTOCOL的IP,也就是4层转发携带的补充报文的IP,也就是客户端IP。  	# xff: 藏在Header中的X-Forwarded-For的IP。 	log_format  proxy_protocol_log  'r: $remote_addr p: $proxy_protocol_addr xff: $http_x_forwarded_for';   	server {  		access_log logs/proxy_protocol_access.log proxy_protocol_log;	# 观察IP: 访问IP,代理IP,XFF  		error_log logs/proxy_protocol_error.log  		listen       80 ;               		# 接收HTTP报文走80端口                                         	 	listen       8000 proxy_protocol;     	# PROXY PROTOCOL 走8000端口,PROTOCOL 是通过四层转发获取真实IP的补充协议                	 	server_name  localhost;     	 	location / { 	 		root html; 	 		index index.html; 	 	} 	} } 

NGINX-PROXY服务器

关键配置

stream { 	server { 	  listen 80 ; 	  proxy_protocol on; 	# 需要开启使用PROXY PROTOCOL协议。 	  proxy_pass 192.168.1.104:8000; 	} } 

浏览器访问, NGINX-HTTP(192.168.1.104)输出日志信息

tail logs/proxy_protocol_access.log r: 192.168.1.102 p: 192.168.1.100 xff: - 

CURL访问,NGINX-HTTP(192.168.1.104)输出日志信息

tail logs/proxy_protocol_access.log r: 192.168.1.102 p: 192.168.1.103 xff: 192.168.1.199 

HAProxy服务器

关键配置

defaults     mode  tcp  backend app     balance     roundrobin     server  app1 192.168.1.104:8000 send-proxy  	# send-proxy 开启支持PROXY-PROTOCOL 

浏览器访问, NGINX-HTTP(192.168.1.104)输出日志信息

tail -f logs/proxy_protocol_access.log r: 192.168.1.103 p: 192.168.1.100 xff: - 

CURL访问,NGINX-HTTP(192.168.1.104)输出日志信息

tail -f logs/proxy_protocol_access.log r: 192.168.1.103 p: 192.168.1.102 xff: 192.168.1.199 

再次小结

我们尝试结合常见配置结构进行验证。 可以初步发现

  1. 4层转发携带客户端IP是需要使用PROPXY-PROTOCOL协议支持。客户端IP会补充在这个协议内
  2. 作为4层转发端,因为4层转发不修改报文,所以在HTTP报文前添加PROPXY-PROTOCOL信息。
    • 4层转发服务器需要开启 protocol 支持发送PROXY-PROTOCOL+HTTP的混淆报文结构
    • 4层目标服务器需要开启 protocol 支持接收PROXY-PROTOCOL+HTTP的混淆报文结构
  3. 使用补充PROXY-PROTOCL后,HTTP的报文不在是一个纯粹的报文结构,所以作为服务网端NGINX开启了两种模式
    • listen 80; 支持处理普通HTTP请求,
    • listen 80 protocol; 支持处理PROXY-PROTOCOL + HTTP报文的形式。这也是四层代理携带IP的特有形式
  4. 在选择HTTP服务器时,需要考虑HTTP服务是否支持PROXY-PROTOCOL。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!