家宽没固定公网 IPv4 也能稳定挂 5 个 Web 服务(Cloudflare 307 + Lucky STUN 双栈方案)

写在前面,全文由 ANY大善人的opus4.7主导 IPV4 NEWAPI IPV6NEW直连 佬友们好。分享一下我这边的家宽双栈对外方案,脱敏整理出来给有同样需求的同学参考。 我家这边情况大概是: 广东移动运营商给了公网 IPv6,但 IPv4 是大内网 NAT1,没固定公网 IPv4 想把家里 ...
家宽没固定公网 IPv4 也能稳定挂 5 个 Web 服务(Cloudflare 307 + Lucky STUN 双栈方案)
家宽没固定公网 IPv4 也能稳定挂 5 个 Web 服务(Cloudflare 307 + Lucky STUN 双栈方案)

写在前面,全文由 ANY大善人的opus4.7主导

IPV4 NEWAPI

image

IPV6NEW直连

image

佬友们好。分享一下我这边的家宽双栈对外方案,脱敏整理出来给有同样需求的同学参考。

我家这边情况大概是:

  • 广东移动运营商给了公网 IPv6,但 IPv4 是大内网 NAT1,没固定公网 IPv4
  • 想把家里 5 个 Web 服务(api / cpa / yx / code / claw)和 1 个 VPN 挂到公网
  • 要兼顾 公司 IPv4 (办公网、IPv4-only 环境)和 IPv6 用户
  • 不想为这事单租 VPS

折腾完之后稳定跑了一阵,整理出来发上来。所有真实域名、公网 IP、动态端口、密钥都用 <...> 占位符替换了,自己用的时候换成真值即可。

整套方案的免费成本:Cloudflare DNS + 307 Redirect Rule 在免费计划够用,Lucky 是开源的。零 VPS 开支。

一、整体思路

核心三招:

  1. 双域名分离:入口域名(橙云)和落地域名(灰云)分开。入口只做 307 重定向,不直连后端;落地域名灰云直接 DNS 解析到家里。
  2. 双栈分流:IPv4 客户端被 307 重定向到 service.stun.<ROOT_DOMAIN>:<STUN_TCP4_PORT>(家宽 IPv4 不能直 443,只能走 Lucky STUN 拿到的动态端口);IPv6 客户端被 307 重定向到 service.stun.<ROOT_DOMAIN>(默认 443,直连 OpenWrt 公网 IPv6)。
  3. VPN 独立:VPN 不走 307,因为 WireGuard 不理解 HTTP 重定向。VPN 用独立的灰云 AAAA 直连。

二、主链路图

                                      ┌────────────────────────────────────┐
                                      │ Client / Browser / App             │
                                      │ service.<ROOT_DOMAIN>              │
                                      └──────────────────┬─────────────────┘
                                                         │
                                      ┌──────────────────▼─────────────────┐
                                      │ Cloudflare                         │
                                      │ proxied entry records              │
                                      │ Dynamic Redirect, HTTP 307         │
                                      └──────────────────┬─────────────────┘
                                                         │
        ┌────────────────────────────────────────────────┼────────────────────────────────────────────────┐
        │                                                │                                                │
┌───────▼────────────────────┐                ┌──────────▼──────────────────┐                 ┌──────────▼──────────────────┐
│ IPv4 client                │                │ IPv6 client                 │                 │ VPN client                  │
│ target should include port │                │ target uses default 443     │                 │ no HTTP redirect            │
└───────┬────────────────────┘                └──────────┬──────────────────┘                 └──────────┬──────────────────┘
        │ 307 Location:                                  │ 307 Location:                                  │
        │ https://service.stun.<ROOT_DOMAIN>:<STUN_PORT>/path │ https://service.stun.<ROOT_DOMAIN>/path   │
        │                                                │                                                │
