摘要:在前一篇文章中,我利用 FRP + VPS 打通了中美两地的家庭网络,实现了一个私有的“住宅 IP 代理池”。但在 Phase A 阶段,系统暴露了大量端口 (7000, 8900, 10087),不仅不够优雅,还存在被 GFW 主动探测的风险。

今天,我完成了 Phase B 架构升级:利用 WebSocket + Nginx 反向代理,将所有管理流量、隧道流量、代理流量全部“塞”进了唯一的 443 端口。外人看来,这只是一个部署了 SSL 的静态技术博客。


1. 痛点:端口过多带来的焦虑

在 Phase A 的架构中,我的 VPS 防火墙简直像个筛子:

  • :7000: FRP 服务端通信端口 (暴露)
  • :7500: FRP 仪表盘 (暴露)
  • :10087: Mac 节点的代理入口 (暴露)
  • :8900: 流量监控 API (暴露)

这种架构有两个致命问题:

  1. 特征明显:FRP 的 TCP 协议特征非常容易被识别,在高敏时期极其脆弱。
  2. 安全隐患:任何扫端口的脚本都能发现这里运行着非 HTTP 服务,增加了被爆破的风险。

目标:所有服务只开放 :443 (HTTPS) 和 :80 (重定向用),其他所有端口全部绑定在 127.0.0.1,不对外服务。


2. 核心技术方案:协议伪装与分流

要实现“全站 443”,我采用了 Nginx 作为统一网关,配合 WebSocket (WSS) 协议进行分流。

架构图解



graph TD
    User["外部访客 / 客户端"] -->|"HTTPS (443)"| Nginx["Nginx 网关 (VPS)"]
    
    subgraph VPS ["VPS Server"]
        direction TB
        Nginx
        Blog["静态博客 (/www/wwwroot)"]
        FRPS["FRP 服务端 (:7000)"]
        Proxy["Mac 节点反代 (:10087)"]
    end
    
    Nginx -->|"默认访问"| Blog
    Nginx -->|"Header: Upgrade=websocket"| FRPS
    Nginx -->|"Path: /vmess-mac"| Proxy
    
    FRPS <==>|"FRP 隧道"| MacNode["Mac 家庭节点"]

2.1 第一层伪装:FRP 的“寄生”

FRP 官方虽然支持 WebSocket 模式,但新版本 (v0.67.0+) 对自定义路径的支持存在兼容性问题。为了极致的隐蔽性,我采用了一种更底层的 Header Hijacking (HTTP 头劫持) 方案。

在 Nginx 的根路径 / 配置中:

  1. 默认情况:返回 /www/wwwroot 下的静态博客(也就是你现在看到的这个伪装站)。
  2. 特殊情况:如果请求头包含 Upgrade: websocket,且没有命中其他特定路径,则判定为 FRP 客户端,流量直接转发给本地监听的 :7000
1
2
3
4
5
6
7
8
9
10
11
location / {
# 核心逻辑:检测 WebSocket 头
# 如果是 WebSocket 连接,视为 FRP 流量
if ($http_upgrade = "websocket") {
proxy_pass http://127.0.0.1:7000;
}

# 普通访客(浏览器)看到的是博客
root /www/wwwroot/blog.example.com;
index index.html;
}

这意味着 FRP 客户端连接 wss://blog.example.com:443 时,Nginx 会自动“偷梁换柱”。在防火墙外部看来,这只是一个长连接的 HTTPS 请求。

2.2 第二层伪装:代理数据流的“隐身”

之前的架构中,Shadowrocket 等客户端是直连 VPS 的 :10087 端口,流量是裸奔的 TCP。
现在,我要求 Mac 端的 X-UI 节点也必须把入站协议改成 WebSocket,并指定一个隐蔽的路径(如 /vmess-mac)。

Nginx 路由规则

