跳转到主要内容

使用 Dnsmasq + NGINX + Socks5(Goproxy) 做反向 HTTPS 4 层透明代理 —— NGINX 7 层和 4 层代理以及各种介绍

现在流量在经过此节点时有部分域名需要走 socks5 或 https 代理通道进行处理。

有以下要求:

  • 不影响其他流量、域名;
  • 客户端不进行任何处理;
  • 尽量低侵入;
  • 尽量低占用;

那么要怎么处理呢?

先进一段科普。

HTTP/HTTPS 代理的分类

分类依据:代理是否对客户端透明
  • Common Proxy 普通代理:需要在客户端填入代理地址等信息,客户端知道有代理存在。
  • Transparent Proxy 透明代理:不需要在客户端进行代理设置。代理对客户端是透明的,客户端不知道有代理的存在。
分类依据:代理是否加解密 HTTPS
  • Tunnel Proxy 隧道代理:透明传输流量的代理。代理服务器不会解密 TLS/SSL 或感知其代理流量的具体内容,而直接将其转发到下一个目的地。客户端与目标服务器建立 TLS/SSL 连接,与代理无关,整个链路上有 1 个 TLS/SSL 链接。
  • Man-in-the-Middle (MITM) Proxy 中间人代理:代理服务器解密 HTTPS 流量,使用自签名证书与客户端建立 TLS/SSL 连接,由代理解密流量,再由代理服务器与目标服务器建立 TLS/SSL 连接加密流量(SSL 终结/SSL 卸载)。整个链路上有 2 个 TLS/SSL 链接,客户端 --- 第一次TLS --- 代理 --- 另一次TLS --- 目标服务器。

注意:在中间人代理的情况下,客户端实际上是在 TLS 握手过程中获取了代理服务器的自签名证书,证书链的校验默认是不成功的。所以代理自签名证书中的根 CA 证书必须在客户端上受信任。
因此,客户端在这个过程中是知道代理的。如果将自签名根 CA 证书安装到客户端,则实现了透明代理。但你需要在客户端上手动安装自签名根 CA 证书,这个过程是不透明的,且安装根证书有一定风险。

NGINX 7 层和 4 层代理

我们主要关注 HTTPS 代理,因为 HTTP 代理真的很随便。
于是 NGINX 给你了 2 种方案,7 层(L7)代理和 4 层(L4)代理。

  • 7 层代理属于中间人代理,需要对流量进行加解密。
  • 4 层代理则属于隧道代理,不需要对流量进行加解密。

如果你无法安装 CA 根证书,或是使用另一个域名和普通被信任的证书,那么 7 层代理不可以实现透明代理。

7 层代理

假如你使用 NGINX 作为过你网站的反向代理,如 LNMP 架构,那么这就是一次标准的 7 层代理。

NGINX 配置文件如下:

server {
     listen  443;
    
  
     location / {
         proxy_pass http://example.com;
    	#proxy_pass https://example.com;
     }
 }

4 层代理

需要 ngx_stream_core_module 模块支持和 ngx_stream_ssl_preread_module。

