代理模式下获取客户真实IP

我们经常会在我们后端服务前加一层代理去做负载均衡或认证,比较有名的就是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. 实现

我们把问题拆分成以下两点:

  1. apisix怎么配置可以生成自定义的nginx配置?
  2. 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。

好了,这也恰恰证明,我们的透明代理是配置是正确的,只不过需要控制一下报文流转,类似于下图,服务端的报文重新指回代理,然后还要控制代理收到的报文不要直接转发,而要自己处理:

如何控制报文轮转?

  1. 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)

  1. 代理自己处理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...

apisix.apache.org/zh/docs/api...

cloud.tencent.com/docum

相关推荐
raoxiaoya19 分钟前
同时安装多个版本的golang
开发语言·后端·golang
考虑考虑2 小时前
go使用gorilla/websocket实现websocket
后端·程序员·go
李少兄2 小时前
解决Spring Boot多模块自动配置失效问题
java·spring boot·后端
Piper蛋窝3 小时前
Go 1.19 相比 Go 1.18 有哪些值得注意的改动?
后端
码农BookSea3 小时前
不用Mockito写单元测试?你可能在浪费一半时间
后端·单元测试
帽儿山的枪手3 小时前
socket套接字你搞清楚了吗
网络协议·面试
codingandsleeping4 小时前
Express入门
javascript·后端·node.js
白山云北诗4 小时前
什么是 DDoS 攻击?高防 IP 如何有效防护?2025全面解析与方案推荐
网络协议·tcp/ip·ddos·高防ip·ddos攻击怎么防·高防ip是什么
ss2734 小时前
基于Springboot + vue + 爬虫实现的高考志愿智能推荐系统
spring boot·后端·高考
mxbb.4 小时前
我的HTTP和HTTPS
网络协议·http·https