1
2
3
4
5
6
7
8
9
10
# 识别特定路径,转发给 Mac 节点的反向代理端口
location ^~ /vmess-mac {
proxy_pass http://127.0.0.1:10087; # 这是 FRP 映射在 VPS 本地的端口
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

# 传递真实 IP,虽然对于 FRP 后的内网穿透不一定有效,但保持好习惯
proxy_set_header X-Real-IP $remote_addr;
}

这样,所有的代理流量就变成了标准的 HTTPS 请求,路径 https://blog.example.com/vmess-mac 看上去就像是博客的一个 API 接口或静态资源,完全融入了正常的网页流量中。


3. 自动化运维:拒绝手动配置

为了配合这次升级,所有的客户端配置都必须同步更新。手动改几十个配置(Android, iOS, Mac, Windows)太蠢了,且容易出错。我重构了 Python 侧边栏脚本 (sync_mac.py)。

现在的逻辑是:

  1. Mac 启动
  2. 拉取 VPS 配置(获取最新的 UUID 和端口映射)
  3. 强制刷写数据库(确保本地 X-UI 设置符合隐身协议)

脚本会自动连接本地 SQLite 数据库,强制将 Inbound 协议修改为 WebSocket,并锁定 Path:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 自动化代码片段:强制纠正传输协议
new_stream_settings = {
"network": "ws",
"security": "none",
"wsSettings": {
"path": "/vmess-mac", # 这里的路径必须与 Nginx 一致
"headers": {}
}
}

# 直接操作数据库,绕过面板 UI 可能的误操作
cursor.execute(
"UPDATE inbounds SET stream_settings = ? WHERE port = ?",
(json.dumps(new_stream_settings), local_port)
)

这意味着无论在面板上怎么乱改,只要重启容器,配置就会自动恢复到正确的“隐身模式”。这确保了我的系统中只存在符合“Phase B”架构的流量。


4. 最终形态与思考

经过这番折腾,我的 VPS 现在的状态是:

  • Open Ports: 仅 80 (HTTP Redirect), 443 (HTTPS), 22 (SSH)。
  • Web: 一个漂亮、合法的静态个人主页。
  • Hidden Services:
    • 流量管理 API (/report)
    • Mac 住宅代理 (/vmess-mac)
    • Android 住宅代理 (/vmess-android)
    • FRP 控制平面 (由 Nginx 分流)

这其实已经是一个微型的 DePIN (去中心化物理基础设施网络) 原型了。和市面上的 Honeygain 或 Grass 不同,这套系统完全由我掌控——数据主权在我,网络质量我说了算,而且没有任何中间商抽成或监控。

在这个数据被大厂垄断的时代,拥有一套完全属于自己的、坚不可摧的物理网络基础设施,大概就是极客最后的浪漫吧。


Next Step: 下一步计划研究如何优化移动端的连接稳定性,或者引入 UDP over WSS 技术来提升视频流的吞吐量。

今天的重构不仅仅是修修补补,而是底层运行架构的彻底革新。我将原本零散的脚本、二进制文件全部容器化,实现了真正意义上的 “Infrastructure as Code” (IaC)。

核心变革:全面的 Docker 化 (The Big Switch)

在此之前,我的客户端(Mac/Windows)是一堆 loose scripts:手动跑 frpc 进程,再配合一个 Docker 里的 X-UI。
问题:依赖环境复杂(Python版本、FRP版本)、进程管理混乱、日志分散。

v3.0 方案
我将所有组件封装进了一个 docker-compose.yml 堆栈中。现在,Mac 和 Windows 客户端不再需要安装 Python 环境,不再需要手动下载 FRP。
只需一个命令

1
docker compose up -d

它会自动拉起:

  • X-UI (代理面板)
  • FRPC (内网穿透)
  • Sync Sidecar (配置同步)
    这三者在同一个 Docker 网络中互联,彻底解耦了宿主机环境。

架构红利:中心化控制 (Centralized Control)

