如何在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\n

v2 格式如下:

  • 12 字节固定签名

\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A
  • 13字节包含协议版本和命令

# 版本
\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 {
        // 业务处理
    }
}

文章作者: 刘同学
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 刘同学的小站
后端 Nginx Linux Java
喜欢就支持一下吧