┌───────▼────────────────────┐                ┌──────────▼──────────────────┐                 ┌──────────▼──────────────────┐
│ DNS-only A                 │                │ DNS-only AAAA               │                 │ DNS-only AAAA               │
│ service.stun.<ROOT_DOMAIN> │                │ service.stun.<ROOT_DOMAIN>  │                 │ vpn.<ROOT_DOMAIN>           │
│ -> STUN public IPv4        │                │ -> OpenWrt public IPv6      │                 │ -> OpenWrt public IPv6      │
└───────┬────────────────────┘                └──────────┬──────────────────┘                 └──────────┬──────────────────┘
        │                                                │                                                │
┌───────▼────────────────────┐                ┌──────────▼──────────────────┐                 ┌──────────▼──────────────────┐
│ Lucky STUN tcp4            │                │ OpenWrt                     │                 │ OpenWrt VPN service         │
│ public IPv4:<STUN_PORT>    │                │ Lucky HTTPS :443            │                 │ WireGuard/OpenVPN/etc.      │
│ -> Lucky HTTPS :18443      │                │ cert *.stun.<ROOT_DOMAIN>   │                 │ protocol-specific port      │
└───────┬────────────────────┘                └──────────┬──────────────────┘                 └─────────────────────────────┘
        │                                                │
        └───────────────────────────────┬────────────────┘
                                        │
                         ┌──────────────▼───────────────┐
                         │ OpenWrt Lucky reverse proxy  │
                         │ same Host rules on 443/18443 │
                         │ service.stun.<ROOT_DOMAIN>   │
                         │ -> backend                   │
                         └──────────────┬───────────────┘
                                        │
        ┌───────────────────────────────┼───────────────────────────────┬───────────────────────────────┐
        │                               │                               │                               │
┌───────▼────────────────────┐ ┌────────▼───────────────────┐ ┌────────▼───────────────────┐ ┌────────▼───────────────────┐
│ api.stun.<ROOT_DOMAIN>     │ │ cpa.stun.<ROOT_DOMAIN>     │ │ yx.stun.<ROOT_DOMAIN>      │ │ code.stun.<ROOT_DOMAIN>    │
│ -> <APP_NODE_A>:8881       │ │ -> <APP_NODE_A>:8317       │ │ -> <APP_NODE_A>:5001       │ │ -> <APP_NODE_B>:19080      │
│ NewAPI                     │ │ CPA / Proxy API            │ │ YX Web                     │ │ code-server                │
└────────────────────────────┘ └────────────────────────────┘ └────────────────────────────┘ └────────────────────────────┘
                                        │
                         ┌──────────────▼───────────────┐
                         │ claw.stun.<ROOT_DOMAIN>      │
                         │ -> <APP_NODE_B>:18789        │
                         │ OpenClaw Gateway             │
                         └──────────────────────────────┘

解读:

  • 客户端先打入口域名(橙云),被 Cloudflare 307 到对应的 stun 落地域名
  • IPv4 客户端拿到 https://service.stun.<ROOT_DOMAIN>:<STUN_TCP4_PORT>/...,解析到 Lucky STUN 公网 IPv4:动态端口,进 Lucky 反代
  • IPv6 客户端拿到 https://service.stun.<ROOT_DOMAIN>/...(443),解析到 OpenWrt 公网 IPv6,进 Lucky 反代
  • 两条路最后都汇到 OpenWrt Lucky 反代,按 Host 把请求转给内网真正的后端服务
  • VPN 走自己那条路,跟 HTTP 没关系

三、DNS 怎么设

入口域名(5 个)—— 橙云

api.<ROOT_DOMAIN>    A    proxied=true   TTL=auto
cpa.<ROOT_DOMAIN>    A    proxied=true   TTL=auto
yx.<ROOT_DOMAIN>     A    proxied=true   TTL=auto
code.<ROOT_DOMAIN>   A    proxied=true   TTL=auto
claw.<ROOT_DOMAIN>   A    proxied=true   TTL=auto