基于 Docker 化带来的标准化环境,我终于能实现一直想做的Master-Slave 架构

  • 配置中心化
    以前配置分散在各个设备上。现在引入了 global.envmaster_config.json
    VPS 是唯一真理。Mac/Android 启动时会自动从 VPS 拉取最新的 UUID 和用户名单。
  • 自动同步
    利用 Docker 容器间的互通性,Sync Sidecar 容器会定期检查配置更新。
    Result: 在 VPS 改一次配置,全家所有设备重启即生效,无需逐台设备 SSH 进去修改。

稳定性提升:静态 IP 网络 (Static IPs)

为了解决 Nginx 反代 Docker 容器时常见的 502/Connection Refused 问题(容器重启 IP 变动导致 DNS 缓存失效或配置指向错误),我在 VPS 上实施了 Docker Internal Static IP 策略。

通过在 docker-compose.yml 中显式指定 IP,确保服务组件之间的通信链路绝对稳定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
networks:
vps_net:
ipam:
config:
- subnet: 10.88.0.0/16

services:
nginx:
networks:
vps_net:
ipv4_address: 10.88.0.4

frps:
networks:
vps_net:
ipv4_address: 10.88.0.2

这让 VPS 的内部网络拓扑固若金汤,无论怎么热更新容器,Nginx 永远知道去 10.88.0.2 找 FRP,去 10.88.0.x 找其他服务。

总结

v3.0 的改动量巨大(看 git log 满屏的 refactor 就知道了),但收益是长远的:

  • 部署:从 “照着文档操作10步” 变成了 “Copy 文件夹 -> Docker Compose Up”。
  • 维护:从 “SSH 到处修” 变成了 “改 Master Config -> 重启”。
  • 体验:配置的一致性得到了极大的保证,再也不用担心某个节点配置漏改导致连接失败。

This is how modern home proxy should be built.

将闲置 Android 手机变身为高性能代理节点

在致力于构建极致的高可用(HA)家庭代理网络的过程中,我意识到手头有一台性能强大、全天候联网却在“吃灰”的 ARM 设备:我的旧 Android 手机

通过将其作为一个“次级主节点(Secondary Primary Node)”集成到我的网络中,通过与 Mac 并行工作,我实现了一个“三活(Triple-Active)”架构:Mac + Android -> VPS 备份。本文将深入分享我是如何利用 Termux 将一台 Android 手机变成“哑节点(Dumb Node)”代理的,以及在此过程中解决的那些奇特的网络陷阱。


📱 概念:什么是“哑节点”?

不同于运行着完整 X-UI 面板和漂亮界面的 VPS 或 Mac,Android 手机受限于电池和散热资源。我并不想在上面运行沉重的 Web 面板和数据库。

我选择了**“无状态哑节点”架构**:

  1. 无 UI:只运行纯粹的 Xray-core 二进制文件。
  2. 硬编码配置:UUID 与 VPS 完全一致。
  3. 隧道:使用 frpc 将本地端口暴露给 VPS。

对于负载均衡器(Nginx)来说,它看起来和我的 Mac 一模一样。而对于手机来说,这只是两个轻量级的后台进程。


🛠 技术栈:Termux 足矣

忘掉 Root 刷机吧。Termux 提供了完全够用的用户空间 Linux 环境。

1
2
pkg install frp      # 注意:包名是 'frp',而不是 'frpc'
pkg install wget vim

对于代理核心,我直接下载了标准的 ARM64 (aarch64) 版 Xray-core。


🕳 踩坑记录(以及我是如何填坑的)

这并不是即插即用的。Android 的网络栈非常……独特。

1. 包名陷阱

如果你尝试在 Termux 里安装 frpc,会提示找不到。
纠正:软件包名称是 frp,安装后会同时包含 frpcfrps

2. “连接被拒绝”的幽灵

起初,Xray 日志看起来很干净,但流量就是不通。在 Android 上使用 netstat 需要 Root 权限才能看到有用的输出,这让调试变成了瞎子摸象。
原因:Xray 实际上并没有成功运行或监听在预期的接口上。
解决:我写了一个 start.sh 脚本来正确管理进程生命周期,并将日志转储到标准输出(stdout)以便调试,而不是丢进 /dev/null

