nginx的tcp透明代理设置
背景
目前,接入层access使用nginx作为反向代理。客户端连接nginx打开的5000端口后,nginx通过客户端ip哈希的方式,将客户端的接入请求负载均衡到后台的4个access服务。
使用nginx作为反向代理时,客户端与nginx直连,同时nginx为每一个客户端连接建立了单独的tcp连接到access服务。此时,nginx代理带来的问题是,access无法获取到客户端的真实ip,而只能拿到nginx代理服务所在机器的ip。(所有的代理都有类似的问题,因为与上游服务(例如这里的access服务)连接的实际上是代理服务器,ip包里的ip地址是代理服务器的地址)。
但是,很时候我们需要拿到客户端的真实ip做一些业务上的判断。这个时候,就需要通过某些方式获取客户端的真实ip。如果代理服务器和上游服务之前是通过http通讯,则可以通过http的首部X-Forwarded-For
将客户端的ip信息传给上游服务(nginx可以很方便的设置该首部设置)。
对于纯tcp代理,则获取客户端ip会麻烦一些。主要有两种方式:
- 类似http协议,在业务服务上包装一层proxy protocol,通过proxy protocol传递客户端的ip地址。
nginx可以通过protocol_proxy on
启用proxy protocol,同时,需要上游服务也增加对proxy protocol的支持。 - 通过Linux 2.6.24中,socket新增的选项IP_TRANSPARENT,使socket可以接收目的地址没有配置的数据包,也可以发送源地址不是本地地址的数据包实现。使用这种方式,上游服务不需要任何修改就可以得到客户端的ip。下文主要说明这种方式。
原理
nginx作为上游服务的反向代理时,每个客户端的连接方式如下:
tcp tcp
client <---------> nginx <---------> upstream server(access)
实现透明代理的原理是,nginx使用获取到的客户端ip来建立nginx与上游服务直接的tcp连接。也即:
- nginx与上游服务的tcp链接建立和通讯时,从nginx发布的ip数据包,源地址由nginx服务所在机器的地址替换为客户端的ip地址;
- 从上游服务器返回的ip数据包,目的地址也是客户端的ip地址,通过特殊的路由方式,将返回的ip数据包路由到nginx进程;
- 最后nginx将收到的数据发送给对应的客户端。
设置
服务和机器部署说明
总共有两台服务器,内网ip地址分别为172.19.228.32
和172.19.228.33
。两台机器部署的服务如下:
-
172.19.228.32
主服务器。nginx绑定5000端口。两个access服务绑定15010以及15011端口。 -
172.19.229.33
副服务器。两个access服务绑定15010以及15011端口。
nginx配置
- nginx需要以
root
的用户运行,因为socket的IP_TRANSPARENT
选项需要超级用户权限。
# in the "main" context
use root;
- nginx在
stream
中设置proxy_bind
,使nginx在连接上游服务器时,启用socket的IP_TRANSPARENT
选项。
# in the "stream" context
server {
...
proxy_bind $remote_addr transparent;
}
nginx设置完成并reload配置后,所有从nginx发送到上游服务器的ip数据包将使用客户端的ip地址作为源地址。后续的设置,需要将上游服务器返回的ip数据包(其中的目的地址是客户端的ip)返回到nginx所在的机器,并投递给nginx进程。
172.19.228.32服务器设置
32机器上的上游服务返回ip数据包给nginx时,指定的目的ip地址是客户端的ip地址。32机器需要将这些数据返回给nginx进程。
# 1. 将32上的上游服务回复的数据路由到nginx进程
# 1.1 对32上的上游服务返回的ip数据设置标记1
# NOTE: 如果32上增加access服务,需要修改这里--sport源端口,将新增的access服务端口包含进来
iptables -t mangle -A OUTPUT -p tcp --src 172.19.228.32 --sport 15010:15011 \
-j MARK --set-xmark 0x1/0xffffffff
# 1.2 新增路由表,表id为100,将所有ip数据路由到lo网络接口
# NOTE: 以下两条ip命令设置的route和rule都需要保存到/etc/rc.local中,以便在开机时启动
ip route add local 0.0.0.0/0 dev lo table 100
# 1.3 为标记为1的ip数据使用路由表100
ip rule add fwmark 1 lookup 100
设置完成后,32上的上游服务(端口为15010以及15011)发出的ip数据包,因为被设置了标记而匹配新增的策略路由,所有ip数据被路由到lo网络接口。同时,因为这些数据包中包含的端口是nginx连接上游服务时打开的端口,所以这些数据最终被分用到nginx进程。
这样,nginx和同在一个机器上的上游服务之间的透明代理设置完整。上游服务将可以得到客户端的ip地址,同时可以将数据返回给nginx,nginx再将返回的数据返回给对应的客户端。
此时,如果使用lsof(1)
命令查看上游服务打开的tcp链接,显示的源ip地址也将是客户端的ip地址。
172.19.228.33服务器设置
33上的上游服务与nginx在不同的主机上。主要设置如下:
# 1. 允许来自局域网网络接口bond0的ip数据
# 1.1 该设置主要避免来自32的ip数据被33屏蔽
iptables -I INPUT -i bond0 -j ACCEPT
# 1.2 禁用33主机上的reverse path filter。
# NOTE:因为nginx启动透明代理后,
# 33将在局域网网络接口bond0上收到源地址是客户端ip的数据(一般是公网ip),
# 如果不禁用reverse path filter, 则因为无法在局域网网络接口回复公网的数据包,
# 导致33上收到的tcp sync包被33直接丢弃,导致tcp连接无法建立。
echo "net.ipv4.conf.all.rp_filter = 0" >> /etc/sysctl.conf
echo "net.ipv4.conf.bond0.rp_filter = 0" >> /etc/sysctl.conf
# 2. 将33上的上游服务回复的数据路由到32机器
# 2.1 对33上的上游服务发出的包设置标记1
# NOTE: 如果33上增加access服务,需要修改这里--sport源端口,将新增的access服务端口包含进来
iptables -t mangle -A OUTPUT -p tcp --src 172.19.228.33 --sport 15010:15011 \
-j MARK --set-xmark 0x1/0xffffffff
# 2.2 新增路由表,表id为100,将所有ip数据路由到32机器
# NOTE: 以下两条ip命令设置的route和rule都需要保存到/etc/rc.local中,以便在开机时启动
ip route add default via 172.19.228.32 table 100
# 2.3 为标记为1的ip数据使用路由表100
ip rule add fwmark 1 lookup 100
# 3. 将33的上游服务返回的ip数据路由到32机器之后,再把这些数据路由到nginx进程
# 3.1 32上收到的来自33上游服务的数据设置标记1,使这些数据可以路由到nginx进程
# NOTE: 注意这一条命令在32主机上执行;
# 33上的增加access服务,需要修改这里的--sport源端口,将新增的access服务端口包含进来
# NOTE: 因为32上已经设置了对标记为1的ip数据使用策略路由表100,将其路由到lo网络接口
# 所以只需要对33返回的数据设置标记1即可
iptables -t mangle -A PREROUTING -p tcp --src 172.19.228.33 --sport 15010:15011 \
-j MARK --set-xmark 0x1/0xffffffff
设置完成后,33可以接收nginx发出的ip数据包,并可以将上游服务回复的ip数据路由给32机上的nginx进程。
同时,使用lsof(1)
命令查看33的上游服务打开的tcp链接,源ip将是客户端的ip。其他所有的与nginx不在同一个主机上的上游服务都可以参考这个配置设置。
参考:
nginx的透明代理实现
IP Transparency and Direct Server Return with NGINX and NGINX Plus as Transparent Proxy
Linux kernel rp_filter settings