为什么橙云:必须橙云,Cloudflare 只有收到 HTTP 请求后才能执行 Redirect Rule。灰云的话客户端绕过 CF 直接打到 A 记录,Redirect Rule 根本触发不了。入口的 A content 没那么重要(反正是 CF 边缘 IP 收的),不需要指向家里。

落地域名(5 个 + 1 个 VPN)—— 灰云

api.stun.<ROOT_DOMAIN>    A=<STUN_PUBLIC_IPV4>   AAAA=<OPENWRT_PUBLIC_IPV6>   proxied=false  TTL=60
cpa.stun.<ROOT_DOMAIN>    A=<STUN_PUBLIC_IPV4>   AAAA=<OPENWRT_PUBLIC_IPV6>   proxied=false  TTL=60
yx.stun.<ROOT_DOMAIN>     A=<STUN_PUBLIC_IPV4>   AAAA=<OPENWRT_PUBLIC_IPV6>   proxied=false  TTL=60
code.stun.<ROOT_DOMAIN>   A=<STUN_PUBLIC_IPV4>   AAAA=<OPENWRT_PUBLIC_IPV6>   proxied=false  TTL=60
claw.stun.<ROOT_DOMAIN>   A=<STUN_PUBLIC_IPV4>   AAAA=<OPENWRT_PUBLIC_IPV6>   proxied=false  TTL=60
vpn.<ROOT_DOMAIN>                                AAAA=<OPENWRT_PUBLIC_IPV6>   proxied=false  TTL=60

为什么灰云 + TTL 60:307 之后客户端要直连家里 Lucky,必须灰云让 DNS 真实解析过去。TTL 60 是为了 STUN 公网地址变化时尽快收敛(用 LuckyDDNS 自动更新 DNS A 记录)。5 个落地域名的 A 都指向 Lucky STUN 探出来的公网 IPv4,AAAA 都指向 OpenWrt 公网 IPv6,这样一张通配证书就能盖住。

四、Cloudflare Redirect Rule

Cloudflare 控制台 → Rules → Redirect Rules,建一个 Dynamic Redirect Ruleset,phase 是 http_request_dynamic_redirect。整体设置:

status_code: 307
preserve_query_string: true

为什么 307:307 会保留 HTTP 方法(POST/PUT/PATCH),API 调用、表单提交、code-server 的 PUT 都不会被吃掉。301/302 在某些客户端会把 POST 改成 GET,直接坑死。

为什么 preserve_query_string:不开的话 ?token=...?folder=... 这种查询参数全丢,API 和 code-server 直接报错。

下面三条规则,按顺序放。

规则 1:cpa 根路径补全

match:
http.host == "cpa.<ROOT_DOMAIN>" and path == "/"

target:
https://cpa.<ROOT_DOMAIN>/management.html

为什么这么设:cpa 的根路径默认不会跳到管理页,先把 / 补成 /management.html,后面通用规则继续接管把它跳到 stun 落地域名。

规则 2:IPv4 入口加 STUN 端口

match:
http.host in {api.<ROOT_DOMAIN>, cpa.<ROOT_DOMAIN>, yx.<ROOT_DOMAIN>, code.<ROOT_DOMAIN>, claw.<ROOT_DOMAIN>}
and ip.src in 0.0.0.0/0

target expression:
wildcard_replace(
  http.request.full_uri,
  "*://*.<ROOT_DOMAIN>/*",
  "https://${2}.stun.<ROOT_DOMAIN>:<STUN_TCP4_PORT>/${3}"
)

为什么这么设:ip.src in 0.0.0.0/0 是 IPv4-only 匹配。家宽 IPv4 走不了 443,必须把 Lucky STUN 当前公网端口写到 Location 里。<STUN_TCP4_PORT>动态值,要靠脚本或 Webhook 同步过来(见第九节坑 2)。