3. DNS 黑洞 ([::1]:53)

这是最恶心的一个 Bug。Xray 日志显示:
dial tcp: lookup www.google.com on [::1]:53: read: connection refused

为什么?
Termux 并没有像 Go 语言(Xray 的开发语言)预期的那样拥有指向系统解析器的标准 /etc/resolv.conf。它试图使用本地回环 IPv6 地址进行 DNS 解析,但那里并没有 DNS 服务监听。

解决
在 Xray 的 config.json 中硬编码 DNS。不要依赖系统。

1
2
3
"dns": {
"servers": ["8.8.8.8", "1.1.1.1"]
}

4. IPv6 幻影

即使 DNS 修好了,连接依然超时。Android 系统倾向于优先使用 IPv6,但在许多移动网络或 WiFi 配置下,IPv6 路由并不稳定或被限制。

解决
强制 Xray 使用 IPv4 进行出站连接。

1
2
3
4
5
6
"outbounds": [
{
"protocol": "freedom",
"settings": { "domainStrategy": "UseIP" }
}
]

UseIP 策略会强制 Xray 将域名解析为 IP(使用我们硬编码的 8.8.8.8),然后直接连接该 IP,绕过系统路由配额的限制。


🔋 优化:电池 vs 性能

在电池供电的设备上 24/7 运行代理需要平衡。

  1. Wakelock(唤醒锁):必不可少。点击 Termux 通知栏的 “Acquire Wakelock”,防止 CPU 进入深度休眠。没有它,关屏 5 分钟后代理就会断连。
  2. 网络选择
    • 5G:速度快,低延迟,但发热大,耗电快。适合作为家庭宽带断网时的临时备份。
    • Wi-Fi:功耗低得多。作为永久设施时的推荐选择。
  3. 进程优先级:Android 会激进地杀掉后台应用。我必须将 Termux 锁定在“最近任务”视图中,并对其禁用电池优化。

🚀 最终效果

我现在拥有了一个冗余的代理节点,它几乎不耗电,零维护(无状态),而且可移动。

如果我的 Mac 崩溃了?流量切到 Android。
如果我家光纤断了?流量切到 Android (走 5G)。
如果所有东西都挂了?流量切到 VPS。

韧性(Resilience)不在于永不故障,而在于故障发生时无人察觉。

构建高可用家庭代理:深入解析 FRP 和 Nginx Stream 故障转移

在个人网络和家庭实验室(Home Lab)的世界里,安全地暴露本地服务或创建一个供家人使用的稳定代理是一个常见的挑战。最初只是一个简单的需求——“我需要一个能使用我干净家庭 IP 的代理给家人用”——最终演变成了一堂关于高可用性(HA)系统设计的课程。

本文将介绍我如何从不可靠的免费层解决方案迁移到利用 FRP (Fast Reverse Proxy)Nginx Stream 的健壮自托管架构,实现了在我的 Mac(主节点)和 VPS(备用节点)之间的零停机故障转移。

⚠️ 场景说明与免责声明

本文所探讨的技术仅用于合法的家庭网络远程访问网络稳定性优化
主要适用场景包括:

  1. 服务连续性保障(HA 需求来源):确保在家庭主服务器(Mac)进行维护、休眠或家庭宽带意外断网时,依赖该网络通道的关键服务(如远程监控、智能家居控制)仍能保持最低限度的可用性,避免单点故障。
  2. 家庭实验室回连:在外网环境下安全访问家中的 NAS、Home Assistant 或自建代码仓库。
  3. 开发环境统一:为移动办公设备提供统一的家庭宽带 IP,以便通过企业防火墙白名单或进行特定网络环境下的调试。
  4. 隐私保护:在公共 Wi-Fi 环境下加密流量,防止中间人攻击。

