阅读量:0
准备工作
实验环境
IP | 角色 |
---|---|
192.168.1.100 | 客户端请求IP |
192.168.1.100 | python 启动的HTTP服务 |
192.168.1.102 | nginx服务 |
192.168.1.103 | haproxy 服务 |
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层转发携带客户端IP | nginx 通过增加header信息传递客户端IP | nginx重新封装HTTP报文 | 通过nginx封装的报文获取XFF |
7层代理会对报文进行重新封装,封装过程中可以通过增加XFF的header传递客户端IP。
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层转发携带客户端IP | nginx 通过增加header信息传递客户端IP | nginx重新封装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 | 客户端请求IP | client | 浏览器请求4层转发到后端HTTP服务 |
192.168.1.102 | NGINX实现提供4层转发 | NGINX-PROXY | 转发到104服务器 |
192.168.1.103 | Haproxy提供4层转发 | HAProxy | 4层转发到102的7层 |
192.168.1.104 | NGINX 7层服务 | NGINX-HTTP | 代表HTTP服务器。直接返回,并观察日志 |
- 启动一台NGINX HTTP服务器,扮演正常访问的网站。 代号NGINX-HTTP
- 另启动一台NGINX服务器,实现四层转发功能到NGINX HTTP服务器,代号NGINX-PROXY
- 启动一台HAProxy服务器,实现四层转发功能到NGINX HTTP服务器,代号HAProxy
- 在客户端分别访问NGINX-PROXY和HAProxy, 并采集NGINX-HTTP的日志
- 在NGINX-PROXY和HAProxy交叉请求CURL,并携带Header信息: X-FORWARDED-FOR, 并采集NGINX-HTTP的日志
- 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
再次小结
我们尝试结合常见配置结构进行验证。 可以初步发现
- 4层转发携带客户端IP是需要使用PROPXY-PROTOCOL协议支持。客户端IP会补充在这个协议内
- 作为4层转发端,因为4层转发不修改报文,所以在HTTP报文前添加PROPXY-PROTOCOL信息。
- 4层转发服务器需要开启 protocol 支持发送PROXY-PROTOCOL+HTTP的混淆报文结构
- 4层目标服务器需要开启 protocol 支持接收PROXY-PROTOCOL+HTTP的混淆报文结构
- 使用补充PROXY-PROTOCL后,HTTP的报文不在是一个纯粹的报文结构,所以作为服务网端NGINX开启了两种模式
- listen 80; 支持处理普通HTTP请求,
- listen 80 protocol; 支持处理PROXY-PROTOCOL + HTTP报文的形式。这也是四层代理携带IP的特有形式
- 在选择HTTP服务器时,需要考虑HTTP服务是否支持PROXY-PROTOCOL。