众所周知,海外高性能服务器加不错的线路等于贵得离谱,然而不看网络的话,价格就便宜不少,再选择一台低配的线路不错的 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 和r eceiver,都指定 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:

而后在这篇帖子中发现问题,研究发现 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,而国外访问直接解析到后端源站。

如果你认为这篇文章还不错,可以考虑支持作者