请勿将相关技术用于违反当地法律法规的用途。


🛑 问题:配额、封锁和不稳定性

我最初的设置依赖于 ngrok,后来是 Cloudflare Tunnel。这两者都是优秀的工具,但在我的特定用例中遇到了瓶颈:

  1. Ngrok:非常适合快速测试,但免费层有连接限制和动态域名。对于日常家庭使用来说,它不够“生产级”稳定。
  2. Cloudflare Tunnel:零信任(Zero-trust)很棒,但在我受限的网络环境中,它在处理特定的 UDP 流量和非标准端口时表现不佳。
  3. 仅本地运行:仅仅在我的 Mac 上运行代理意味着如果我合上笔记本电脑或网络断开,其他人的互联网也就“断”了。

我需要一个既快速(在可用时使用我的家庭带宽/IP)又可靠(当我的 Mac 离线时回退到 VPS)的解决方案。

🛠 解决方案:主备高可用架构

我设计了一个**主-备(Primary-Backup)**架构。

  • 主节点 (Primary - Mac):在本地运行代理。低延迟,“干净”的住宅 IP。
  • 备用节点 (Backup - VPS):廉价的云服务器。永远在线,作为故障保险。
  • “开关” (The Switch):运行在 VPS 上的 Nginx 负载均衡器。


graph TD
    User["客户端 (Family Devices)"] -->|"访问 VPS:10086"| Nginx["Nginx Stream 负载均衡"]
    
    subgraph VPS ["VPS Server (Cloud)"]
        direction TB
        Nginx
        Backup["备用代理 (Port 10088)"]
        FRPS["FRPS Server"]
    end
    
    subgraph Home ["Home Network"]
        direction TB
        FRPC["FRPC Client"]
        Mac["Mac 主代理 (Port 10086)"]
    end

    Nginx -->|"主路径 (Primary)"| FRPS
    FRPS <==>|"FRP 隧道 (:7000)"| FRPC
    FRPC -->|"转发流量"| Mac
    Mac -->|"访问互联网"| Internet1
    
    Nginx -.->|"故障转移 (Failover)"| Backup
    Backup -->|"访问互联网"| Internet2

    Internet1(("Internet"))
    Internet2(("Internet"))

    style Mac fill:#a7f3d0,stroke:#047857,stroke-width:2px
    style Backup fill:#fde68a,stroke:#d97706,stroke-width:2px
    style Nginx fill:#bfdbfe,stroke:#1d4ed8

1. 隧道:为什么选择 FRP?

由于我的 Mac 没有公网 IP,我需要一个反向隧道。我选择了 FRP 而不是其他工具,因为:

  • 协议支持:它完美支持 TCP/UDP,这对于代理流量至关重要(不像某些仅支持 HTTP 的隧道)。
  • 自托管:我控制服务器。没有第三方的速率限制。
  • 轻量级frpc(客户端)和 frps(服务端)都是单二进制文件。

配置
我的 Mac (frpc) 连接到 VPS (frps) 的 7000 端口,将其本地代理端口 (10086) 映射到 VPS 的 10087 端口。

2. 魔法所在:Nginx Stream 模块与故障转移

这是最大的技术学习点。标准 Nginx 以 HTTP 反向代理闻名,但对于原始 TCP 代理(如 VMess),我们需要 Stream 模块

我使用了 Nginx 的 upstream 模块配合 backup 参数来实现主备故障转移逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
stream {
upstream vmess_backend {
# 主节点:来自 Mac 的隧道端口
# max_fails=1 fail_timeout=3s: 如果失败一次,将其标记为不可用即刻生效,并在 3 秒内不再尝试
server 127.0.0.1:10087 max_fails=1 fail_timeout=3s;

# 备用节点:本地 VPS 代理
# 'backup': 仅在主节点宕机时使用
server 127.0.0.1:10088 backup;
}

server {
listen 10086; # 公网入口点
proxy_pass vmess_backend;
proxy_connect_timeout 2s;
}
}

