手上有台腾讯轻量的 4M 小机器,本来是拿来做别的事,闲了大半年。上周坐在外面想进家里路由器看一眼——家里那台 iStoreOS 上跑着 AdGuard Home、每日日报脚本,真出点什么只能等回家再说,这感觉挺难受的。
第一反应是给路由器开个端口映射。但这事我给自己定过规矩:管理面绝不映射到公网。路由器是家里唯一的总闸,把它挂在公网上等人来扫,不值当。何况 25.12 这版固件我连 apk upgrade 都不敢跑,更不会去动 WAN 侧策略。
那就只剩一条路:让路由器主动往外连。管理面留在私网里,连接由里面发起,外面看不到任何可以攻的入口。这篇记录完整过程——两条通道怎么分工、各自怎么配、以及五个我实打实踩到的坑。其中两个属于"配置看着生效了其实没生效",一个属于"能验证的地方全验证过了,但还是不通"。
先说结论
- 不要只做一条通道。我最后是 SSH 反向隧道 + WireGuard 两条并存。前者极简、依赖少,什么时候都能进去;后者能覆盖整个内网,但依赖的东西多。前者是后者的保底。
- 家庭侧一个入站端口都不用开。不需要公网 IP、不需要 DDNS、不用碰防火墙的 WAN 策略。所有连接都是路由器自己往外拨出去的。
- OpenWrt 上的
dbclient读不了 OpenSSH 格式的私钥。这一步直接报String too long,我在这卡了半天。必须用dropbearkey在路由器本机生成密钥。 - 反向隧道的目标不能写
127.0.0.1。OpenWrt 的 dropbear 默认Interface='lan',只监听 br-lan 那个地址,回环口上压根没有服务在听。 - WireGuard 通了不等于能进内网。握手成功、
wg show一切正常,但访问内网设备全部超时——因为防火墙的forward策略是 REJECT,得给 wg 单开一个区域。

两条通道,各管各的
动手之前先想清楚一件事:我要的到底是什么。如果只是"出问题时能进去改个配置",那 SSH 一条隧道就够了;如果还想顺手访问家里的 NAS、看看监控面板,那就得把整个内网连出来。这两个需求的成本差得挺远,所以我分开做。
| 对比项 | 通道 A · SSH 反向隧道 | 通道 B · WireGuard 组网 |
|---|---|---|
| 协议 | TCP | UDP |
| 能访问什么 | 只到路由器自己 | 整个内网,含 NAS 和其他机器 |
| 配置量 | 小,十几分钟能通 | 中等,还要动防火墙区域 |
| 依赖 | 只要 SSH 能出去 | WireGuard 服务 + 路由宣告 + 防火墙都对 |
| 出问题时的排查面 | 窄 | 宽 |
| 我的定位 | 应急保底 | 日常主力 |
顺序上我建议先做 A。它要满足的前提少,万一哪里不通,排查面窄得多——先把"路由器能不能连出去""服务器端口能不能收"这些基础问题排掉,再上 B 心里有底。我这次就是这么走的,A 通了之后 B 只花了不到一半的时间。
通道 A:SSH 反向隧道
原理没什么花头:让路由器拨向云服务器,把服务器上的一个端口映射回自己的 SSH。连接是路由器发起的,所以家庭侧一个入站端口都不用开。
服务器那边要先放开一件事。sshd 默认 GatewayPorts no,这种状态下反向转发的端口只会绑在 127.0.0.1 上,公网根本连不进来。我改成了 clientspecified——比直接写 yes 更精细,绑定地址由客户端自己指定,不会把服务器上所有转发端口都摊开。
# /etc/ssh/sshd_config.d/50-gatewayports.conf
GatewayPorts clientspecified
# 改完先验语法再重启,别把自己关在门外
sshd -t && systemctl restart sshd
路由器侧的命令行是这样:
dbclient -N \
-R 0.0.0.0:8022:192.168.100.1:22 \
-i /root/.ssh/tunnel_dropbear \
-y -y -K 30 \
-p 22222 root@你的服务器IP

然后从外面:
ssh -p 8022 root@你的服务器IP

