2026-07-15 · 在外链路已上线(锚点 ali)· 家内直连(P5/P6)待做 · 相关:Tailscale 公网访问(保留作应急后门)
git.yanmeiai.com(Forgejo)、nas.yanmeiai.com(DSM/Photos)、win.yanmeiai.com(Windows RDP/SSH)。| 对象 | 现状 | 与本方案的关系 |
|---|---|---|
| NAS(DS925+) | 192.168.3.200;跑 Forgejo(:3000 HTTP / :2222 SSH)、mihomo 透明代理(:53 fake-ip DNS / TUN)、Tailscale(100.95.151.13) | 装 frpc;mihomo 加 hosts 覆盖;Tailscale 作本次部署跳板 |
| Windows | 192.168.3.88,网关/DNS 均指向 NAS;有 OpenSSH server(ssh windows) | 基本不改;frpc 从 NAS 直接转发其 RDP/SSH |
| pangolin1 | 103.119.13.48,Debian 12,2C/1G;:443 sing-box REALITY、:8080 控制面(仅 127.0.0.1)、:9443 gRPC、cloudflared 出站隧道;ufw 默认拒入站 | 装 frps(~20MB 内存);7000/2222/3000/3389/5001/10022/10023 均空闲无冲突 |
| 家宽 | 有真实公网 IP(动态),路由器无端口映射 | 保持零开放端口,不做 DDNS |
| DNS | yanmeiai.com 在 Cloudflare(管理走 cf-api);git.51yanmei.com 公网 A 记录指 192.168.3.200(仅家内可达) | 新增 3 条 DNS-only A 记录;旧 git 域名可选迁移 |
选端口的原则:内外端口号一致(同一个 URL 全球通用)。唯一例外是裸 SSH——:22 在 pangolin1 上被它自己的 sshd 占用,只能错位到 10022/10023,由 ~/.ssh/config 别名消化(见 §7)。
| 服务 | 统一地址 | frpc 转发目标(家内) | pangolin1 remotePort | 备注 |
|---|---|---|---|---|
| Forgejo Git SSH | ssh://git@git.yanmeiai.com:2222 | 127.0.0.1:2222 | 2222 | 内外一致 ✓ |
| Forgejo Web | http://git.yanmeiai.com:3000 | 127.0.0.1:3000 | 3000 | 内外一致 ✓ |
| DSM / Photos | https://nas.yanmeiai.com:5001 | 127.0.0.1:5001 | 5001 | 内外一致 ✓;Photos App 填这个地址 |
| Windows RDP | win.yanmeiai.com:3389 | 192.168.3.88:3389 | 3389 | 内外一致 ✓ |
| NAS SSH | 外:nas.yanmeiai.com:10022家: 192.168.3.200:22 | 127.0.0.1:22 | 10022 | 端口错位,ssh alias 兜底 |
| Windows SSH | 外:win.yanmeiai.com:10023家: 192.168.3.88:22 | 192.168.3.88:22 | 10023 | 端口错位,ssh alias 兜底 |
不转发的:SMB/445(协议不适合过公网,文件走 Synology Drive/Photos 或 git);DSM 5000(明文 HTTP,只留 5001 HTTPS);mihomo 9090 管理口(有需要时临时经 NAS SSH 端口转发)。
pangolin1 的 ufw 默认拒绝入站。frps 的业务端口(2222/3000/3389/5001/10022/10023)一条 allow 规则都不加。你在外面走 pangolin 时,流量从 sing-box 出站连 103.119.13.48——目的地是本机自己的公网 IP,内核走 loopback 回环(hairpin)送达,而 ufw 对 lo 接口默认全放行。效果:这些端口只有「连着 pangolin 的你」能访问,公网扫描器看到的是全拒。RDP 这种绝不能裸奔的服务因此是安全的。
验证兜底:若实测 hairpin 未走 lo(§9 验收步骤 P1-4 会测),补一条 ufw allow from 103.119.13.48 等价实现,语义不变。
:7000 是唯一新开的公网端口(frpc 从家里拨入用,家宽 IP 动态、无法限源)。加固:auth.token(32 字节随机,存 Bitwarden)+ transport.tls.force = true(强制 TLS,未握手连接直接断)。
Forgejo 密钥登录、DSM 账号 2FA + 自动封锁、Windows RDP 强密码/NLA——原有认证全部保留,隧道只是网络通路。Tailscale 保留不动,作为 frp 链路故障时的应急后门。
机器:ssh pangolin1 · 人在外也能直接做(公网可达)
# 生成 32 字节随机 token,存进 Bitwarden(条目名:frp home-tunnel)
openssl rand -hex 32
rbw add "frp home-tunnel" # 密码字段贴上面的输出;两端配置从这里取,不落明文文件
# ssh pangolin1 之后(root)。VER 以 frp releases 最新版为准,下例为示意
VER=0.61.1
curl -LO https://github.com/fatedier/frp/releases/download/v${VER}/frp_${VER}_linux_amd64.tar.gz
tar xzf frp_${VER}_linux_amd64.tar.gz
install -m 755 frp_${VER}_linux_amd64/frps /usr/local/bin/frps
mkdir -p /etc/frp
/etc/frp/frps.toml(token 不入 git,与 /etc/pangolin* 密钥同规矩):
bindAddr = "0.0.0.0"
bindPort = 7000
auth.method = "token"
auth.token = "<Bitwarden: frp home-tunnel>"
transport.tls.force = true # 拒绝非 TLS 的 frpc
# 不开 dashboard;业务端口按 frpc 请求动态监听
allowPorts = [
{ start = 2222, end = 2222 }, { start = 3000, end = 3000 },
{ start = 3389, end = 3389 }, { start = 5001, end = 5001 },
{ start = 10022, end = 10023 },
] # 白名单锁死,token 泄露也开不了别的口
/etc/systemd/system/frps.service:
[Unit]
Description=frp server (home reverse tunnel)
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=always
RestartSec=5
User=nobody # 非特权端口,无需 root
LimitNOFILE=65536
# 1G 小机安全带:内存超限就重启,绝不拖垮 sing-box/控制面
MemoryMax=128M
[Install]
WantedBy=multi-user.target
chmod 600 /etc/frp/frps.toml && chown nobody /etc/frp/frps.toml
systemctl daemon-reload && systemctl enable --now frps
ufw allow 7000/tcp comment 'frp control (frpc dial-in from home)'
# 验证:只多了 7000 这一个对外口
systemctl status frps --no-pager | head -5
ss -tlnp | grep frps
⚠️ 台账提醒:pangolin1 续费到期 2026-07-19(baize billing.renew_at)——动工前先确认已续费。
必须 DNS only(灰云,proxied:false)——git SSH / RDP 是任意 TCP,CF 橙云只代理 HTTP。zone id 见 baize:2325730de45276d87180a8b66bd4cca0。
cf-api -X POST "/zones/2325730de45276d87180a8b66bd4cca0/dns_records" \
--data '{"type":"A","name":"nas","content":"103.119.13.48","proxied":false,"ttl":300}'
cf-api -X POST "/zones/2325730de45276d87180a8b66bd4cca0/dns_records" \
--data '{"type":"A","name":"git","content":"103.119.13.48","proxied":false,"ttl":300}'
cf-api -X POST "/zones/2325730de45276d87180a8b66bd4cca0/dns_records" \
--data '{"type":"A","name":"win","content":"103.119.13.48","proxied":false,"ttl":300}'
代价说明:灰云记录会暴露 pangolin1 的 IP——它本来就是 VPN 数据面入口(客户端 endpoint 直连该 IP),无新增暴露。改完回写 baize domains.yaml。
/volume1/docker/proxy/mihomo/config.yaml 两处改动(仓库 docker/proxy/mihomo/config.yaml 同步改):
hosts: # ① 家内答案:直接给内网 IP
nas.yanmeiai.com: 192.168.3.200
git.yanmeiai.com: 192.168.3.200
win.yanmeiai.com: 192.168.3.88
git.51yanmei.com: 192.168.3.200 # 兼容既有 git remote(可选,见 §10)
dns:
fake-ip-filter: # ② 追加:不给这几个域名发 fake-ip,走 hosts 真实值
# ……既有条目保持不动(含 +.tailscale.com 等)……
- nas.yanmeiai.com
- git.yanmeiai.com
- win.yanmeiai.com
- git.51yanmei.com
# 热重载(不重启容器),沿用仓库既有做法
ssh nas "curl -s -X PUT 'http://127.0.0.1:9090/configs?force=true' \
-H 'Content-Type: application/json' \
-d '{\"path\":\"/root/.config/mihomo/config.yaml\"}'"
# 验证:家内解析应返回 192.168.3.200
ssh nas "nslookup git.yanmeiai.com 127.0.0.1"
Windows 已手动指 NAS;要让 Mac/手机在家也吃到 hosts 覆盖,把路由器 DHCP 下发的 DNS 改为 192.168.3.200(一处改动、全家生效)。取舍:NAS 由此成为家内 DNS 单点——不要配第二个公共 DNS 兜底(客户端会随机用它,覆盖时灵时不灵);NAS 重启的几分钟内家里断解析,可接受。
仓库新增:docker/frp/docker-compose.yml + docker/frp/frpc.toml + docker/frp/.env.example · NAS 路径:/volume1/docker/frp/
沿用本仓库 docker-compose 惯例(forgejo/proxy 同款)。wangjia 不在 docker 组,部署用 sudo 或 Container Manager UI。
docker-compose.yml:
services:
frpc:
image: snowdreamtech/frpc:0.61 # 多架构维护镜像;版本与 frps 对齐
container_name: frpc
network_mode: host # 需直达 127.0.0.1 各服务 + 192.168.3.88
restart: unless-stopped
volumes:
- ./frpc.toml:/etc/frp/frpc.toml:ro
frpc.toml(token 同样不入 git,仓库放 frpc.toml.example 占位):
serverAddr = "103.119.13.48"
serverPort = 7000
auth.method = "token"
auth.token = "<Bitwarden: frp home-tunnel>"
transport.tls.enable = true
[[proxies]]
name = "git-ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 2222
remotePort = 2222
[[proxies]]
name = "git-web"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3000
remotePort = 3000
[[proxies]]
name = "dsm"
type = "tcp"
localIP = "127.0.0.1"
localPort = 5001
remotePort = 5001
[[proxies]]
name = "nas-ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 10022
[[proxies]]
name = "win-rdp"
type = "tcp"
localIP = "192.168.3.88" # 同网段代转 Windows,一个 frpc 管全家
localPort = 3389
remotePort = 3389
[[proxies]]
name = "win-ssh"
type = "tcp"
localIP = "192.168.3.88"
localPort = 22
remotePort = 10023
# 部署(经 Tailscale 跳板 ssh 到 NAS 后)
sudo mkdir -p /volume1/docker/frp && cd /volume1/docker/frp
# (把仓库里的两个文件放进来,frpc.toml 填真 token)
sudo docker compose up -d
sudo docker logs frpc --tail 20 # 应见 "login to server success" + 6 个 proxy 启动成功
~/.ssh/config)# 既有 Host nas / windows(内网 IP 直连)保持不动,家里继续用
Host nas-r # 在外用:ssh nas-r
HostName nas.yanmeiai.com
Port 10022
User wangjia
Host win-r # 在外用:ssh win-r
HostName win.yanmeiai.com
Port 10023
User wangjia
| 项 | 动作 | 说明 |
|---|---|---|
| 网络 | 不改 | 网关/DNS 已指 NAS,天然吃到 hosts 覆盖 |
| RDP | 确认已启用(设置 → 系统 → 远程桌面),保持 NLA 开启 | frpc 从 NAS(同网段 .200)连入,默认防火墙「专用网络」范围即放行 |
| OpenSSH server | 已有(ssh windows 在用),确认服务设为自动启动 | 同上,无需改防火墙 |
| 防火墙 | 仅当连不通时检查:RDP/SSH 入站规则的作用域含本地子网 | 不需要也不要开「任意来源」 |
代码:server/internal/httpapi/clientconfig.go::BuildClientConfig · 单独一刀,四端零改动(配置服务端渲染下发)
pangolin TUN 的首条路由规则是 hijack-dns,所有 DNS 都被劫去隧道解析——家里也会拿到公网 IP、绕道外网。解法是给这几个域名开「本地解析 + 按结果路由」的侧门:
// 伪代码示意。域名清单走 server 配置(env PRIVATE_SPLIT_DOMAINS),默认空 = 行为不变,
// 不影响其他用户。
// ① dns.servers 增本地解析器
{ "tag": "dns-local", "address": "local", "detour": "direct" }
// ② dns.rules 前插:这 3 个域名用本地 DNS(家=mihomo hosts→内网IP;外=公网DNS→pangolin1)
{ "domain": ["nas.yanmeiai.com","git.yanmeiai.com","win.yanmeiai.com"], "server": "dns-local" }
// ③ route.rules 在 hijack-dns 之后插:先解析再按 IP 匹配后续规则
{ "domain": ["…同上…"], "action": "resolve" }
// ④ 解析结果为私网 → 命中既有 LAN 直连规则 → 家里直连;
// 公网 IP(pangolin1)→ 不匹配 → 走默认代理出口 → 机内 hairpin 到 frps
实现时必须验证:action: resolve 是 sing-box ≥ 1.11 语法——先确认四端内嵌 libbox 版本;不够就先升 libbox(scripts/build-libbox.sh 重编,产物 gitignore 需各端重新放置)。按仓库惯例先在 macOS 端端到端验证,再推 iOS/Android/Windows。
此刀未落地前的过渡用法:在外一切正常(隧道 DNS 拿公网 IP 本来就该走隧道);在家访问这几个域名会绕外网一圈(功能可用、不满足目标①),或临时关 pangolin 直连。
P0 · 前置(5 分钟)
P1 · pangolin1 装 frps(直接可做,§5.2)
ufw allow 7000/tcpss -tlnp | grep frps 只见 7000;P1-4 hairpin 实测——pangolin1 本机 curl --max-time 3 http://103.119.13.48:7000 通(说明本机对自己公网 IP 走 lo),外网机器(如 ali)nc -zw3 103.119.13.48 2222 必须不通P2 · Cloudflare 加 3 条灰云 A 记录(直接可做,§6.1)
dig +short git.yanmeiai.com @1.1.1.1 → 103.119.13.48P3 · 经 Tailscale 跳板配 NAS(§6.2 + §7)
ssh wangjia@100.95.151.13:部署 frpc compose、改 mihomo(hosts + fake-ip-filter)并热重载docker logs frpc 见 6 个 proxy 成功;nslookup git.yanmeiai.com 127.0.0.1 → 192.168.3.200P4 · 在外端到端验收(pangolin 开着)
git ls-remote ssh://git@git.yanmeiai.com:2222/wangjia/nas.git ✓http://git.yanmeiai.com:3000 出 Forgejo 登录页 ✓https://nas.yanmeiai.com:5001 出 DSM(证书告警属预期,见 §12 增强)✓win.yanmeiai.com ✓;ssh nas-r / ssh win-r ✓P5 · 回家后收尾(§6.3)
dig +short git.yanmeiai.com → 192.168.3.200;关 pangolin 时 git ls-remote/DSM 直连均通且 traceroute 一跳P6 · pangolin 分流规则(单独一刀,§9)
BuildClientConfig + 单测 → macOS 端在家实测「pangolin 开着直连内网」→ 推其余三端P7 · 台账与文档回写
domains.yaml(3 条新记录 + expected_resolution);nas 仓 CLAUDE.md(frp 服务 + 端口清单);~/.ssh/config(nas-r/win-r);todo 登记| 风险 | 影响 | 缓解 / 回滚 |
|---|---|---|
| NAS 成家内 DNS 单点 | NAS 挂 → 全家断解析 | 接受(NAS 稳定);应急把路由器 DHCP DNS 改回运营商 |
| token 泄露 | 他人可向 frps 注册代理 | allowPorts 白名单锁死端口范围;业务端口本就 ufw 隐形;换 token 重启两端即失效 |
| frps 占用拖累 1G 小机 | 控制面/数据面受影响 | systemd MemoryMax=128M,超限自杀重启,隔离爆炸半径 |
| hairpin 假设不成立 | 在外连不上业务端口 | P1-4 提前实测;兜底 ufw allow from 103.119.13.48 |
| pangolin1 迁移/换 IP | 公网侧全断 | frps 配置随 deploy 脚本走;改 3 条 A 记录即恢复;家内直连不受影响 |
| libbox 版本不支持 resolve | P6 延期 | P1–P5 不依赖 P6;过渡期家里关 pangolin 或接受绕行 |
| 整体回滚 | — | NAS:docker compose down;pangolin1:systemctl disable --now frps + 删 ufw 7000;CF 删 3 条记录;mihomo 还原备份重载。全程不动业务服务 |
git.51yanmei.com 公网 A 记录(阿里云)从 192.168.3.200 改指 103.119.13.48——既有仓库 remote 在外也能用,家内靠 §6.2 的 hosts 覆盖。或者反过来:全量迁 remote 到 git.yanmeiai.com,旧域名退役。nas.yanmeiai.com 签证书,消掉 5001 的浏览器告警。git.yanmeiai.com(对应 CLAUDE.md 里既有 TODO),web 页面上的 clone 地址就会是新域名。