工作原理:

  1. 客户端连接到 VPS:10086
  2. Nginx 尝试将流量转发到 10087(通往 Mac 的 FRP 隧道)。
  3. 正常路径:Mac 在线 → 流量通过隧道 → Mac -> 互联网。
  4. 故障路径:Mac 休眠/离线 → 10087 连接被拒绝。
  5. 故障转移:Nginx 立即检测到故障,并将流量路由到 10088(直接在 VPS 上运行的备份代理)。
  6. 恢复:每隔 3 秒(由 fail_timeout 定义),Nginx 会试探性地再次尝试主节点。如果 Mac 唤醒,流量会自动切回。

3. “无状态”的客户端体验

为了让终端用户(我的家人)感觉不到切换,我确保 Mac 代理和 VPS 备用代理共享完全相同的 UUID 和加密设置

对于客户端(例如 Shadowrocket)来说,它只看到“Server A”。它完全不知道底层的后端已经从西雅图的 Mac 切换到了洛杉矶的 VPS。

🚀 关键收获

1. 被动健康检查 > 主动探测

在简单的 TCP 设置中,你并不总是需要复杂的主动健康检查(ping 端点)。Nginx 的被动健康检查(监控实际的连接响应)极其高效。它能对连接拒绝做出即时反应。

2. “等待”的艺术 (fail_timeout)

调整 fail_timeout 是一门艺术。设置得太高,主节点恢复后你会停留在备用服务器上太久。设置得太低,不稳定的连接会导致“震荡”(快速切换)。3 秒 被证明是家庭网络环境的最佳平衡点。

3. 自托管赋予控制权

通过在这个特定用例中摆脱 Cloudflare Tunnel 等托管服务,我重新获得了传输层的控制权。我可以确切地看到连接在哪里失败——是 FRP 隧道、本地代理进程,还是 Nginx 路由。

总结

这个项目不仅仅是为了节省几美元的代理服务费。这是一次关于**弹性工程(Resiliency Engineering)**的实践练习。我们构建了一个能够优雅降级而不是灾难性崩溃的系统。

对于任何希望增强家庭实验室可访问性的人:不要只构建服务;构建一个备份。然后让 Nginx 处理剩下的事情。


使用的工具:FRP (v0.66.0), Nginx (1.26), Docker, X-UI

突发奇想,Github Pages 就可以直接访问网站,那么可以不用Cloudflare了,直接用Github Pages,这样就可以省下Cloudflare的用量了。

需要改的内容如下:

  • 去掉Cloudflare原来的DNS解析
  • Github Pages设置自定义域名
  • 在域名注册商那里设置Github Pages的域名解析
  • 添加Hexo deploy配置文件,指定Github Pages的仓库地址

换起来还是有点问题。再观察观察。

更新:没问题了,配置域名域名有延迟,不要来回换仓库。下一步是配置一下Github Actions,自动部署。现在是手动部署的。

换成Github Actions自动部署过程中,遇到了各种各样的问题:
首先要写workflow的脚本。更改了几版,最后的版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages

on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false

jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true # Checkout private submodules(themes or something else).

# Caching dependencies to speed up workflows. (GitHub will remove any cache entries that have not been accessed in over 7 days.)
- name: Cache node modules
uses: actions/cache@v3
id: cache
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 8
run_install: true

- name: Generate Content
run: pnpm run build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./dist

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

然后我发现我的theme主题不对……我用的theme-next/hexo-theme-next,这个已经好几年没更新了。但有个仓库是next-theme/hexo-theme-next
theme-next和next-theme……
然后我做了一下更换主题,我用的submodule的方式,要删除原本的主题,然后注意.git/config里的配置也要删掉,然后重新submodule add新的主题。
主题里还有点小问题是有个muse.js找不到,我就把schema里的muse改成Pisces

现在是把仓库的名字换成了username.github.io,之后会尝试其他的仓库名字,部署Github Pages。

0%