这里有两个细节,都是我踩过才明白的。
坑一:dbclient 读不了 OpenSSH 的私钥
我先在自己电脑上用 ssh-keygen 生成了一对 ed25519 密钥,把私钥传到路由器,一跑就报:
dbclient: Exited: String too long
这个报错相当不直观。真实原因是 dropbear 的客户端解析不了 OpenSSH 新格式私钥里的某些字段,缓冲不够就直接退出了,连个"格式不对"都不给你。解法是在路由器本机生成 dropbear 自己格式的密钥:
# 在路由器上生成
dropbearkey -t ed25519 -f /root/.ssh/tunnel_dropbear
# 提取对应的公钥,装到服务器 authorized_keys
dropbearkey -y -f /root/.ssh/tunnel_dropbear | grep '^ssh-'
反过来是没问题的——OpenSSH 服务端认得 dropbear 生成的公钥,公钥格式本来就是通用的。所以只有"路由器当客户端"这一侧需要注意,服务器侧照常用 ssh-keygen。
坑二:转发目标不能写 127.0.0.1
我第一版写的是 -R 0.0.0.0:8022:127.0.0.1:22,直觉上"转发到本机 22 端口"就该是回环地址。结果隧道建立成功、服务器那边确实出现了 8022 监听,但一连接就被 reset。
原因是 OpenWrt 的 dropbear 配置里写着 Interface='lan',它只监听 br-lan 那个地址:
$ netstat -tlnp | grep :22
tcp 0 0 192.168.100.1:22 0.0.0.0:* LISTEN 7856/dropbear
看见了——它监听的是 192.168.100.1:22,回环口上什么都没有。所以转发目标得写成 LAN 地址,改成 192.168.100.1:22 立刻就好了。
顺便说,这类"连接被 reset"比"连接超时"信息量大得多:超时说明路没通,reset 说明路通了但对面没人应答。用这个区分能省不少时间,我后面排 WireGuard 那个坑也是靠它。
把它做成开机自启的常驻服务
手动挂着个 dbclient 进程肯定不行——重启就没了,断线也不会自己回来。我用 procd 包了一层,中间加了个包装脚本,做两件 procd 自己做不好的事:等 WAN 就绪、以及无限重试。
为什么这两件事 procd 干不了?等 WAN 是因为开机时 PPPoE 可能还没拨通,这时候连服务器必然失败,白白消耗重试次数;无限重试是因为 procd 的 respawn 有阈值,重试次数耗尽它会放弃,而我要的是"只要机器还开着就一直尝试"。
# /usr/bin/tunnel-runner(片段)
# 等默认路由出现,最多 5 分钟
i=0
while [ "$i" -lt 60 ]; do
if ip route 2>/dev/null | grep -q '^default '; then break; fi
i=$((i + 1)); sleep 5
done
# 然后无限重试
while true; do
dbclient -N -R "0.0.0.0:8022:192.168.100.1:22" \
-i "$KEY" -y -y -K 30 -p 22222 "$USER@$HOST" >> "$LOGFILE" 2>&1
sleep 10
done
启动脚本注册成 S99,排在网络之后。实测把服务重启一次,隧道十来秒就能自己重建回来。

通道 B:WireGuard 组网
通道 A 只够进路由器自己。想让笔记本在外面直接连家里的 NAS,就得组一张网。
设计上把云服务器当中转节点(Hub),路由器和一个漫游客户端都接进来。关键是路由器要向 Hub 宣告家庭网段,否则 Hub 只知道有个 10.10.0.3 的邻居,不知道它背后还有一整个 192.168.100.0/24。
10.10.0.1 云服务器(Hub)
10.10.0.2 漫游客户端
10.10.0.3 家庭路由器
192.168.100.0/24 由路由器向 Hub 宣告
服务器端的 peer 配置里,那一行 AllowedIPs 就是"宣告"这个动作本身:
# /etc/wireguard/wg0.conf(服务器)
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <服务器的私钥>
[Peer]
# 家里的路由器
PublicKey = <路由器的公钥>
AllowedIPs = 10.10.0.3/32, 192.168.100.0/24
[Peer]
# 漫游客户端
PublicKey = <客户端的公钥>
AllowedIPs = 10.10.0.2/32
路由器这边我用 UCI 配,而不是直接写 wg0.conf——这样重启能持久化,LuCI 里也能看见、能改:
uci set network.wg0=interface
uci set network.wg0.proto='wireguard'
uci set network.wg0.private_key='<路由器私钥>'
uci set network.wg0.addresses='10.10.0.3/24'
uci set network.wg0.mtu='1420'
uci set network.wgpeer=wireguard_wg0
uci set network.wgpeer.public_key='<服务器公钥>'
uci set network.wgpeer.endpoint_host='你的服务器IP'
uci set network.wgpeer.endpoint_port='51820'
uci set network.wgpeer.allowed_ips='10.10.0.0/24'
uci set network.wgpeer.persistent_keepalive='25'
uci set network.wgpeer.route_allowed_ips='1'
uci commit network
MTU 写 1420 是有来历的:1500 减去 PPPoE 的 8 字节,再减掉 WireGuard 的封装开销,留点余量就是 1420。如果 PPPoE 环境下 MTU 按 1500 写,会出现很迷惑的症状——ping 能通、SSH 能敲,但网页就是打不开,因为大包被静默丢了。
坑三:握手通了,但内网进不去
配完重载,两边 wg show 都不难看——握手成功、allowed ips 也对,服务器 ping 路由器的 10.10.0.3 也通。我看着觉得成了,然后去访问 192.168.100.1,超时。
这个坑最费时间,因为所有"看着该对"的地方都是对的。真实原因是家庭防火墙的 forward 策略是 REJECT,而 wg0 不属于任何区域,从 wg 进来的流量到不了 lan。得给它单开一个区域,并放行两个方向:
# 建一个 wg 区域,把 wg0 挂进去
uci set firewall.wg=zone
uci set firewall.wg.name='wg'
uci set firewall.wg.input='ACCEPT'
uci set firewall.wg.output='ACCEPT'
uci set firewall.wg.forward='ACCEPT'
uci set firewall.wg.masq='1'
uci add_list firewall.wg.network='wg0'
# 双向放行
uci set firewall.wg2lan=forwarding
uci set firewall.wg2lan.src='wg'
uci set firewall.wg2lan.dest='lan'
uci set firewall.lan2wg=forwarding
uci set firewall.lan2wg.src='lan'
uci set firewall.lan2wg.dest='wg'
uci commit firewall
这里还有个容易忽略的点:给 wg 区域加了 masq='1'。不带这个的话,内网设备收到来自 10.10.0.0/24 的包,回包时不知道该往哪送——它没有那条路由。开了 NAT 之后,内网设备看到的就是路由器自己的地址,原路返回即可。
坑四:第一次连不上是正常的
配完之后我盯着 wg show 看了很久,一直是这个状态:
peer: <服务器公钥>
endpoint: 你的服务器IP:51820
allowed ips: 10.10.0.0/24
transfer: 0 B received, 2.02 KiB sent <-- 只发不收
路由器在发,服务器在等,两边对不上。我一度以为是云服务器安全组配错了,去控制台翻了两遍确认是 UDP 51820 放行的。后来抓包才看明白:包确实从家里发出去了,只是还没到服务器。
真正的原因是家庭 PPPoE 出口的 NAT 映射需要几次 keepalive 才能建立起来(NAT映射需要周期性、持续地发送,直到不需要这个映射)。配置里的 PersistentKeepalive = 25 会每 25 秒发一次,等一两分钟它自己就通了。后来再遇到这个状态我就不折腾了,泡杯茶回来就好。

