@@ -0,0 +1,435 @@
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > 家庭内网远程访问方案 —— frp 反向隧道 × pangolin × 分流 DNS< / title >
< style >
: root {
--bg : #0f1117 ; --panel : #171a22 ; --panel2 : #1d2129 ; --fg : #e6e8ee ; --fg2 : #a8afbd ;
--accent : #e0884f ; --accent2 : #5fb0c9 ; --ok : #5ec27a ; --bad : #e06a6a ; --warn : #e0b84f ;
--border : #272c36 ; --mono : "SF Mono" , ui-monospace , Menlo , Consolas , monospace ;
--sans : - apple-system , "PingFang SC" , "Helvetica Neue" , Arial , sans-serif ;
}
* { box-sizing : border-box }
body { margin : 0 ; background : var ( - - bg ) ; color : var ( - - fg ) ; font-family : var ( - - sans ) ; line-height : 1.7 ; font-size : 15 px }
. wrap { max-width : 960 px ; margin : 0 auto ; padding : 48 px 24 px 96 px }
h1 { font-size : 30 px ; line-height : 1.3 ; margin : 0 0 8 px ; letter-spacing : -.01 em }
. sub { color : var ( - - fg2 ) ; font-size : 15 px ; margin : 0 0 32 px }
h2 { font-size : 21 px ; margin : 44 px 0 14 px ; padding-bottom : 8 px ; border-bottom : 1 px solid var ( - - border ) }
h3 { font-size : 16 px ; margin : 24 px 0 8 px ; color : var ( - - fg ) }
p { margin : 10 px 0 }
code { font-family : var ( - - mono ) ; font-size : .86 em ; background : var ( - - panel2 ) ; padding : 1 px 6 px ; border-radius : 5 px ; color : #f0d9c4 }
pre { background : #0a0c11 ; border : 1 px solid var ( - - border ) ; border-radius : 10 px ; padding : 14 px 16 px ; overflow-x : auto ; font-family : var ( - - mono ) ; font-size : 12.5 px ; line-height : 1.55 ; color : #cdd3df }
pre code { background : none ; padding : 0 ; color : inherit }
. c { color : #6a7385 }
. k { color : #5fb0c9 }
. s { color : #9ec97f }
. tag { display : inline-block ; font-size : 12 px ; font-weight : 600 ; padding : 2 px 9 px ; border-radius : 999 px ; vertical-align : middle }
. tag . ok { background : rgba ( 94 , 194 , 122 , .16 ) ; color : var ( - - ok ) }
. tag . warn { background : rgba ( 224 , 184 , 79 , .16 ) ; color : var ( - - warn ) }
. tag . info { background : rgba ( 95 , 176 , 201 , .16 ) ; color : var ( - - accent2 ) }
. tag . bad { background : rgba ( 224 , 106 , 106 , .16 ) ; color : var ( - - bad ) }
. card { background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; border-radius : 12 px ; padding : 16 px 20 px ; margin : 14 px 0 }
. card h3 { margin-top : 0 ; color : var ( - - accent2 ) }
. files { font-family : var ( - - mono ) ; font-size : 12 px ; color : var ( - - fg2 ) ; margin : 6 px 0 10 px }
table { width : 100 % ; border-collapse : collapse ; margin : 16 px 0 ; font-size : 14 px }
th , td { text-align : left ; padding : 9 px 12 px ; border-bottom : 1 px solid var ( - - border ) ; vertical-align : top }
th { color : var ( - - fg2 ) ; font-weight : 600 ; font-size : 13 px }
ul , ol { padding-left : 22 px ; margin : 8 px 0 }
li { margin : 5 px 0 }
. lead { background : linear-gradient ( 180 deg , rgba ( 224 , 136 , 79 , .10 ) , transparent ) ; border : 1 px solid var ( - - border ) ; border-radius : 12 px ; padding : 18 px 20 px ; margin : 0 0 8 px }
. small { color : var ( - - fg2 ) ; font-size : 13 px }
a { color : var ( - - accent2 ) }
. back { display : inline-block ; margin-bottom : 24 px ; font-size : 13 px }
b { color : #fff }
. phase { font-size : 19 px ; margin : 40 px 0 6 px ; color : var ( - - accent ) ; font-weight : 700 }
. diagram { background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; border-radius : 12 px ; padding : 12 px ; margin : 16 px 0 ; overflow-x : auto }
< / style >
< / head >
< body >
< div class = "wrap" >
< a class = "back" href = "index.html" > ← 文档索引< / a >
< h1 > 家庭内网远程访问方案< br > frp 反向隧道 × pangolin × 分流 DNS< / h1 >
< p class = "sub" > 2026-07-15 · < span class = "tag ok" > 在外链路已上线< / span > (锚点 ali)· 家内直连(P5/P6)待做 · 相关:< a href = "public-access.md" > Tailscale 公网访问(保留作应急后门)< / a > < / p >
< div class = "card" style = "border-color:var(--warn)" >
< h3 style = "color:var(--warn)" > ⚠️ 2026-07-15 落地修正(与下文原设计的差异)< / h3 >
< ul >
< li > < b > 锚点从 pangolin1 迁到 ali( 182.92.213.171) < / b > :实测发现 sing-box 客户端 auto_route 会把< b > 自己服务器的 IP< / b > 排除出 TUN(内核级防环路,Apple 端 NE excludedRoutes),客户端永远无法经隧道回连 pangolin1——「hairpin 到 VPN 机自身」的原设计不成立。迁 ali 后链路:Mac→隧道→pangolin1 出口→公网→ali:frps←frpc←NAS。限源改用< b > 阿里云安全组< / b > (业务端口仅允许 103.119.13.48/32),负向测试已验证。frpc 家→北京为纯国内长连接,更稳。< / li >
< li > < b > NAS 上 mihomo 已不存在< / b > ( 2026-07-05 随 ec2 退役下线,本文 §6.2 的 mihomo hosts 方案作废):家内 DNS 覆盖改用< b > 群晖 DNS Server 套件< / b > (为 nas/git/win.yanmeiai.com 建单主机权威 zone,不劫持父域),待回家后落地。< / li >
< li > < b > frpc 用二进制 + systemd< / b > (非 docker):NAS 无出国通道拉不了 Docker Hub 镜像;顺带消除对 Container Manager 的依赖。< / li >
< li > < b > smartRoute 暂需关闭< / b > : ali 是国内 IP, smartRoute 开着会把它分流成直连、被安全组限源拦截。根治=P6 客户端规则(这几个域名强制走隧道 + 私网结果直连),落地前先关 smartRoute。< / li >
< li > Windows( 192.168.3.88)当前不在线(LAN 内 no route),RDP/SSH 转发待它开机即用,无需重配。< / li >
< / ul >
< / div >
< div class = "lead" >
< b > 目标:< / b > ①在家直连内网、不出外网;②在外< b > 开着 pangolin< / b > 就能穿透回家(不装第二个 VPN 客户端);③内外用< b > 同一套地址< / b > ——< code > git.yanmeiai.com< / code > ( Forgejo)、< code > nas.yanmeiai.com< / code > ( DSM/Photos)、< code > win.yanmeiai.com< / code > ( Windows RDP/SSH)。< br >
< b > 核心思路:< / b > NAS 上的 frpc 主动外连 pangolin1 上的 frps 建反向隧道(家里零开放端口、不暴露家庭 IP);同一批域名< b > 公网解析到 pangolin1、家内 DNS 解析到内网 IP< / b > ( split-horizon);pangolin 客户端配置加一条「这几个域名走本地 DNS、私网结果直连」的分流规则,保证 pangolin 常开时内外自动走对路。
< / div >
< h2 > 1 · 现状盘点(事实依据)< / h2 >
< table >
< tr > < th > 对象< / th > < th > 现状< / th > < th > 与本方案的关系< / th > < / tr >
< tr > < td > < b > NAS< / b > ( DS925+) < / td > < td > < code > 192.168.3.200< / code > ;跑 Forgejo( < code > :3000< / code > HTTP / < code > :2222< / code > SSH)、mihomo 透明代理(< code > :53< / code > fake-ip DNS / TUN)、Tailscale( < code > 100.95.151.13< / code > ) < / td > < td > 装 frpc; mihomo 加 hosts 覆盖;Tailscale 作本次部署跳板< / td > < / tr >
< tr > < td > < b > Windows< / b > < / td > < td > < code > 192.168.3.88< / code > ,网关/DNS 均指向 NAS;有 OpenSSH server( < code > ssh windows< / code > ) < / td > < td > 基本不改;frpc 从 NAS 直接转发其 RDP/SSH< / td > < / tr >
< tr > < td > < b > pangolin1< / b > < / td > < td > < code > 103.119.13.48< / code > , Debian 12, 2C/1G; < code > :443< / code > sing-box REALITY、< code > :8080< / code > 控制面(仅 127.0.0.1)、< code > :9443< / code > gRPC、cloudflared 出站隧道;ufw 默认拒入站< / td > < td > 装 frps( ~20MB 内存);< code > 7000/2222/3000/3389/5001/10022/10023< / code > 均空闲无冲突< / td > < / tr >
< tr > < td > < b > 家宽< / b > < / td > < td > 有真实公网 IP(动态),路由器无端口映射< / td > < td > 保持零开放端口,不做 DDNS< / td > < / tr >
< tr > < td > < b > DNS< / b > < / td > < td > < code > yanmeiai.com< / code > 在 Cloudflare(管理走 < code > cf-api< / code > );< code > git.51yanmei.com< / code > 公网 A 记录指 < code > 192.168.3.200< / code > (仅家内可达)< / td > < td > 新增 3 条 DNS-only A 记录;旧 git 域名可选迁移< / td > < / tr >
< / table >
< h2 > 2 · 架构总览< / h2 >
< div class = "diagram" >
< svg viewBox = "0 0 900 470" width = "100%" font-family = "-apple-system,'PingFang SC',sans-serif" font-size = "13" >
<!-- ===== 在外场景 ===== -->
< text x = "20" y = "28" fill = "#e0884f" font-weight = "700" font-size = "15" > 在外(pangolin 开)< / text >
< rect x = "20" y = "44" width = "130" height = "52" rx = "8" fill = "#1d2129" stroke = "#272c36" / >
< text x = "85" y = "66" fill = "#e6e8ee" text-anchor = "middle" > Mac / 手机< / text >
< text x = "85" y = "84" fill = "#a8afbd" text-anchor = "middle" font-size = "11" > pangolin TUN< / text >
< line x1 = "150" y1 = "70" x2 = "255" y2 = "70" stroke = "#e0884f" stroke-width = "2" / >
< polygon points = "255,70 246,65 246,75" fill = "#e0884f" / >
< text x = "202" y = "60" fill = "#e0884f" text-anchor = "middle" font-size = "11" > REALITY 隧道< / text >
< rect x = "258" y = "30" width = "240" height = "130" rx = "10" fill = "#171a22" stroke = "#e0884f" / >
< text x = "378" y = "52" fill = "#e6e8ee" text-anchor = "middle" font-weight = "600" > pangolin1 · 103.119.13.48< / text >
< rect x = "274" y = "64" width = "98" height = "36" rx = "6" fill = "#1d2129" stroke = "#272c36" / >
< text x = "323" y = "87" fill = "#e6e8ee" text-anchor = "middle" font-size = "12" > sing-box< / text >
< rect x = "386" y = "64" width = "98" height = "36" rx = "6" fill = "#1d2129" stroke = "#272c36" / >
< text x = "435" y = "87" fill = "#e6e8ee" text-anchor = "middle" font-size = "12" > frps< / text >
< line x1 = "372" y1 = "82" x2 = "386" y2 = "82" stroke = "#5fb0c9" stroke-width = "2" / >
< polygon points = "386,82 378,78 378,86" fill = "#5fb0c9" / >
< text x = "378" y = "125" fill = "#5fb0c9" text-anchor = "middle" font-size = "11" > 机内回环(hairpin):出口连自己< / text >
< text x = "378" y = "143" fill = "#a8afbd" text-anchor = "middle" font-size = "11" > 业务端口 ufw 不开放 → 外网扫不到< / text >
< line x1 = "498" y1 = "82" x2 = "610" y2 = "82" stroke = "#5fb0c9" stroke-width = "2" stroke-dasharray = "6 4" / >
< polygon points = "502,82 511,77 511,87" fill = "#5fb0c9" / >
< text x = "554" y = "72" fill = "#5fb0c9" text-anchor = "middle" font-size = "11" > frp 反向长连接< / text >
< text x = "554" y = "100" fill = "#a8afbd" text-anchor = "middle" font-size = "10" > (家里主动拨出,TLS+token) < / text >
< rect x = "612" y = "30" width = "268" height = "150" rx = "10" fill = "#171a22" stroke = "#5ec27a" / >
< text x = "746" y = "52" fill = "#e6e8ee" text-anchor = "middle" font-weight = "600" > 家庭内网 192.168.3.0/24< / text >
< rect x = "628" y = "64" width = "110" height = "52" rx = "6" fill = "#1d2129" stroke = "#272c36" / >
< text x = "683" y = "84" fill = "#e6e8ee" text-anchor = "middle" font-size = "12" > NAS .200< / text >
< text x = "683" y = "102" fill = "#a8afbd" text-anchor = "middle" font-size = "10" > frpc·git·DSM·DNS< / text >
< rect x = "754" y = "64" width = "110" height = "52" rx = "6" fill = "#1d2129" stroke = "#272c36" / >
< text x = "809" y = "84" fill = "#e6e8ee" text-anchor = "middle" font-size = "12" > Windows .88< / text >
< text x = "809" y = "102" fill = "#a8afbd" text-anchor = "middle" font-size = "10" > RDP 3389 · SSH 22< / text >
< line x1 = "738" y1 = "90" x2 = "754" y2 = "90" stroke = "#5ec27a" stroke-width = "2" / >
< polygon points = "754,90 746,86 746,94" fill = "#5ec27a" / >
< text x = "746" y = "168" fill = "#a8afbd" text-anchor = "middle" font-size = "11" > 路由器零端口映射,家庭 IP 不出现在公网 DNS< / text >
<!-- ===== 在家场景 ===== -->
< text x = "20" y = "250" fill = "#5ec27a" font-weight = "700" font-size = "15" > 在家(pangolin 照样开)< / text >
< rect x = "20" y = "266" width = "130" height = "52" rx = "8" fill = "#1d2129" stroke = "#272c36" / >
< text x = "85" y = "288" fill = "#e6e8ee" text-anchor = "middle" > Mac / 手机< / text >
< text x = "85" y = "306" fill = "#a8afbd" text-anchor = "middle" font-size = "11" > pangolin TUN< / text >
< rect x = "240" y = "252" width = "300" height = "80" rx = "10" fill = "#171a22" stroke = "#272c36" / >
< text x = "390" y = "276" fill = "#e6e8ee" text-anchor = "middle" font-weight = "600" > 客户端分流规则(BuildClientConfig 下发)< / text >
< text x = "390" y = "298" fill = "#a8afbd" text-anchor = "middle" font-size = "11" > 这 3 个域名 → 本地 DNS 解析 → 得内网 IP< / text >
< text x = "390" y = "316" fill = "#5ec27a" text-anchor = "middle" font-size = "11" > 私网地址命中 LAN 直连规则 → 不进隧道< / text >
< line x1 = "150" y1 = "292" x2 = "240" y2 = "292" stroke = "#5ec27a" stroke-width = "2" / >
< polygon points = "240,292 231,287 231,297" fill = "#5ec27a" / >
< line x1 = "540" y1 = "292" x2 = "640" y2 = "292" stroke = "#5ec27a" stroke-width = "2" / >
< polygon points = "640,292 631,287 631,297" fill = "#5ec27a" / >
< text x = "590" y = "282" fill = "#5ec27a" text-anchor = "middle" font-size = "11" > 纯局域网直连< / text >
< rect x = "642" y = "252" width = "238" height = "80" rx = "10" fill = "#171a22" stroke = "#5ec27a" / >
< text x = "761" y = "280" fill = "#e6e8ee" text-anchor = "middle" > NAS .200 / Windows .88< / text >
< text x = "761" y = "302" fill = "#a8afbd" text-anchor = "middle" font-size = "11" > mihomo hosts 把 3 个域名< / text >
< text x = "761" y = "318" fill = "#a8afbd" text-anchor = "middle" font-size = "11" > 解析成内网 IP< / text >
<!-- ===== DNS 分流对照 ===== -->
< rect x = "20" y = "366" width = "860" height = "84" rx = "10" fill = "#0a0c11" stroke = "#272c36" / >
< text x = "40" y = "392" fill = "#a8afbd" font-size = "12" > 同一个域名,两处答案(split-horizon):< / text >
< text x = "40" y = "416" fill = "#e6e8ee" font-size = "12" font-family = "ui-monospace,Menlo,monospace" > git.yanmeiai.com< / text >
< text x = "220" y = "416" fill = "#5fb0c9" font-size = "12" > 公网 DNS( Cloudflare)→ 103.119.13.48( pangolin1/frps) < / text >
< text x = "220" y = "438" fill = "#5ec27a" font-size = "12" > 家内 DNS( NAS mihomo)→ 192.168.3.200(直连)< / text >
< / svg >
< / div >
< h2 > 3 · 统一地址与端口规划< / h2 >
< p > 选端口的原则:< b > 内外端口号一致< / b > (同一个 URL 全球通用)。唯一例外是裸 SSH——< code > :22< / code > 在 pangolin1 上被它自己的 sshd 占用,只能错位到 10022/10023,由 < code > ~/.ssh/config< / code > 别名消化(见 §7)。< / p >
< table >
< tr > < th > 服务< / th > < th > 统一地址< / th > < th > frpc 转发目标(家内)< / th > < th > pangolin1 remotePort< / th > < th > 备注< / th > < / tr >
< tr > < td > Forgejo Git SSH< / td > < td > < code > ssh://git@git.yanmeiai.com:2222< / code > < / td > < td > < code > 127.0.0.1:2222< / code > < / td > < td > 2222< / td > < td > 内外一致 ✓< / td > < / tr >
< tr > < td > Forgejo Web< / td > < td > < code > http://git.yanmeiai.com:3000< / code > < / td > < td > < code > 127.0.0.1:3000< / code > < / td > < td > 3000< / td > < td > 内外一致 ✓< / td > < / tr >
< tr > < td > DSM / Photos< / td > < td > < code > https://nas.yanmeiai.com:5001< / code > < / td > < td > < code > 127.0.0.1:5001< / code > < / td > < td > 5001< / td > < td > 内外一致 ✓;Photos App 填这个地址< / td > < / tr >
< tr > < td > Windows RDP< / td > < td > < code > win.yanmeiai.com:3389< / code > < / td > < td > < code > 192.168.3.88:3389< / code > < / td > < td > 3389< / td > < td > 内外一致 ✓< / td > < / tr >
< tr > < td > NAS SSH< / td > < td > 外:< code > nas.yanmeiai.com:10022< / code > < br > 家:< code > 192.168.3.200:22< / code > < / td > < td > < code > 127.0.0.1:22< / code > < / td > < td > 10022< / td > < td > 端口错位,ssh alias 兜底< / td > < / tr >
< tr > < td > Windows SSH< / td > < td > 外:< code > win.yanmeiai.com:10023< / code > < br > 家:< code > 192.168.3.88:22< / code > < / td > < td > < code > 192.168.3.88:22< / code > < / td > < td > 10023< / td > < td > 端口错位,ssh alias 兜底< / td > < / tr >
< / table >
< p class = "small" > 不转发的:SMB/445(协议不适合过公网,文件走 Synology Drive/Photos 或 git);DSM 5000(明文 HTTP,只留 5001 HTTPS);mihomo 9090 管理口(有需要时临时经 NAS SSH 端口转发)。< / p >
< h2 > 4 · 安全模型(三层)< / h2 >
< div class = "card" >
< h3 > 第一层:业务端口对公网「隐形」(关键设计)< / h3 >
< p > pangolin1 的 ufw 默认拒绝入站。< b > frps 的业务端口(2222/3000/3389/5001/10022/10023)一条 allow 规则都不加。< / b > 你在外面走 pangolin 时,流量从 sing-box 出站连 < code > 103.119.13.48< / code > ——目的地是本机自己的公网 IP,内核走 < b > loopback 回环(hairpin) < / b > 送达,而 ufw 对 < code > lo< / code > 接口默认全放行。效果:< b > 这些端口只有「连着 pangolin 的你」能访问< / b > ,公网扫描器看到的是全拒。RDP 这种绝不能裸奔的服务因此是安全的。< / p >
< p class = "small" > 验证兜底:若实测 hairpin 未走 lo(§9 验收步骤 P1-4 会测),补一条 < code > ufw allow from 103.119.13.48< / code > 等价实现,语义不变。< / p >
< / div >
< div class = "card" >
< h3 > 第二层:frp 控制通道加固< / h3 >
< p > < code > :7000< / code > 是唯一新开的公网端口(frpc 从家里拨入用,家宽 IP 动态、无法限源)。加固:< code > auth.token< / code > ( 32 字节随机,存 Bitwarden) + < code > transport.tls.force = true< / code > (强制 TLS,未握手连接直接断)。< / p >
< / div >
< div class = "card" >
< h3 > 第三层:业务自身认证不放松< / h3 >
< p > Forgejo 密钥登录、DSM 账号 2FA + 自动封锁、Windows RDP 强密码/NLA——原有认证全部保留,隧道只是网络通路。Tailscale 保留不动,作为 frp 链路故障时的应急后门。< / p >
< / div >
< h2 > 5 · pangolin1 怎么配(装 frps) < / h2 >
< p class = "files" > 机器:< code > ssh pangolin1< / code > · 人在外也能直接做(公网可达)< / p >
< h3 > 5.1 生成 token(本机 Mac 上)< / h3 >
< pre > < code > < span class = "c" > # 生成 32 字节随机 token,存进 Bitwarden(条目名:frp home-tunnel) < / span >
openssl rand -hex 32
rbw add "frp home-tunnel" < span class = "c" > # 密码字段贴上面的输出;两端配置从这里取,不落明文文件< / span > < / code > < / pre >
< h3 > 5.2 安装 frps 二进制 + systemd< / h3 >
< pre > < code > < span class = "c" > # ssh pangolin1 之后(root)。VER 以 frp releases 最新版为准,下例为示意< / span >
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< / code > < / pre >
< p > < code > /etc/frp/frps.toml< / code > ( < span class = "tag warn" > token 不入 git< / span > ,与 < code > /etc/pangolin*< / code > 密钥同规矩):< / p >
< pre > < code > < span class = "k" > bindAddr< / span > = < span class = "s" > "0.0.0.0"< / span >
< span class = "k" > bindPort< / span > = 7000
< span class = "k" > auth.method< / span > = < span class = "s" > "token"< / span >
< span class = "k" > auth.token< / span > = < span class = "s" > "< Bitwarden: frp home-tunnel> "< / span >
< span class = "k" > transport.tls.force< / span > = true < span class = "c" > # 拒绝非 TLS 的 frpc< / span >
< span class = "c" > # 不开 dashboard;业务端口按 frpc 请求动态监听< / span >
< span class = "k" > allowPorts< / span > = [
{ < span class = "k" > start< / span > = 2222, < span class = "k" > end< / span > = 2222 }, { < span class = "k" > start< / span > = 3000, < span class = "k" > end< / span > = 3000 },
{ < span class = "k" > start< / span > = 3389, < span class = "k" > end< / span > = 3389 }, { < span class = "k" > start< / span > = 5001, < span class = "k" > end< / span > = 5001 },
{ < span class = "k" > start< / span > = 10022, < span class = "k" > end< / span > = 10023 },
] < span class = "c" > # 白名单锁死,token 泄露也开不了别的口< / span > < / code > < / pre >
< p > < code > /etc/systemd/system/frps.service< / code > : < / p >
< pre > < code > [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 < span class = "c" > # 非特权端口,无需 root< / span >
LimitNOFILE=65536
< span class = "c" > # 1G 小机安全带:内存超限就重启,绝不拖垮 sing-box/控制面< / span >
MemoryMax=128M
[Install]
WantedBy=multi-user.target< / code > < / pre >
< pre > < code > 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)'
< span class = "c" > # 验证:只多了 7000 这一个对外口< / span >
systemctl status frps --no-pager | head -5
ss -tlnp | grep frps< / code > < / pre >
< p class = "small" > ⚠️ 台账提醒:pangolin1 续费到期 < b > 2026-07-19< / b > ( baize < code > billing.renew_at< / code > )——动工前先确认已续费。< / p >
< h2 > 6 · DNS 怎么配(Cloudflare + 家内 mihomo) < / h2 >
< h3 > 6.1 公网侧:Cloudflare 加 3 条 A 记录(本机 Mac, cf-api) < / h3 >
< p > < b > 必须 DNS only(灰云,< code > proxied:false< / code > ) < / b > ——git SSH / RDP 是任意 TCP, CF 橙云只代理 HTTP。zone id 见 baize: < code > 2325730de45276d87180a8b66bd4cca0< / code > 。< / p >
< pre > < code > 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}'< / code > < / pre >
< p class = "small" > 代价说明:灰云记录会暴露 pangolin1 的 IP——它本来就是 VPN 数据面入口(客户端 endpoint 直连该 IP),无新增暴露。改完回写 baize < code > domains.yaml< / code > 。< / p >
< h3 > 6.2 家内侧:NAS mihomo 加 hosts 覆盖< / h3 >
< p > < code > /volume1/docker/proxy/mihomo/config.yaml< / code > 两处改动(仓库 < code > docker/proxy/mihomo/config.yaml< / code > 同步改):< / p >
< pre > < code > < span class = "k" > hosts< / span > : < span class = "c" > # ① 家内答案:直接给内网 IP< / span >
< span class = "k" > nas.yanmeiai.com< / span > : < span class = "s" > 192.168.3.200< / span >
< span class = "k" > git.yanmeiai.com< / span > : < span class = "s" > 192.168.3.200< / span >
< span class = "k" > win.yanmeiai.com< / span > : < span class = "s" > 192.168.3.88< / span >
< span class = "k" > git.51yanmei.com< / span > : < span class = "s" > 192.168.3.200< / span > < span class = "c" > # 兼容既有 git remote(可选,见 §10) < / span >
< span class = "k" > dns< / span > :
< span class = "k" > fake-ip-filter< / span > : < span class = "c" > # ② 追加:不给这几个域名发 fake-ip,走 hosts 真实值< / span >
< span class = "c" > # ……既有条目保持不动(含 +.tailscale.com 等)……< / span >
- < span class = "s" > nas.yanmeiai.com< / span >
- < span class = "s" > git.yanmeiai.com< / span >
- < span class = "s" > win.yanmeiai.com< / span >
- < span class = "s" > git.51yanmei.com< / span > < / code > < / pre >
< pre > < code > < span class = "c" > # 热重载(不重启容器),沿用仓库既有做法< / span >
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\"}'"
< span class = "c" > # 验证:家内解析应返回 192.168.3.200< / span >
ssh nas "nslookup git.yanmeiai.com 127.0.0.1"< / code > < / pre >
< h3 > 6.3 路由器:DHCP 的 DNS 指到 NAS(回家后做)< / h3 >
< p > Windows 已手动指 NAS;要让 Mac/手机在家也吃到 hosts 覆盖,把路由器 DHCP 下发的 DNS 改为 < code > 192.168.3.200< / code > (一处改动、全家生效)。< span class = "tag warn" > 取舍< / span > :NAS 由此成为家内 DNS 单点——不要配第二个公共 DNS 兜底(客户端会随机用它,覆盖时灵时不灵);NAS 重启的几分钟内家里断解析,可接受。< / p >
< h2 > 7 · NAS 怎么配(装 frpc) < / h2 >
< p class = "files" > 仓库新增:< code > docker/frp/docker-compose.yml< / code > + < code > docker/frp/frpc.toml< / code > + < code > docker/frp/.env.example< / code > · NAS 路径:< code > /volume1/docker/frp/< / code > < / p >
< p > 沿用本仓库 docker-compose 惯例(forgejo/proxy 同款)。< code > wangjia< / code > 不在 docker 组,部署用 < code > sudo< / code > 或 Container Manager UI。< / p >
< p > < code > docker-compose.yml< / code > : < / p >
< pre > < code > < span class = "k" > services< / span > :
< span class = "k" > frpc< / span > :
< span class = "k" > image< / span > : < span class = "s" > snowdreamtech/frpc:0.61< / span > < span class = "c" > # 多架构维护镜像;版本与 frps 对齐< / span >
< span class = "k" > container_name< / span > : frpc
< span class = "k" > network_mode< / span > : host < span class = "c" > # 需直达 127.0.0.1 各服务 + 192.168.3.88< / span >
< span class = "k" > restart< / span > : unless-stopped
< span class = "k" > volumes< / span > :
- < span class = "s" > ./frpc.toml:/etc/frp/frpc.toml:ro< / span > < / code > < / pre >
< p > < code > frpc.toml< / code > (token 同样不入 git,仓库放 < code > frpc.toml.example< / code > 占位):< / p >
< pre > < code > < span class = "k" > serverAddr< / span > = < span class = "s" > "103.119.13.48"< / span >
< span class = "k" > serverPort< / span > = 7000
< span class = "k" > auth.method< / span > = < span class = "s" > "token"< / span >
< span class = "k" > auth.token< / span > = < span class = "s" > "< Bitwarden: frp home-tunnel> "< / span >
< span class = "k" > transport.tls.enable< / span > = true
[[< span class = "k" > proxies< / span > ]]
< span class = "k" > name< / span > = < span class = "s" > "git-ssh"< / span >
< span class = "k" > type< / span > = < span class = "s" > "tcp"< / span >
< span class = "k" > localIP< / span > = < span class = "s" > "127.0.0.1"< / span >
< span class = "k" > localPort< / span > = 2222
< span class = "k" > remotePort< / span > = 2222
[[< span class = "k" > proxies< / span > ]]
< span class = "k" > name< / span > = < span class = "s" > "git-web"< / span >
< span class = "k" > type< / span > = < span class = "s" > "tcp"< / span >
< span class = "k" > localIP< / span > = < span class = "s" > "127.0.0.1"< / span >
< span class = "k" > localPort< / span > = 3000
< span class = "k" > remotePort< / span > = 3000
[[< span class = "k" > proxies< / span > ]]
< span class = "k" > name< / span > = < span class = "s" > "dsm"< / span >
< span class = "k" > type< / span > = < span class = "s" > "tcp"< / span >
< span class = "k" > localIP< / span > = < span class = "s" > "127.0.0.1"< / span >
< span class = "k" > localPort< / span > = 5001
< span class = "k" > remotePort< / span > = 5001
[[< span class = "k" > proxies< / span > ]]
< span class = "k" > name< / span > = < span class = "s" > "nas-ssh"< / span >
< span class = "k" > type< / span > = < span class = "s" > "tcp"< / span >
< span class = "k" > localIP< / span > = < span class = "s" > "127.0.0.1"< / span >
< span class = "k" > localPort< / span > = 22
< span class = "k" > remotePort< / span > = 10022
[[< span class = "k" > proxies< / span > ]]
< span class = "k" > name< / span > = < span class = "s" > "win-rdp"< / span >
< span class = "k" > type< / span > = < span class = "s" > "tcp"< / span >
< span class = "k" > localIP< / span > = < span class = "s" > "192.168.3.88"< / span > < span class = "c" > # 同网段代转 Windows,一个 frpc 管全家< / span >
< span class = "k" > localPort< / span > = 3389
< span class = "k" > remotePort< / span > = 3389
[[< span class = "k" > proxies< / span > ]]
< span class = "k" > name< / span > = < span class = "s" > "win-ssh"< / span >
< span class = "k" > type< / span > = < span class = "s" > "tcp"< / span >
< span class = "k" > localIP< / span > = < span class = "s" > "192.168.3.88"< / span >
< span class = "k" > localPort< / span > = 22
< span class = "k" > remotePort< / span > = 10023< / code > < / pre >
< pre > < code > < span class = "c" > # 部署(经 Tailscale 跳板 ssh 到 NAS 后)< / span >
sudo mkdir -p /volume1/docker/frp & & cd /volume1/docker/frp
< span class = "c" > # (把仓库里的两个文件放进来,frpc.toml 填真 token) < / span >
sudo docker compose up -d
sudo docker logs frpc --tail 20 < span class = "c" > # 应见 "login to server success" + 6 个 proxy 启动成功< / span > < / code > < / pre >
< h3 > Mac 的 ssh 别名收尾(< code > ~/.ssh/config< / code > ) < / h3 >
< pre > < code > < span class = "c" > # 既有 Host nas / windows(内网 IP 直连)保持不动,家里继续用< / span >
Host nas-r < span class = "c" > # 在外用:ssh nas-r< / span >
HostName nas.yanmeiai.com
Port 10022
User wangjia
Host win-r < span class = "c" > # 在外用:ssh win-r< / span >
HostName win.yanmeiai.com
Port 10023
User wangjia< / code > < / pre >
< h2 > 8 · Windows 怎么改(几乎不用)< / h2 >
< table >
< tr > < th > 项< / th > < th > 动作< / th > < th > 说明< / th > < / tr >
< tr > < td > 网络< / td > < td > < span class = "tag ok" > 不改< / span > < / td > < td > 网关/DNS 已指 NAS,天然吃到 hosts 覆盖< / td > < / tr >
< tr > < td > RDP< / td > < td > 确认已启用(设置 → 系统 → 远程桌面),保持 NLA 开启< / td > < td > frpc 从 NAS(同网段 .200)连入,默认防火墙「专用网络」范围即放行< / td > < / tr >
< tr > < td > OpenSSH server< / td > < td > < span class = "tag ok" > 已有< / span > ( < code > ssh windows< / code > 在用),确认服务设为自动启动< / td > < td > 同上,无需改防火墙< / td > < / tr >
< tr > < td > 防火墙< / td > < td > 仅当连不通时检查:RDP/SSH 入站规则的作用域含本地子网< / td > < td > 不需要也不要开「任意来源」< / td > < / tr >
< / table >
< h2 > 9 · pangolin 客户端分流(BuildClientConfig,让家里直连成立)< / h2 >
< p class = "files" > 代码:< code > server/internal/httpapi/clientconfig.go::BuildClientConfig< / code > · 单独一刀,四端零改动(配置服务端渲染下发)< / p >
< p > pangolin TUN 的首条路由规则是 < code > hijack-dns< / code > ,所有 DNS 都被劫去隧道解析——家里也会拿到公网 IP、绕道外网。解法是给这几个域名开「本地解析 + 按结果路由」的侧门:< / p >
< pre > < code > < span class = "c" > // 伪代码示意。域名清单走 server 配置(env PRIVATE_SPLIT_DOMAINS),默认空 = 行为不变,< / span >
< span class = "c" > // 不影响其他用户。< / span >
< span class = "c" > // ① dns.servers 增本地解析器< / span >
{ < span class = "s" > "tag"< / span > : < span class = "s" > "dns-local"< / span > , < span class = "s" > "address"< / span > : < span class = "s" > "local"< / span > , < span class = "s" > "detour"< / span > : < span class = "s" > "direct"< / span > }
< span class = "c" > // ② dns.rules 前插:这 3 个域名用本地 DNS(家=mihomo hosts→内网IP;外=公网DNS→pangolin1) < / span >
{ < span class = "s" > "domain"< / span > : [< span class = "s" > "nas.yanmeiai.com"< / span > ,< span class = "s" > "git.yanmeiai.com"< / span > ,< span class = "s" > "win.yanmeiai.com"< / span > ], < span class = "s" > "server"< / span > : < span class = "s" > "dns-local"< / span > }
< span class = "c" > // ③ route.rules 在 hijack-dns 之后插:先解析再按 IP 匹配后续规则< / span >
{ < span class = "s" > "domain"< / span > : [< span class = "s" > "…同上…"< / span > ], < span class = "s" > "action"< / span > : < span class = "s" > "resolve"< / span > }
< span class = "c" > // ④ 解析结果为私网 → 命中既有 LAN 直连规则 → 家里直连;< / span >
< span class = "c" > // 公网 IP( pangolin1)→ 不匹配 → 走默认代理出口 → 机内 hairpin 到 frps< / span > < / code > < / pre >
< p > < span class = "tag warn" > 实现时必须验证< / span > : < code > action: resolve< / code > 是 sing-box ≥ 1.11 语法——先确认四端内嵌 libbox 版本;不够就先升 libbox( < code > scripts/build-libbox.sh< / code > 重编,产物 gitignore 需各端重新放置)。按仓库惯例先在 macOS 端端到端验证,再推 iOS/Android/Windows。< / p >
< p class = "small" > 此刀未落地前的过渡用法:在外一切正常(隧道 DNS 拿公网 IP 本来就该走隧道);< b > 在家< / b > 访问这几个域名会绕外网一圈(功能可用、不满足目标①),或临时关 pangolin 直连。< / p >
< h2 > 10 · 执行顺序(人在外,现在就能开工)< / h2 >
< p class = "phase" > P0 · 前置(5 分钟)< / p >
< ul >
< li > 确认 pangolin1 已续费(< b > 2026-07-19 到期< / b > ) < / li >
< li > 生成 token 存 Bitwarden「frp home-tunnel」(§5.1) < / li >
< / ul >
< p class = "phase" > P1 · pangolin1 装 frps(直接可做,§5.2) < / p >
< ul >
< li > 装二进制 + 配置 + systemd 拉起,< code > ufw allow 7000/tcp< / code > < / li >
< li > 验证:< code > ss -tlnp | grep frps< / code > 只见 7000; < b > P1-4 hairpin 实测< / b > ——pangolin1 本机 < code > curl --max-time 3 http://103.119.13.48:7000< / code > 通(说明本机对自己公网 IP 走 lo),外网机器(如 ali)< code > nc -zw3 103.119.13.48 2222< / code > 必须< b > 不通< / b > < / li >
< / ul >
< p class = "phase" > P2 · Cloudflare 加 3 条灰云 A 记录(直接可做,§6.1)< / p >
< ul >
< li > 验证:< code > dig +short git.yanmeiai.com @1.1.1.1< / code > → < code > 103.119.13.48< / code > < / li >
< / ul >
< p class = "phase" > P3 · 经 Tailscale 跳板配 NAS(§6.2 + §7) < / p >
< ul >
< li > Mac:断 pangolin → 连 Tailscale →(若登录卡住:Tailscale 登录要过 Google,可先在 pangolin 开着时完成浏览器授权再切换)< / li >
< li > < code > ssh wangjia@100.95.151.13< / code > :部署 frpc compose、改 mihomo( hosts + fake-ip-filter)并热重载< / li >
< li > 验证:< code > docker logs frpc< / code > 见 6 个 proxy 成功;< code > nslookup git.yanmeiai.com 127.0.0.1< / code > → 192.168.3.200< / li >
< li > Mac:断 Tailscale → 恢复 pangolin< / li >
< / ul >
< p class = "phase" > P4 · 在外端到端验收(pangolin 开着)< / p >
< ul >
< li > < code > git ls-remote ssh://git@git.yanmeiai.com:2222/wangjia/nas.git< / code > ✓< / li >
< li > 浏览器 < code > http://git.yanmeiai.com:3000< / code > 出 Forgejo 登录页 ✓< / li >
< li > < code > https://nas.yanmeiai.com:5001< / code > 出 DSM(证书告警属预期,见 §12 增强)✓< / li >
< li > Microsoft Remote Desktop 连 < code > win.yanmeiai.com< / code > ✓;< code > ssh nas-r< / code > / < code > ssh win-r< / code > ✓< / li >
< li > 反证:< b > 关掉 pangolin< / b > 后上述地址应全部超时(ufw 隐形生效)< / li >
< / ul >
< p class = "phase" > P5 · 回家后收尾(§6.3) < / p >
< ul >
< li > 路由器 DHCP DNS → 192.168.3.200; Mac 重新拿 DHCP< / li >
< li > 验证:< code > dig +short git.yanmeiai.com< / code > → 192.168.3.200;关 pangolin 时 < code > git ls-remote< / code > /DSM 直连均通且 traceroute 一跳< / li >
< / ul >
< p class = "phase" > P6 · pangolin 分流规则(单独一刀,§9)< / p >
< ul >
< li > 确认 libbox 版本 → 改 < code > BuildClientConfig< / code > + 单测 → macOS 端在家实测「pangolin 开着直连内网」→ 推其余三端< / li >
< / ul >
< p class = "phase" > P7 · 台账与文档回写< / p >
< ul >
< li > baize < code > domains.yaml< / code > ( 3 条新记录 + expected_resolution);nas 仓 < code > CLAUDE.md< / code > ( frp 服务 + 端口清单);< code > ~/.ssh/config< / code > ( nas-r/win-r);todo 登记< / li >
< / ul >
< h2 > 11 · 风险与回滚< / h2 >
< table >
< tr > < th > 风险< / th > < th > 影响< / th > < th > 缓解 / 回滚< / th > < / tr >
< tr > < td > NAS 成家内 DNS 单点< / td > < td > NAS 挂 → 全家断解析< / td > < td > 接受(NAS 稳定);应急把路由器 DHCP DNS 改回运营商< / td > < / tr >
< tr > < td > token 泄露< / td > < td > 他人可向 frps 注册代理< / td > < td > < code > allowPorts< / code > 白名单锁死端口范围;业务端口本就 ufw 隐形;换 token 重启两端即失效< / td > < / tr >
< tr > < td > frps 占用拖累 1G 小机< / td > < td > 控制面/数据面受影响< / td > < td > systemd < code > MemoryMax=128M< / code > ,超限自杀重启,隔离爆炸半径< / td > < / tr >
< tr > < td > hairpin 假设不成立< / td > < td > 在外连不上业务端口< / td > < td > P1-4 提前实测;兜底 < code > ufw allow from 103.119.13.48< / code > < / td > < / tr >
< tr > < td > pangolin1 迁移/换 IP< / td > < td > 公网侧全断< / td > < td > frps 配置随 deploy 脚本走;改 3 条 A 记录即恢复;家内直连不受影响< / td > < / tr >
< tr > < td > libbox 版本不支持 resolve< / td > < td > P6 延期< / td > < td > P1–P5 不依赖 P6;过渡期家里关 pangolin 或接受绕行< / td > < / tr >
< tr > < td > 整体回滚< / td > < td > —< / td > < td > NAS: < code > docker compose down< / code > ; pangolin1: < code > systemctl disable --now frps< / code > + 删 ufw 7000; CF 删 3 条记录;mihomo 还原备份重载。全程不动业务服务< / td > < / tr >
< / table >
< h2 > 12 · 可选增强(不阻塞主线)< / h2 >
< ul >
< li > < b > 旧 git 域名兼容< / b > :把 < code > git.51yanmei.com< / code > 公网 A 记录(阿里云)从 192.168.3.200 改指 103.119.13.48——既有仓库 remote 在外也能用,家内靠 §6.2 的 hosts 覆盖。或者反过来:全量迁 remote 到 < code > git.yanmeiai.com< / code > ,旧域名退役。< / li >
< li > < b > DSM 正经证书< / b > : DSM 用 Let's Encrypt DNS-01( CF API)给 < code > nas.yanmeiai.com< / code > 签证书,消掉 5001 的浏览器告警。< / li >
< li > < b > Forgejo ROOT_URL< / b > : app.ini 的 DOMAIN/SSH_DOMAIN/ROOT_URL 改为 < code > git.yanmeiai.com< / code > (对应 CLAUDE.md 里既有 TODO),web 页面上的 clone 地址就会是新域名。< / li >
< li > < b > 监控< / b > : pangolin-monitor( bootstrap 自带 Telegram 通道)加一条 frps 存活/7000 端口探测;NAS 侧 frpc 掉线重连靠 docker restart 策略兜底。< / li >
< / ul >
< / div >
< / body >
< / html >