众所周知,海外高性能服务器加不错的线路等于贵得离谱,然而不看网络的话,价格就便宜不少,再选择一台低配的线路不错的VPS作为前端进行代理的就划算很多。做站的话一般通过nginx进行http或者tcp代理,以前一直都在使用http反向代理,好处是配置简单效果也不差,但如果进行https代理,前端也需要配置ssl证书,从安全的角度来说显然是不合理的。因此折腾了下通过Nginx Stream模块进行tcp代理,因为只是传输层协议,便可以不用配置ssl证书。

然而无论那种方式,对于做站来说,都有获取到真实ip的需求。如果是http代理的话,可以通过编译ngx_http_realip_module模块并设置proxy_set_header实现,这里不再讨论。这篇文章着重说下tcp代理的实现方法和过程。

要使用stream模块,编译Nginx的时候需要加上--with-stream参数,然后再nginx.conf中配置,一个简单的代理模型如下:

stream{
    ...
    upstream backend{
        server your.domain.com:443
    }

    server{
        listen 443 ssl;
        proxy_pass backend;
        ...
    }
}

而要实现代理多个后端还需要用到ngx_stream_ssl_preread_module模块,需要自行编译,该模块可以在不解密的情况下,通过SNI获取到访问的域名,之后在server中加入ssl_preread on,示例如下:

stream{
    ...

    map $ssl_preread_server_name $backend {
        server-1.com server_1;
        server-2.com server_2;
        ...
        default server_1;
    }

    upstream server_1{
        server server-1.com:443;
    }

    upstream server_2{
        server server-2.com:443;
    }

    ...

    server{
        listen 443 ssl;
        ssl_preread on;
        proxy_pass backend;
        ...
    }
}

那么如tcp代理如何获取到真实ip呢,这里就需要用到proxy_protocol协议,该协议是HAProxy作者开发的一个开源协议,通过添加tcp头信息传递客户端的参数。具体的实现主要是参考了Nginx官方的文档Accepting the PROXY Protocol。具体的实现不算复杂,proxy_protocol需要两个角色sender和receiver,都指定proxy_protocol协议即可,示例如下:

stream{
    ...
    # 这里修改记录格式为$proxy_protocol_addr
    log_format basic '$proxy_protocol_addr - $remote_user [$time_local] '
                '$protocol $status $bytes_sent $bytes_received '
                '$session_time';

    map $ssl_preread_server_name $backend {
        server-1.com server_1;
        server-2.com server_2;
        ...
        default server_1;
    }

    upstream server_1{
        server server-1.com:443;
    }

    upstream server_2{
        server server-2.com:443;
    }

    ...

    server{
        listen 443 ssl;
        ssl_preread on;
        proxy_protocol on; # 这里打开proxy_protocol协议
        proxy_pass backend;
        ...
    }
}

正确的接受信息,需要在server的listen端口后加上proxy_protocol,如listen 443 ssl http2 proxy_protocol;并设置tcp代理的CIDR地址:

set_real_ip_from 192.168.1.0/24;
real_ip_header   proxy_protocol;

对于http,记录真实ip需要设置proxy_set_header

http {
    proxy_set_header X-Real-IP       $proxy_protocol_addr;
    proxy_set_header X-Forwarded-For $proxy_protocol_addr;
}

以上设置便可以通过tcp反向代理并获取到真实ip了,由于我是国内外分线路解析,国内走前端代理,国外直连,因此我的问题并没有解决。最开始发现前端能访问,后端无法直接访问,也就是国内能访问到,而国外则无法连接,查看error.log日志也发现乱码并提示broken header:

使用Nginx实现TCP四层反向代理

而后在这篇帖子中发现问题,研究发现proxy_protocol的接收端必须在接收到完整有效的proxy_protocol头部后才能开始处理连接数据,sender在与receiver之间建立连接后,会先发送一个带有客户信息的tcp header,因为更改了tcp协议,receiver也必须支持proxy_protocol,否则不能识别tcp包头,导致无法成功建立连接。也就是如果服务器接收到的第一个数据包不符合proxy_protocol的格式,服务器会直接终止连接。而我的策略是国外走的是直连,并不需要proxy_protocol,添加上的话没有前端发送对应的协议,就会无法打开,而如果去掉的话,就会变成国外能访问,国内无法访问。

最终的解决方案与stackoverflow上的一篇帖子中提到的类似,国外保留443接口直连,使用传统协议,而前端将请求转发到后端的另一个端口,设置防火墙该端口只能通过前端的ip访问,并通过Nginx监听接受proxy_protocol协议。这样就解决了在tcp代理的同时分线路的问题,最终实现了国内访问解析到前端,然后通过tcp四层反向代理到后端并传递真实ip,而国外访问直接解析到后端源站。

Tags: Nginx

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.