规则 3:IPv6 入口直接 443

match:
http.host in {api.<ROOT_DOMAIN>, cpa.<ROOT_DOMAIN>, yx.<ROOT_DOMAIN>, code.<ROOT_DOMAIN>, claw.<ROOT_DOMAIN>}
and not ip.src in 0.0.0.0/0

target expression:
wildcard_replace(
  http.request.full_uri,
  "*://*.<ROOT_DOMAIN>/*",
  "https://${2}.stun.<ROOT_DOMAIN>/${3}"
)

为什么这么设:IPv6 客户端可以直达家里 OpenWrt 公网 IPv6 的 443,不需要动态端口,省事。

五、Lucky 配置

STUN tcp4

类型: tcp4
Target: <OPENWRT_LAN_IP>:18443  (指向 Lucky 自己的 HTTPS 监听口,不是 443)
PublicAddr (Lucky 探出来): <STUN_PUBLIC_IPV4>:<STUN_TCP4_PORT>

为什么 Target 是 18443 不是 443:家宽 IPv4/443 实测不通,IPv4 数据面只能走 Lucky 自己单独起的 HTTPS 监听口 18443。443 留给 IPv6 直连用。

端口同步:STUN 端口是运营商分配的,动态值。Lucky 探到新端口后必须同步到 Cloudflare Redirect Rule 的规则 2。两种方式:

  • Lucky Webhook → 调用 Cloudflare API PATCH Ruleset
  • 外部脚本轮询 Lucky API → PATCH Ruleset

同步完后必须用 curl -4 -I 从外部复测 Location(见第九节坑 3,API 和边缘可能短暂不一致)。

HTTPS 双监听

Lucky 反代要同时挂两个 HTTPS 监听口:

监听 1: <OPENWRT_PUBLIC_IPV6>:443    给 IPv6 客户端直连
监听 2: <OPENWRT_LAN_IP>:18443       给 STUN tcp4 把 IPv4 流量打进来

两个口共用同一组反代规则,因为 IPv4 和 IPv6 路径最后到 Lucky 时 Host 都是 *.stun.<ROOT_DOMAIN>

通配证书

类型: 通配证书 *.stun.<ROOT_DOMAIN>
签发方式: DNS-01
DNS Provider: Cloudflare (用 API Token,只给 Zone DNS Edit 权限)

为什么 DNS-01:HTTP-01 要 80/443 开放给 ACME,家宽 80/443 路径本来就不全通,DNS-01 不依赖入站端口,能自动续签。一张通配盖住 5 个 stun 落地域名。

反代规则

每个 stun 落地 Host 对一个内网后端:

api.stun.<ROOT_DOMAIN>   ->  http://<APP_NODE_A>:8881    (NewAPI)
cpa.stun.<ROOT_DOMAIN>   ->  http://<APP_NODE_A>:8317    (CPA / Proxy API)
yx.stun.<ROOT_DOMAIN>    ->  http://<APP_NODE_A>:5001    (YX Web)
code.stun.<ROOT_DOMAIN>  ->  http://<APP_NODE_B>:19080   (code-server)
claw.stun.<ROOT_DOMAIN>  ->  http://<APP_NODE_B>:18789   (OpenClaw Gateway)

公共选项:

前端协议: https
后端协议: http              (TLS 在 Lucky 终止,后端走明文省事,不用每个后端搞证书)
WebSocket: 开                (code-server 这种长连接工具必须开)
自动反代重定向: 关           (避免后端 Location 头被二次改写,排障痛苦)
访问日志: 开                  (出问题方便看)

为什么 TLS 在 Lucky 终止:证书、SNI、续签集中维护一处,后端跑明文 HTTP 反而干净。

六、认证保护

公网第一道认证放在 Lucky,不放在 Cloudflare 入口层(CF 只做 307 不挡):

