前言
在软路由或 PVE 环境下搭建 UniFi Controller (自托管版) 是很多 Network Engineer 的常规操作。但如果你像我一样,希望通过一台链路较好的云服务器建立一个专网来统一管理远程设备,并且遭遇了 手机 UniFi App 无法连接 或 网页端频繁报错 的困扰,那么这篇文章就是为你准备的。
本文不讲基础安装,只干一件事情:如何用 Apache 完美地反向代理一个跨地域、跨隧道的 UniFi Controller 实例。
踩坑复盘:为什么普通的 ProxyPass 搞不定 UniFi?
在尝试过 ProxyPass / https://172.x.x.x:11443/ 以及各种 mod_proxy 的组合后,我遭遇了全套的 HTTP 错误大礼包:
- 502 Bad Gateway (静态资源失败):HTML 加载了,但 CSS、JS 和 Favicon 全部 502。
- 400 Bad Request (登录失败):输入账号密码后弹出 400,根本进不去 Dashboard。
- 白屏 / 循环重定向:F12 看到 WebSocket (wss://) 连接一直处于 Pending 或 Finished 状态。
核心难点分析
UniFi 的后端 Nginx 非常"挑剔",它的反代有三个死穴:
- 死穴一:WebSocket 深度依赖 :UniFi 的 Dashboard 流量图和实时设备状态是通过 WebSocket 推送的。如果反代没有实现精准的 协议升级(Upgrade),页面就是死的。
- 死穴二:Host 头校验 :UniFi 会校验 HTTP 请求头中的
Host。如果传的是域名,而它内部只接受 IP:端口,或者传端口不匹配,它会觉得你在进行 CSRF(跨站请求伪造) 攻击,直接报 400。 - 死穴三:Origin 和 Referer 的 CSRF 保护 :它对
Origin和Referer头也非常敏感。跨地域反代时,浏览器发出的域名必须在到达后端之前被彻底清理或重写。
终极解决方案:mod_rewrite [P] 协议硬切分
在无数次尝试后,我发现常规的 mod_proxy 处理器在处理这种情况时过于"智能",反而导致了逻辑混淆。最后的解法是回归本质,使用 mod_rewrite 的 [P] (Proxy) 模式,手动实现 HTTP 与 WebSocket 的硬切分。
1. 基础环境准备
在 Apache 服务器上,确保启用了以下关键模块:
bash
# Ubuntu/Debian 示例
sudo a2enmod rewrite proxy_wstunnel proxy_http ssl headers
sudo systemctl restart apache2
2. VirtualHost 配置文件 (The Working One)
这是经过实战检验的配置。重点在于 使用 RewriteRule 配合 RewriteCond 精准剥离 WebSocket 流量。
网络架构假设:
- 公网域名:
unifi.example.net(已获取 SSL 证书) - 后端 UniFi IP:
10.0.57.5(运行在虚拟局域网隧道内) - 后端 UniFi 端口:
8443(默认 HTTPS 端口)
apache
<Virtualhost *:443>
ServerName unifi.example.net
ServerAlias unifi
# 开启重写引擎和代理引擎
RewriteEngine On
SSLProxyEngine on
# 1. 忽略后端证书校验 (因为是自签名证书)
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLProxyCheckPeerExpire off
# ============================================================
# 核心逻辑:协议硬切分 (WebSocket vs HTTPS)
# ============================================================
# 场景 A:处理 WebSocket 升级请求 (wss://)
# 检查 HTTP Header 是否包含 Upgrade: websocket
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) wss://10.0.57.5:8443/$1 [P,L]
# 场景 B:处理常规 HTTPS 请求 (https://)
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) https://10.0.57.5:8443/$1 [P,L]
# ============================================================
# 关键 Header 欺骗与清理 (解决 400 错误的关键)
# ============================================================
# 1. 关键:把域名传给浏览器 (告诉它你是 unifi.example.net)
Header set Host "unifi.example.net"
# 2. 关键:把内网 IP和端口传给 UniFi 后端 (骗过校验)
# 注意:这里的端口号 :8443 至关重要,否则会报 400
RequestHeader set Host "10.0.57.5:8443"
# 3. CSRF 绕过:彻底清理可能导致校验失败的头
Header unset Referer
RequestHeader unset Origin
RequestHeader unset Referer
# SSL 证书配置 (Certbot 自动生成)
SSLCertificateFile /etc/letsencrypt/live/unifi.example.net/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/unifi.example.net/privkey.pem
# 开启日志便于调试
CustomLog ${APACHE_LOG_DIR}/unifi.log vhost_combined
</Virtualhost>
3. 配置解析 (为什么要这么写?)
- 为什么是
RewriteRule /(.*) ... [P,L]?
不同于普通的ProxyPass,[P]标记强制将请求通过mod_proxy的特定协议处理器转发。它允许我们将ws://和http://分开,解决了混合模式下 502 的问题。 RequestHeader set Host "10.0.57.5:8443"
这是解决 400 错误的神来之笔。UniFi 的后端 Nginx 可能配置了严格的ServerName校验。通过强制设置 Host 为后端的内网 IP 且带上端口,我们骗过了后端的安全性检查,使请求得以通过。RequestHeader unset Origin
UniFi 的 Web UI 会比对Origin头和它已知的 Host。如果一个从公网发来的请求带着域名 Origin到达只知道 IP 的后端,校验会失败。干掉这个头可以绕过该保护机制。
避坑总结与进阶
- HSTS 与 502 :如果你之前尝试过开启 Cloudflare 的 Proxy(橘云),可能会有 HSTS 缓存。在调试前,务必清理浏览器的 HSTS 状态(Chrome 可在
chrome://net-internals/#hsts下处理)。 - App 远程接入 :解决完 Web 端,手机 App 远程接入就迎刃而解了。在 App 中选择 Manual Setup ,Host 填写域名,端口填
443即可实现秒开。 - MTU 隐患 :如果你的内网隧道性能较差,加载大文件(CSS/JS)可能会因为大包被截断而 502。此时需要检查隧道的 MTU 设置(建议调低至 1280 观察)。
结语
Apache 作为反向代理,在面对 UniFi 这种结合了 WSS、H2 和严格 CSRF 校验的现代应用时,其默认行为确实有些"落后"。通过回归底层协议硬切分 的思路,结合 mod_rewrite 的灵活性,我们成功绕过了它的所有死穴。
希望这篇文章能帮你省下几个小时的排障时间。