准备工作
实验环境
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 | 不修改原始报文,在报文前方附加数据 |
附伪代码获取客户端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()