api.stun.<ROOT_DOMAIN>     Lucky Basic Auth: 关     后端自己有登录页 (NewAPI)
cpa.stun.<ROOT_DOMAIN>     Lucky Basic Auth: 开     后端通过 Basic Auth 后到 CPA API
yx.stun.<ROOT_DOMAIN>      Lucky Basic Auth: 开     后端通过 Basic Auth 后跳 /login
code.stun.<ROOT_DOMAIN>    Lucky Basic Auth: 开     code-server 后端 auth=none
claw.stun.<ROOT_DOMAIN>    Lucky Basic Auth: 开     后端是 OpenClaw Gateway UI

为什么 api 不开:NewAPI 自带账号系统,再叠一层 Basic Auth 反而碍事。

为什么其他都开:cpa/yx 是私有管理类入口,code-server 直接是命令执行入口(裸露公网 = 送服务器),claw 是网关 UI。统一拿 Lucky Basic Auth 当公网第一道挡板,简单粗暴。

为什么 code-server 设 auth=none:code-server 自带密码方案不够灵活,统一交给前置 Basic Auth 处理。前提是 Lucky Basic Auth 必须开,否则 code-server 在公网完全没保护。

顺嘴提一下:我账号下还有 2 个 Cloudflare Access SaaS/OIDC App,但那是给 Lucky 自己做第三方登录用的回调端点,不是业务入口拦截页。业务入口前没有 CF Access。

七、VPN 为什么不能套 307

vpn.<ROOT_DOMAIN>    AAAA -> <OPENWRT_PUBLIC_IPV6>    灰云

直接灰云 AAAA 指向 OpenWrt 公网 IPv6,按 VPN 自己协议端口(WireGuard / OpenVPN / IPsec)连。

为什么不能走 307:307 是 HTTP 状态码,只有浏览器/curl 会跟随 Location。WireGuard/OpenVPN 客户端根本不解析 HTTP,把 VPN 域名扔进 Cloudflare 橙云 → 客户端连不上 → 超时。VPN、SSH、其他非 HTTP 协议都不要套 Cloudflare 307。

八、验收 curl

每条命令独立可复制粘贴,把 <...> 换成真值即可。

# 入口域名是不是返回 307
curl -I https://api.<ROOT_DOMAIN>/some-path

# IPv4 Location 必须带 :<STUN_TCP4_PORT>
curl -4 -I https://api.<ROOT_DOMAIN>/some-path

# IPv6 Location 应该不带端口
curl -6 -I https://api.<ROOT_DOMAIN>/some-path

# IPv4 STUN 落地是否可用
curl -4 -k -I https://api.stun.<ROOT_DOMAIN>:<STUN_TCP4_PORT>/

# IPv6 443 落地是否可用
curl -6 -k -I https://api.stun.<ROOT_DOMAIN>/

# Lucky Host 规则是否命中 (LAN 内测)
curl -k -H "Host: api.stun.<ROOT_DOMAIN>" https://<OPENWRT_LAN_IP>:18443/

# Basic Auth 是否生效 (期望 401 + WWW-Authenticate)
curl -I https://code.stun.<ROOT_DOMAIN>:<STUN_TCP4_PORT>/

九、踩过的坑

坑 1:IPv4 走不了 443

家宽 IPv4 是大内网 NAT,公网 443 不通。一开始以为 IPv4 也能走 443,折腾半天 timeout。最后老老实实用 Lucky STUN tcp4 拿一个动态公网端口,把 Cloudflare Redirect 的 Location 写成 :<STUN_TCP4_PORT> 才通。

坑 2:STUN 端口是动态的

Lucky STUN PublicAddr 的端口运营商会换。如果 Cloudflare Redirect Rule 里写死旧端口,IPv4 用户会被重定向到失效端口,表现就是"网页打不开"。必须做端口同步:Lucky Webhook 或外部脚本 PATCH Cloudflare Ruleset,同步完用 curl -4 -I 验证 Location 是否真的带新端口。

