我们经常会在我们后端服务前加一层代理去做负载均衡或认证,比较有名的就是apisix。但是,这样会出现一些问题,比如说后端服务无法获取到客户真实的ip,显示的都是代理的ip,对于业务展示会有问题。对于7层的后端服务还好,可以在载荷里将客户的出口ip带上来,但是四层和三层就不太容易了,目前主流的有下面两种解决方案:
1. 原理
1.1. L4层获取---TOA插件
下载toa代码,编译出新的linux内核模块,这样tcp/ip协议栈可以将客户真实IP插入tcp option中,具体位置如下图所示:
这样以来,业务四层端口接入后,节点和源站经过三次握手,在最后一个ACK数据包的TCP Option中插入了源端口号和源IP等信息,共占8个字节。
1.2. L3层获取---透明转发
主要依赖nginx透明代理+iptables mark+route
核心思想是apisix使用获取到的客户端ip来建立apisix与上游服务直接的tcp连接。
考虑1.2实现最简单,也最稳定,以1.2方案进行实验。
2. 测试环境搭建
本地环境如下:
scss
tcp tcp
client(192.168.134.34) <---------> apisix(192.168.134.31:19001) <---------> server(192.168.134.35:19000)
server 192.168.134.35:19000
采用python起一个tcp服务,监听19000,负责回显客户端发来的tcp载荷
python
#coding:utf-8
from socket import *
from time import ctime
print("=====================时间戳TCP服务器=====================");
HOST = '0.0.0.0' #主机号为空白表示可以使用任何可用的地址。
PORT = 19000 #端口号
BUFSIZ = 1024 #接收数据缓冲大小
ADDR = (HOST, PORT)
tcpSerSock = socket(AF_INET, SOCK_STREAM) #创建TCP服务器套接字
tcpSerSock.bind(ADDR) #套接字与地址绑定
tcpSerSock.listen(5) #监听连接,同时连接请求的最大数目
while True:
print('等待客户端的连接...')
tcpCliSock, addr = tcpSerSock.accept() #接收客户端连接请求
print('取得连接:', addr)
while True:
data = tcpCliSock.recv(BUFSIZ) #连续接收指定字节的数据,接收到的是字节数组
print('rcv:', data)
if not data: #如果数据空白,则表示客户端退出,所以退出接收
break
if len(data) == 0:
continue
#tcpCliSock.send('[%s] %s' % (bytes(ctime(), 'utf-8'), data))
tcpCliSock.close() #关闭与客户端的连接
tcpSerSock.close() #关闭服务器socket
client 192.168.134.34
采用python写一个tcp客户端,目的ip和端口写为apisix代理端口
ini
#coding:utf-8
from socket import *
print("=====================TCP客户端=====================");
HOST = '192.168.134.31' #代理ip地址
PORT = 19001 #代理通信端口号
BUFSIZ = 1024 #接收数据缓冲大小
ADDR = (HOST, PORT)
tcpCliSock = socket(AF_INET, SOCK_STREAM) #创建客户端套接字
tcpCliSock.connect(ADDR) #发起TCP连接
while True:
data = input('> ') #接收用户输入
if not data: #如果用户输入为空,直接回车就会发送"",""就是代表false
break
tcpCliSock.send(bytes(data, 'utf-8')) #客户端发送消息,必须发送字节数组
data = tcpCliSock.recv(BUFSIZ) #接收回应消息,接收到的是字节数组
if not data: #如果接收服务器信息失败,或没有消息回应
break
print(data.decode('utf-8')) #打印回应消息,或者str(data,"utf-8")
tcpCliSock.close() #关闭客户端socket
apisix 192.168.134.31
github上下载最新的apisix-docker-master,解压后修改example中docker-compose.yml内的apisix服务为network_mode: host,即使用主机网络栈。
docker-compose -p docker-apisix up -d
可以看到服务启动正常:
3. 实现
我们把问题拆分成以下两点:
- apisix怎么配置可以生成自定义的nginx配置?
- nginx怎么配置透明代理?
vi ./apisix_conf/config.yaml
一定要配置root,因为SOCKET的IP_TRANSPARENT模式改包需要root权限。
进到容器里可以看到已经生成了自定义的ngnix配置,cat ./conf/nginx.conf:
但这样还不行,因为apache/apisix:3.2.1-debian的dokcerfile是以自建用户apisix启动的,所以即使配置了user root也会被忽略掉:
我采用多年的套壳大法改一下dockerfile:
重新compose down + up,我们就可以登录客户主机试一下IP透明转发了:
不出意外的话,肯定是出意外了:
我们用抓包大法看看啥情况,代理主机+客户端+服务端同时抓:
原来是代理和客户端握手后,代理以客户端源ip发起了新的请求到了服务端,然后服务端收到了报文后,由于目的ip是客户端,所以报文直接转发到了客户端,客户端很懵逼,会话表里没有,直接对服务端无情RST。
好了,这也恰恰证明,我们的透明代理是配置是正确的,只不过需要控制一下报文流转,类似于下图,服务端的报文重新指回代理,然后还要控制代理收到的报文不要直接转发,而要自己处理:
如何控制报文轮转?
- server回代理:
sql
iptables -t mangle -A OUTPUT -p tcp --src 192.168.134.35 --sport 19000:19005 -j MARK --set-xmark 0x1/0xffffffff
ip route add default via 192.168.134.31 table 100
ip rule add fwmark 1 lookup 100
对于从 IP 地址 192.168.134.35 发出的 TCP 流量,且源端口在 19000 到 19005 范围内的流量,进行标记。被标记的流量会走自定义100的路由表发给代理主机(192.168.134.31)
- 代理自己处理ngnix报文:
sql
iptables -t mangle -A PREROUTING -p tcp --src 192.168.134.35 --sport 19000:19005 -j MARK --set-xmark 0x1/0xffffffff
ip route add local 0.0.0.0/0 dev lo table 100
ip rule add fwmark 1 lookup 100
对于从 IP 地址 192.168.134.35 发过来的 TCP 流量转发进行标记,被标记的流量会走自定义100的路由表而不是本地路由表转发,它会直接路由回本地,由于本地有apisix和server的会话信息,因此协议栈会将报文转发给apisix。
这样server就获得了真实的客户ip:
之前我试过apisix的briage模式部署,但是发现代理发的报文并没有到达服务端,只发到网桥的虚拟网卡就丢掉了,没有抓发给eth0,怀疑与docker的iptablses策略有关,先不研究了,非要搞在搞。
Reference
www.duidaima.com/Group/Topic...