通了之后是这样,服务器能直接摸到家里的每一台设备:

客户端怎么用
这块反而是整个流程里最省事的。客户端配置只有十几行,长这样:
# client1.conf
[Interface]
PrivateKey = <客户端私钥>
Address = 10.10.0.2/24
DNS = 192.168.100.1
[Peer]
PublicKey = <服务器公钥>
Endpoint = 你的服务器IP:51820
AllowedIPs = 10.10.0.0/24, 192.168.100.0/24
PersistentKeepalive = 25

注意 AllowedIPs 这一行。我没有写 0.0.0.0/0。这意味着只有访问家里内网的流量才走隧道,平时上网还是走本地网络。那台云服务器只有 4M 带宽,拿它当全局代理会卡到没法看视频,没必要。
手机上装官方 WireGuard 客户端,扫码导入就行——服务器上用 qrencode 把配置直接渲染成二维码,比手动输密钥靠谱得多:
qrencode -t png -s 10 -m 4 -o client.png < client.conf
顺便说个安卓上的小坑。官方客户端对导入的 .conf 文件名长度有限制,太长会报 Invalid name,而且不告诉你为什么——应用商店评论区里为这个吵了好几轮。文件名起短点就好,比如 client1.conf 这种。
还有个现实问题:国内网络下 download.wireguard.com 和 F-Droid 都不太好访问,我实测解析出来指向的是一个完全不相干的 IP 段。所以如果也打算装,先把安装包准备好,或者干脆连上隧道之后再下载。
设备多了之后,写了个小脚本管
手工分配地址、生成密钥、拼配置、再生成二维码,两三台还行,多了必出错。我在服务器上放了个脚本统一处理:
wg-client list # 列出所有设备与连接状态
wg-client add iphone # 新增:自动分配地址 + 生成配置 + 出二维码
wg-client remove iphone # 吊销(手机丢了就用这个)
wg-client qr iphone # 重新出二维码
地址从 .4 开始自动往后分,跳过已经占用的,也跳过保留给服务器和路由器的那几个。list 会顺带告诉每一台谁连着——比对着 wg show 里一串 base64 公钥猜人是谁舒服多了。
写的时候有个细节值得记一下:吊销必须同时改两个地方。只 wg set 把 peer 从运行态拿掉,wg0.conf 里还留着,重启一次设备就"复活"了;反过来只改配置文件不重载,运行态也还连着。所以我每次改完都拿这两边做一次比对,公钥列表必须一模一样才算数。

