正常的透明代理

  1. 工作在 L3 的 VPN
  2. tun2socks 
  3. Linux 内核的 TPROXY

曲折的透明代理

  1. 支持 UDP 的 SOCKS5 代理软件
  2. 用户空间的 WireGuard 服务端结合 SOCKS5 转发
  3. 安装了 WireGuard 模块的 OpenWRT 路由器

刚好也是三项,但这不是三个方案,是一个方案的三个组件。

听起来似乎完全没必要做这么复杂,但为了利用上唯一能刷 OpenWRT 的低配路由器、不干扰本就复杂的服务器路由,同时保障信道安全、可达,最终扭曲成了这样。

即使你没有这种特殊情况,或许也能用上某个组件就是了,例如在没有 root 的环境下提供 WireGuard 服务。

支持 UDP 的 SOCKS5 代理

这个相对好找,不过建议使用底层为 UDP 且保留了 UDP 不可靠传输特性的代理软件。否则发挥不了 UDP 的优势,性能上可能有负面影响。

用户空间的 WireGuard 服务端

服务端在一台单独的服务器上运行,建议配置成静态 IP。服务器可以通过 DMZ 或 VLAN 方式接入路由器 LAN 口,也可以接入上一层路由、通过 WAN 口通信。

我选择了开源的 Wiretap,好处是运行在用户空间不用动内核,坏处是性能会比原生网络栈差一些,但应该比虚拟机强。我对 Wiretap 进行了轻度的魔改以支持代理,但 ICMP 包的处理上还没完善。 https://github.com/l2dy/wiretap/tree/tproxy

别忘了在防火墙上放行对应的 UDP 端口,否则是连不上的(废话)。

OpenWRT 路由器

有条件的话买一个高配路由器安装上代理软件并配置 TPROXY 是最佳方案。

但如果你坚持使用低内存或闪存空间有限的低配路由器,WireGuard 是 OpenWRT 上最轻量的 VPN 方案之一了。由于 WireGuard 协议不具备混淆功能、特征过于明显,建议仅在本地使用。当然如果信道完全可信,也可以考虑 tun2socks 方案,省下加密的开销。

那么 DNS 怎么办呢?由于 LuCI 不支持给 WireGuard 接口配置 DNS,需要配置全局 DNS 并关闭从 DHCP 获取 DNS 的选项(设置 noresolv '1')。在 WireGuard 接口配置了 0.0.0.0/0 路由后,DNS 也会走 VPN。这个方案的前提是 SOCKS5 代理支持 UDP,但如果不支持的话也可以魔改服务端,把请求转发到本地的可信 DNS。

TCP 层由于 Wiretap 已经在用户空间 ACK 了相关的包,不会像远程 L3 VPN 那样出现 TCP meltdown。但为了 UDP 还是需要合理调整 MTU 值,否则底层可能会把 UDP 包拆分传输,也可能直接丢弃。

参考资料

使用 HAProxy 可以将 https 的服务反向代理成 http,这样即使服务本身不好升级也能利用上 TLS 1.3 等更安全、高效的协议。

这里给出一个示例配置,server-template 中 6 为生成的 server 数量。日志输出可以按需调整。

global
  maxconn 10000
  log stderr format iso local7
  ssl-default-bind-options ssl-min-ver TLSv1.3

defaults
  timeout connect 100ms
  timeout client 30s
  timeout server 30s
  mode http
  maxconn 2000

frontend healthz
  bind *:8402
  monitor-url /healthz

frontend stats
  bind *:8404
  http-request use-service prometheus-exporter if { path /metrics }
  stats enable
  stats uri /
  stats refresh 5s

frontend http-in
  bind *:80
  log global
  option httplog

  use_backend proxy-%[req.hdr(host)],lower]

  default_backend deny

resolver defdns
  parse-resolv-conf
  accepted_payload_size 8192
  resolve_retries       3
  timeout resolve       1s
  timeout retry         2s
  hold other           1h
  hold refused         1h
  hold nx              1h
  hold timeout         1h
  hold valid           60s
  hold obsolete        30s

backend proxy-g.co
  balance roundrobin
  http-reuse always
  server-template web 6 g.co:443 ssl verify required ca-file /etc/pki/tls/cert.pem check-sni g.co sni req.hdr(host) check inter 16s fastinter 2s resolvers defdns init-addr none

backend deny
  http-request deny

官方建议的禁用方式是将 OTP 输入程序切换到需要长按的 Slot 2,可以使用 YubiKey Manager 或 ykman otp swap 实现。

使用命令行切换后可以通过 info 命令验证是否切换成功。

$ ykman otp info
Slot 1: empty
Slot 2: programmed

注意 macOS 系统下需要给终端 app 开启 Input Monitoring 权限,并关闭 app 的 Secure Keyboard Entry(如有)。

参考谷歌提供的文档,对于高负载(如何定义?)的服务器建议同时使用 fq qdisc。

# copy following into /etc/sysctl.d/98-tcp-bbr.conf
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

普通负载的虚拟机可以直接用默认的 fq_codel qdisc,只配置 tcp_congestion_control

#net.core.default_qdisc = fq_codel
net.ipv4.tcp_congestion_control = bbr

注意对于 qdisc 的修改需要重启服务器生效。

Rocky Linux 8 上安装 nginx 后对 nginx.conf 进行修改增加了一个 proxy_pass 配置,但用浏览器访问时提示 502 Bad Gateway,也就是连接不上后端服务。照例先怀疑一下 SELinux。

# getsebool httpd_can_network_connect
httpd_can_network_connect --> off

检查 SELinux 配置发现没有给 HTTP 服务主动发起 TCP 连接的权限,用 setsebool 调整后就正常了。

setsebool -P httpd_can_network_connect on