但 NGINX 如何在不解密流量的情况下拿到客户端想访问的域名?
4 层只能拿到五元组信息(源IP地址,源端口,目的IP地址,目的端口和传输层协议。

因此,NGINX 必须查看上层信息才能找到域名,因此 NGINX stream 模块不能算作严格意义上的 4 层代理。

SNI

为了在不解密流量的情况下拿到 HTTPS 流量的目标域名,原版 HTTPS 是不行的,必须使用在 RFC 6066 中定义的 TLS 扩展协议: SNI(Server Name Indication) 服务器名称指示。
SNI 最初用于在一个 IP 和端口上同时支持多个域名,因为流量已经加密,TLS 握手时 HTTP 交互还没开始,所以服务器拿不到 Host 标头(在 HTTP 1.1 加入)也就不知道你要访问哪个域名。

SNI 会在 Client Hello 包中加入明文(TLS 连接建立前)的 Server Name: example.com 字段,于是 NGINX 可以从这里获取到域名。
当然 SNI 也有 SNI 前置的假域名和加密的 ESNI,但那又是另一回事了。

也因此,NGINX 的 4 层 HTTPS 代理必须在 Client Hello 包中包含 SNI 字段(ngx_stream_ssl_preread_module 模块)。

NGINX 配置文件如下:

stream {
    server {
        listen 443;
        ssl_preread on;
    
        proxy_pass $ssl_preread_server_name:$server_port;
    }
}

注意这是 stream{},不可以写在 http{} 内。

  • ssl_preread on:启用在预读阶段从 ClientHello 消息中提取信息;
  • $ssl_preread_server_name:通过 SNI 请求的服务器名称;

应用场景

现在流量在经过此节点时有部分域名需要走 socks5 或 http 代理通道进行处理。
有以下要求:

  • 不影响其他流量、域名;
  • 客户端不进行任何处理;
  • 尽量低侵入;
  • 尽量低占用;

有以下方案

  1. 使用 CLASH redir-port + 防火墙重定向流量;缺点:防火墙配置复杂,容易出错,优点:协议支持多,分流能力强;
  2. 使用 CLASH tun + 防火墙重定向流量;缺点:防火墙配置复杂,容易出错、tun 兼容问题,优点:协议支持多,分流能力强;
  3. 使用 Goproxy 作为反向代理服务器;缺点:需要安装 CA 证书,优点:配置简单;
  4. 使用 Dnsmasq 重定向域名 + NGINX 4 层代理 + 透明代理服务端;缺点:组件较多,优点:低侵入性;
方案 4

当然我们选择第 4 种方案,其实前 3 种方案我都试过了。

首先我们使用 Dnsmasq 将要访问的域名重定向到本地或 NGINX 所在地。
当然,这个用 hosts 就可以,我使用 Dnsmasq 主要是服务器上已经有了。

address=/example.com/127.0.0.1

普通的 NGINX 是不能直接处理 https 代理和 socks5 代理的,所以我们起一个 4 层代理,将流量转发到一个透明代理,由透明代理再去访问 https 代理和 socks5 代理。
然后起一个 NGINX 的 4 层代理,监听本地的 443 和 80 端口,不需要加主机名,因为 dnsmasq 已经分流过了,其他的流量只会路过而已。

443:

stream {
    server {
        listen 443;
        ssl_preread on;
    
        proxy_pass $ssl_preread_server_name:$server_port;
    }
}

80:

server{
        listen 80;

        location / {
                proxy_buffering off;
                proxy_pass http://$http_host;
        }
}

这样一来,流量就变成了是从本机发出的。

接下来,我们可以利用 iptables 等工具将 NGINX 发出的流量重定向到透明代理,只重定向了 nginx 用户发出的流量,所以对其他流量是没有影响的。

iptables:

iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner 995 --dport 80 -j REDIRECT --to-ports 23457
iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner 995 --dport 443 -j REDIRECT --to-ports 23457

995 是我这里 nginx 用户的 uid。
将 nginx 用户发出的目标为 80 和 443 的流量都重定向到 23456 端口。

firewalld:

firewall-cmd --permanent --zone=public --add-forward-port=port=80:proto=tcp:toport=23456 --uid-owner=995
firewall-cmd --permanent --zone=public --add-forward-port=port=443:proto=tcp:toport=23456 --uid-owner=995

不是特别确定这两条命令是否有用,但你可以使用 --direct 命令来使用 iptabels 语法。

最后,我们使用 https://github.com/snail007/goproxy 的透明代理模式(代理协议转换)将 socks5 或 https 代理转换为透明代理。

./proxy sps --redir --forever -p :23456 -P socks5://foo:bar@example.com:443 -P https://foo:bar@example.com:443
  • sps 代表透明代理模式;
  • --redir iptables 透明代理模式;
  • --forever 守护运行;
  • -p 代表下游/监听端口;
  • -P 代表上游,可以指定多个;

主要参考