坑 3:Cloudflare API 配置和边缘行为可能短暂不一致

PATCH 完 Ruleset,Cloudflare API 返回的 target_expression 显示带端口,但边缘 HTTPS 实际返回的 Location 没带端口,IPv4 用户继续 timeout。这种漂移可能持续几分钟。别只看 API 返回值,必须从外部 curl -4 -I 看最终 Location 才算数。

坑 4:Lucky 404 不一定是后端挂了

直接访问 Lucky 监听口或 Host 不匹配会返回 Lucky 自己的 404/Warning 页。第一反应别去重启后端,先确认 Host 头是不是命中了反代规则:

curl -k -H "Host: api.stun.<ROOT_DOMAIN>" https://<OPENWRT_LAN_IP>:18443/

加上正确 Host 才能命中规则。

坑 5:VPN 不能套 Cloudflare 307

前面讲过:VPN 客户端不理解 HTTP 重定向,必须独立灰云 AAAA 直连,按 VPN 自己协议处理。

坑 6:排障先分清 timeout / 404 / 401

  • timeout:网络层问题(端口没开、IPv4/443 路径、NAT 失效、防火墙)
  • 404:Host 没命中 Lucky 反代规则(多半是 Cloudflare Redirect 写错了 stun 域名,或者直接打了 Lucky 监听口没带 Host)
  • 401:规则命中了,Lucky Basic Auth 在工作,这是正常表现

别把 401 当成"挂了"去重启服务。

十、完整脱敏抽象架构图

                           ┌────────────────────┐
                           │ client             │
                           └─────────┬──────────┘
                                     │
                           ┌─────────▼──────────┐
                           │ Cloudflare orange  │
                           │ HTTP 307 only      │
                           └─────────┬──────────┘
                                     │
             ┌───────────────────────┴───────────────────────┐
             │                                               │
   ┌─────────▼──────────┐                         ┌──────────▼─────────┐
   │ IPv4 path          │                         │ IPv6 path          │
   │ stun host + port   │                         │ stun host + 443    │
   └─────────┬──────────┘                         └──────────┬─────────┘
             │                                               │
   ┌─────────▼──────────┐                         ┌──────────▼─────────┐
   │ Lucky STUN tcp4    │                         │ OpenWrt IPv6       │
   │ public:<port>      │                         │ Lucky HTTPS 443    │
   └─────────┬──────────┘                         └──────────┬─────────┘
             │                                               │
             └───────────────────────┬───────────────────────┘
                                     │
                           ┌─────────▼──────────┐
                           │ Lucky reverse proxy│
                           │ Host based routing │
                           └─────────┬──────────┘
                                     │
        ┌────────────────────────────┼────────────────────────────┐
        │                            │                            │
┌───────▼────────┐          ┌────────▼───────┐          ┌─────────▼──────┐
│ service A      │          │ service B      │          │ service C      │
│ node A:port    │          │ node A:port    │          │ node B:port    │
└────────────────┘          └────────────────┘          └────────────────┘

写在最后

整套方案核心就这几条:

  1. 入口必须橙云,落地必须灰云
  2. IPv4 走 STUN 端口,IPv6 直接 443,别假设 IPv4 能走 443
  3. 307 + preserve_query_string,API 和长连接才不挂
  4. VPN 独立,别套 307
  5. 认证放在 Lucky,不放在 CF 入口
  6. STUN 端口动态值,必须同步 + 外部复测

所有 <...> 占位符替换成真实值再用。 真实域名、公网 IP、STUN 端口、Cloudflare Zone ID、API Token、Lucky 管理凭据、Basic Auth 账号密码、TOTP 种子、origin 旁路密钥这类敏感信息绝对不要贴论坛,包括截图里也要打码。

有问题评论区交流,佬友们如果有更优雅的 STUN 端口同步方案也欢迎贴出来。

2 个帖子 - 2 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文