Netty服务如何使用Nginx代理转发请求并获得原始IP
如何在Nginx的stream模块配置启用proxy_protocol来让Netty服务端获取原始客户端IP。关键在于Nginx中的proxy_protocol配置以及在Netty中添加HAProxyMessageDecoder解码器。当接收到HAProxy消息时,可以从解码后的HAProxyMessage中获取源地址和端口信息。
proxy protocol在nginx中应用
我们知道nginx是一个web服务器和代理服务器,它一般工作在proxy server或者负载均衡软件(Haproxy,Amazon Elastic Load Balancer (ELB)的后面。
客户端首先请求proxy server或者LSB负载均衡软件,然后再到nginx进行真实的web访问。
因为经过了多层软件,所以客户端的一些信息比如ip地址,端口号等可能就会被隐藏,这对于我们问题分析,数据统计都是不利的。因为对于nginx来说,我们希望能够获得真实的客户端IP地址,这样才能获取真实的请求环境。
这种情况下就需要用到PROXY protocol了。
如果前面所说的proxy或者LSB都实现了PROXY protocol协议的话,不管是HTTP, SSL, HTTP/2, SPDY, WebSocket 还是 TCP协议,nginx都可以拿到客户端的原始IP地址,从而根据原始IP地址进行一些特殊的操作,比如屏蔽恶意IP的访问,根据IP不同展示不同的语言或者页面,或者更加简单的日志记录和统计等,都非常有效。
当然,如果想要支持PROXY protocol,对nginx的版本也是有要求的,具体版本需求如下:
想要支持PROXY protocol v2,需要NGINX Plus R16或者NGINX Open Source 1.13.11。
想要支持ROXY protocol for HTTP,需要NGINX Plus R3或者NGINX Open Source 1.5.12。
想要支持TCP client‑side PROXY protocol,需要NGINX Plus R7或者 NGINX Open Source 1.9.3。
想要支持PROXY protocol for TCP,需要NGINX Plus R11 或者 NGINX Open Source 1.11.4。
在nginx中可以通过下面的变量来获得对应的客户端信息,具体而言如下所示:
$proxy_protocol_addr和$proxy_protocol_port分别表示的是原始客户端的IP地址和端口号。
$remote_addr和$remote_port表示的是load balancer的的IP地址和端口。
如果你使用了RealIP扩展模块,那么这个模块会重写$remote_addr和$remote_port这两个值,将其替换成原始客户端的IP地址和端口号。
然后使用$realip_remote_addr和$realip_remote_port来表示load balancer的的IP地址和端口。
在RealIP扩展模块中,$proxy_protocol_addr和$proxy_protocol_port表示的含义不变,还是原始客户端的IP地址和端口号。
在nginx中配置使用proxy protocol
上面我们提到了nginx中proxy protocol的基本应用,下面来讲一下如何在nginx中进行具体的配置。
v1 格式如下:
# PROXY 传输协议 客户端IP 目标IP 客户端端口 目标端口
PROXY_STRING + single space + INET_PROTOCOL + single space + CLIENT_IP + single space + PROXY_IP + single space + CLIENT_PORT + single space + PROXY_PORT + "\r\n"示例
# IPV4
PROXY TCP4 198.51.100.22 203.0.113.7 35646 80\r\n
# IPV6
PROXY TCP6 2001:DB8::21f:5bff:febf:ce22:8a2e 2001:DB8::12f:8baa:eafc:ce29:6b2e 35646 80\r\n
# 未知PROXY UNKNOWN\r\nv2 格式如下:
12 字节固定签名
\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A13字节包含协议版本和命令
# 版本
\x2
# 命令
\x0 # LOCAL ,客户段直连
\x1 # PROXY ,进过转发第 14 个字节包含传输协议和地址族
# 传输协议
\x0 # AF_UNSPEC
\x1 # AF_INET (IPv4)
\x2 # AF_INET6 (IPv6)
\x3 # AF_UNIX (UNIX)
# 地址族
\x0 # UNSPEC
\x1 # STREAM (如:TCP 或 UNIX_STREAM)
\x2 # DGRAM (如:UDP 或 UNIX_DGRAM)第15、16字节为长度标记(剩下的报头长度)
# 前面的结构
struct proxy_hdr_v2 {
uint8_tsig[12];/* 十六进制 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */
uint8_tver_cmd;/* 协议版本和命令 */
uint8_tfam;/* 协议族和地址 */
uint16_tlen;/* 头的后续字节数 */
};接下来就是
源第三层地址
目标第三层地址
源第四层地址(如果有)
目标第四层地址(如果有)
union proxy_addr {
struct { /* for TCP/ UDP over IPv4, len = 12 */
uint32_t src_addr;
uint32_t dst_addr;
uint16_t src_port;
uint16_t dst_port;
} ipv4_addr;
struct { /* for TCP/UDP over IPv6, len = 36 */
uint8_t src_addr[16];
uint8_t dst_addr[16];
uint16_t src_port;
uint16_t dst_port;
} ipv6_addr;
struct { /* 对于 AF_UNIX 套接字,len = 216 */
uint8_t src_addr[108];
uint8_t dst_addr[108];
} unix_addr;
};
但,要开启 PROXY Protocol , 必须在转发器和服务端同时支持该协议, 很庆幸 Nginx 早就支持了。
开源版本 v1.5.12 以及之后支持v1; v1.13.11以及之后支持v2。
proxy protocol 插入配置
stream {
server {
listen 12345;
proxy_pass backend.example.com:8080;
proxy_protocol on;
}
}proxy protocol 接收配置
http {
log_format elb_log '$proxy_protocol_addr';
server {
listen 8080 proxy_protocol;
root /usr/local/nginx/html;
index index.html index.htm;
server_name hello-world.com;
set_real_ip_from 192.168.10.0/24;
real_ip_header proxy_protocol;
}
location / {
try_files $uri $uri/ /index.html;
proxy_pass http://backend.example2.com:8088;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
}
access_log /usr/local/nginx/stream.log elb_log;
}Netty配置
代理http协议的时候,可以通过增加X-Forwarded-For请求头传递。
然而TCP采用另一种方式,每次在建立连接的时候发送一个非常小的报告头信息,提供这些数据,注意也只发送一次。
我们在用Netty的时候,是需要提供相应的编解码器的,因此对于这个代理头Netty内置的有相应的Decoder.
类路径:io.netty.handler.codec.haproxy.HAProxyMessageDecoder
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (enableProxy) {
pipeline.addLast("proxyDecoder", new HAProxyMessageDecoder());
}
pipeline.addLast("frameDecoder", new FrameDecoder());
pipeline.addLast("frameEncoder", new FrameEncoder());
pipeline.addLast("protocolEncoder", new ProtocolEncoder());
pipeline.addLast("protocolDecoder", new ProtocolDecoder());
pipeline.addLast("channelHandler", new ChannelHandler());
}
当我们第一次获得请求头的时候,需要拿到原始的IP等信息,保存下来:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HAProxyMessage) {
HAProxyMessage proxyMessage = (HAProxyMessage) msg;
// 原始IP
proxyMessage.sourceAddress();
// 原始端口
proxyMessage.sourcePort();
} else {
// 业务处理
}
}