安全上我做的几件事
| 项目 | 做法 |
|---|---|
| 家庭侧入站 | 零开放,所有连接由路由器主动外连发起 |
| 隧道密钥 | 在 authorized_keys 里加了 restrict,port-forwarding,就算私钥泄露也拿不到 shell |
| SSH 认证 | 服务器关掉了密码登录,只认密钥 |
| WireGuard | 没有正确密钥的探测包被静默丢弃,不返回任何响应 |
| 暴露面 | 公网只多了两个端口:TCP 8022 和 UDP 51820 |
那个 restrict 前缀值得单独说一下。它在语义上等于"这个密钥只能做端口转发,别的什么都别想"——不给 pty、不给 agent 转发、不给 X11、不给执行命令。这样即使哪天私钥落到别人手里,对方也只能在服务器上开个转发端口,看不到服务器上任何文件。
4M 带宽能干什么
这点必须把预期说清楚。4Mbps 换算过来大概 500KB/s,它不是远程办公通道,是应急管理通道。
| 用途 | 4M 下的实际体验 |
|---|---|
| SSH 敲命令 | 完全无感 |
| 打开 LuCI 管理页 | 首屏 1–3 秒,能忍 |
| 看 Netdata 监控面板 | 图表比较大,要转几秒圈 |
| 远程桌面 | 基本不可用 |
| 传文件 | 1GB 大概 35 分钟 |
所以别指望这条路能当日常出口。它的价值在于:人在外面发现家里网络出问题,能进去看一眼、改个配置、重启个服务。这个定位下 4M 完全够用,也别为了它去升级带宽——不值当。
五个坑汇总
| 现象 | 真实原因 | 解法 |
|---|---|---|
dbclient: String too long | dropbear 解析不了 OpenSSH 格式私钥 | 用 dropbearkey 在本机生成 |
| 隧道建好但连接被 reset | 转发目标写了 127.0.0.1,而 dropbear 只监听 br-lan 地址 | 目标改成 192.168.100.1:22 |
| 握手成功但访问不了内网 | wg0 不属于任何防火墙区域,forward 被 REJECT | 建 wg 区域 + 双向放行 + 开 masq |
| 只发不收,看着像没连上 | PPPoE 的 NAT 映射还没建立 | 等 1–2 分钟,靠 keepalive 自愈 |
| ping 通但网页打不开 | MTU 过大,大包被静默丢弃 | wg0 的 MTU 设成 1420 |
回头看,前两个坑都属于"配置看着生效了其实没生效",第三个属于"能验证的地方全验证过了,但还是不通"。这类问题有个共同点:光看配置和状态是查不出来的,得抓包看包到底走到哪了。我在路由器 WAN 口和服务器上各抓了一次,两次都是同一分钟内定位的,比对着配置文件反复看高效太多。
顺便推荐一下这个习惯:抓包之前先想清楚"这个包该出现在哪个位置"。比如 WireGuard 那个坑,我先在路由器出口抓——有包,说明家里侧没问题;再到服务器抓——没包,说明卡在中间链路上。两步就把范围缩到了安全组和运营商,不用瞎猜。
万一哪天要撤掉
这种东西必须留后路。我把所有改动都限制在几个可以单独撤销的点上,动配置文件之前先存一份带时间戳的副本,这样哪一步出问题都能单独退回去,不用整台机器重来。
# 通道 A · 路由器侧
kill $(pidof dbclient)
/etc/init.d/reverse-tunnel disable
rm -f /etc/init.d/reverse-tunnel /usr/bin/tunnel-runner
# 通道 A · 服务器侧(恢复 GatewayPorts)
rm /etc/ssh/sshd_config.d/50-gatewayports.conf && systemctl restart sshd
# 通道 B · 删掉 UCI 配置即可
uci -q delete network.wg0; uci -q delete network.wgpeer
uci -q delete firewall.wg; uci -q delete firewall.wg2lan
uci -q delete firewall.lan2wg
uci commit network; uci commit firewall
/etc/init.d/network reload; /etc/init.d/firewall reload
通道 B 我特意全程用 UCI 配,就是为了撤的时候能一行行删干净。如果当时图省事直接编辑 wg0.conf 和防火墙的配置文件,撤销时就得手工挑行,漏一行就是一个查不出来的隐患。多花的那点时间,在这种地方是划算的。
最后
现在这两条通道是并着的。日常用 WireGuard,出门在外笔记本连上就能进 NAS 拿文件;SSH 隧道留着保底,WireGuard 万一哪天抽风,还能从 8022 进去看看怎么回事。两条加起来公网只多开了两个端口,我觉得这个代价可以接受。
如果你手边也有一台闲着的小机器,我建议从 SSH 反向隧道开始——十几分钟就能验证"路由器能不能连出来"这件事。这一步通了,后面的组网只是在同一套信任关系上加配置,心里会踏实很多。真有卡住的地方,先抓包,别对着配置文件反复看。


Comments NOTHING