架构 - 保持客户端IP的透明代理方案
场景:

如上图,客户端通过反向代理或者负载均衡接入后端机器,这时候server直接通过remote_addr获取到的是反向代理或者负载均衡的IP地址,而不是客户端的真实IP地址。
解决方案:
1:通过http-x-forwarded-for
这种方式是通过在反向代理或者负载均衡器中配置http-x-forwarded-for,将客户端的真实IP地址传递给后端机器。
缺点:需要业务端配合改动,业务端需要封装IP获取方法,先从http请求头中的http-x-forwarded-for获取,对业务是侵入式的,如果是老业务中大量存在通过remote_addr获取的则改动量大,此方式不是很友好。如果是PHP则也可以通过nginx fastcgi参数修改传递给php的remote_addr,但如果是golang这些直接监听端口的场景也不适用,且这种改动不利于后期统一维护。
2:透明代理
内核模块(Transparent Proxy) + IP_TRANSPARENT + Netfilter 规则 + setsockopt
优点:业务层无需任何改动,代理层或者负载均衡对业务来说完全是透明的,后端server通过remote_addr获取的即是客户端真实ip。
仅展示了部分实现原理,完整配置方法请联系 yixiaogo@gmail 邮箱获取
1. IP_TRANSPARENT选项
在常规情况下,如果你想在一个 Socket 上调用 bind(),内核会检查你绑定的 IP 地址是否属于本机网卡。如果不属于,内核会直接返回 EADDRNOTAVAIL 错误。
IP_TRANSPARENT(透明代理选项)的作用是:
它允许一个进程 bind() 到一个不属于本机的 IP 地址,并且能够接收发送到该地址的数据包。
int opt = 1;
// 设置 IP 层的透明属性
setsockopt(fd, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt));
// 此时你可以 bind 一个完全不属于本机的 IP,比如 8.8.8.8
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("8.8.8.8");
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if (!net->ipv4_sysctl_ip_nonlocal_bind &&
!(inet->freebind || inet->transparent) &&
addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
chk_addr_ret != RTN_LOCAL &&
chk_addr_ret != RTN_MULTICAST &&
chk_addr_ret != RTN_BROADCAST)
goto out; // 绑定失败,返回错误
2. Netfilter
//新建一个mangle表上的规则, 名称是DIVERT
iptables -t mangle -N DIVERT
//在PREROUTING链上的mangle表添加规则, 如果请求包匹配系统已经存在的socket, 则跳转到DIVERT规则
iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
//在DIVERT规则上对该包执行打标记动作, 标记值是222
iptables -t mangle -A DIVERT -j MARK --set-mark 222
//运行该包通过DIVERT规则, 继续执行后续的规则
iptables -t mangle -A DIVERT -j ACCEPT
//在路由查找中, 带有222标记的包,走100路由表
ip rule add fwmark 222 lookup 100
//接收目的地址是任意IP地址的包,并当作本地IP由内核直接处理,而不转发到其他机器
ip route add local 0.0.0.0/0 dev lo table 100
-m socket 的作用:匹配下面状态的tcp链接

3. 三层转发

因为该socket的目标ip是客户端地址,路由后会走3层转发直接发到网关,所以后端机器的网关指向LB节点,这样不会走2层链路而是直接通过网关转发包。
#网关临时配置
ip route add default via 192.168.1.224 dev eth0 # 添加代理服务器为后端的网关
ip route del default via 192.168.1.1 dev eth0 # 删除原有网关地址
#网关永久生效网卡配置
vim /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
BOOTPROTO="static"
GATEWAY=192.168.0.207
IPADDR=192.168.0.62
NETMASK=255.255.255.0
ONBOOT=yes
#配置iptables, 把所有出口包都导向配置的网关 -- 主要解决和透明代理服务器同网段机器访问透明代理服务器问题
// 注: 如果客户端和代理服务器在同一个网段, 后端服务器因为是同2层会直接回包给客户端, 必须让它回包给代理服务器
#在后端服务器上,把本地服务(比如 80 端口)发出的包打上标记 333
iptables -t mangle -A OUTPUT -p tcp --sport 80 -j MARK --set-mark 333
#让带有 333 标记的包,去查自定义路由表 100
ip rule add fwmark 333 lookup 100
#在表 100 中,强制下一跳指向你的代理服务器 IP (比如 192.168.1.244)
ip route add default via 192.168.1.244 table 100
4. 整体流程
. LB 开启 IP_TRANSPARENT 选项:开启后LB会把socket的source ip设置为客户端的ip然后发送给后端机器。
. 后端处理完包后,会把包发送到网关,通过把网关设置为LB节点让包回到LB节点上。
. 流量拦截:通常配合 iptables和ip rule的使用,把后端回的包,重定向到指定路由表中,当作本机地址处理。
. LB收到后端回包后,再把这个包发送给客户端。