跳转到主要内容

使用 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 HTTPS 7 层和 4 层代理

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

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

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

注意:此处的 HTTPS 4 层代理并不完全是 4 层代理,因为 HTTPS 工作在 7 层,需要从 7 层拿到域名信息。完全的 4 层分流只能做到根据 5 元组信息分流(源 IP 地址,源端口,目的 IP 地址,目的端口,传输层协议)。

7 层代理

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

NGINX 配置文件如下:

http {
	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 必须查看上层信息才能找到域名,使用 ngx_stream_ssl_preread_module 模块我们可以从 7 层读取信息,所以通过 ngx_stream_ssl_preread_module 模块实现的 4 层代理其实不能算作严格意义上的 4 层代理

SNI

为了在不解密流量的情况下拿到 HTTPS 流量的目标域名,原版 HTTPS 是不行的,必须使用在 RFC 6066 中定义的 TLS 扩展协议: SNI(Server Name Indication) 服务器名称指示。

为了在一个 IP 和端口上同时支持多个 HTTP 域名,在 HTTP 1.1 时加入了 Host 标头用于识别你访问的具体域名。
然而到了 HTTPS 时代,流量已经被加密,TLS 握手时 HTTP 交互还没开始,所以服务器拿不到 Host 标头,也就不知道你要访问哪个域名。

于是 SNI 出现了,用于在一个 IP 和端口上同时支持多个 HTTPS 域名。

SNI 会在 Client Hello 包中加入明文(TLS 连接建立前)的 Server Name: example.com 字段,于是服务器可以从这里获取到域名,从而返回正确的证书。
当然 SNI 也有 SNI 前置(假 Client Hello)和加密的 ESNI 和 ECH 来避免明文问题,但那又是另一回事了。

所以,NGINX 的 4 层 HTTPS 代理使用 ngx_stream_ssl_preread_module 模块来在不终止 SSL/TLS 的情况下从 ClientHello 消息中提取信息,也因此 Client Hello 包中必须包含有 SNI 字段才能够被正确读取。

NGINX 配置文件如下:

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

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

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

应用场景

现在流量在经过此设备(网关、隧道等)时有部分域名需要走 socks5 或 https 代理通道进行处理。
有以下要求:

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

初略看来有以下几种方案:

  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 已经分流过了,其他的流量只会路过而已。

当然,客户端要能直接访问 NGINX 服务器,且 DNS 解析需要发送到 dnsmasq 服务器,例如使用 Iptables 将来自客户端的 53 端口 udp 流量重定向至本机的 53 端口。

443:

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

80:

http {
	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 代表上游,可以指定多个;

主要参考