共计 6094 个字符,预计需要花费 16 分钟才能阅读完成。
文章目录
一、家里那台 Ubuntu,一出门就成了孤岛
我家书房有台 Ubuntu 24.04,是我的 " 主力工位 ":开发环境、Docker 容器、自建 Git 仓库、共享盘,全在它身上,局域网地址 192.168.3.10。
在家时,它是瑞士军刀;一出门,它就成了 孤岛。
- 咖啡馆里想
ssh回去改一行配置——连不上; - 出差时想打开内网
192.168.3.x上的管理页面——打不开; - 用远程桌面软件?画质糊、延迟高,还得把屏幕交给第三方服务器转一圈。
我想要的其实很朴素:
无论在哪,手机、笔记本拨一下开关,就像坐在家里一样:
ssh直连,内网网页秒开;其他流量照常走本地网络,一点不绕路。
路由器能做端口映射,家宽有公网 IP,Ubuntu 24.04 的内核里本来就躺着一把好钥匙。于是我花了一个上午,把家里的内网 " 装进了口袋 "。
过程不复杂,但踩的三个坑,个个都能让人怀疑人生。先说方案,再说坑。
二、第一性原理:从外网 " 回家 ",只需要四块拼图
别急着搜教程。先把 " 远程回家 " 这件事拆到不能再拆:
| 拼图 | 要解决的问题 | 我的选择 |
|---|---|---|
| ① 门牌 | 外网怎么找到我家? | 公网 IP + 路由器端口映射 |
| ② 会搬家的门牌号 | 家宽 IP 隔几天就变 | DDNS 域名(下文用 home.example.com 代替) |
| ③ 只认钥匙的门 | 怎么保证只有我能进? | WireGuard 加密通道 |
| ④ 进门后的走廊 | 进了主机,怎么去内网其他设备? | IP 转发 + 地址伪装(MASQUERADE) |
第①块有个前提:路由器 WAN 口拿到的必须是真公网 IP。登录路由器看一眼 WAN 地址,如果落在 100.64.x.x ~ 100.127.x.x,说明你在运营商的 " 大内网 " 里,端口映射做了也白做——先给运营商打电话申请公网 IP。
第③块为什么选 WireGuard? 三个理由:
- 内核自带:Linux 5.6 起进入主线,Ubuntu 24.04 的 6.8 内核开箱即用,
modinfo wireguard就能看到它; - 代码极简:核心实现约 4000 行,配置文件十几行,没有证书体系那一套繁文缛节;
- " 装死 " 特性 :没有正确密钥的数据包,它 一个字节都不回。端口扫描器扫过来,看到的是一片死寂——你家这扇门,在外人眼里根本不存在。
拼起来就是这样:
手机 / 笔记本 ──UDP──▶ home.example.com:51820
│ 路由器端口映射
▼
192.168.3.10:51820(wg0,10.66.66.1)├─ 访问本机:10.66.66.1
└─ MASQUERADE 转发 ─▶ 内网其他设备 192.168.3.x
三、十分钟搭起来
1. 装工具、造钥匙
sudo apt install -y wireguard-tools qrencode
umask 077
wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub
umask 077 别省——私钥文件必须只有 root 能读。
2. 服务端配置 /etc/wireguard/wg0.conf
[Interface]
Address = 10.66.66.1/24
ListenPort = 51820
PrivateKey = <服务端私钥>
MTU = 1380
FwMark = 0xc8 # 坑一的解药,后面细说
PostUp = iptables -I FORWARD 1 -i %i -j ACCEPT
PostUp = iptables -I FORWARD 1 -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -s 10.66.66.0/24 -o enp7s0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT
PostDown = iptables -D FORWARD -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.66.66.0/24 -o enp7s0 -j MASQUERADE
[Peer]
PublicKey = <手机公钥>
AllowedIPs = 10.66.66.2/32
两个细节:
- MASQUERADE 的妙处 :手机访问内网设备时,源地址被改写成
192.168.3.10,内网设备以为是 " 自家人 " 在访问,回包自然回到主机, 路由器上一条静态路由都不用加。 #是雷区:wg-quick 会把每行#之后的内容全部当注释切掉。FwMark后面写注释无所谓,但PostUp命令里要是出现#,命令会被拦腰截断,还不报错。
3. 开转发、起服务
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
sudo systemctl enable --now wg-quick@wg0
4. 路由器:只映射一个 UDP 端口
外部 51820 → 192.168.3.10:51820,协议选 UDP。(伏笔,请记住这句话。)
SSH 之类的管理端口,一个都不用对外开。
5. 客户端:只让 " 回家的流量 " 进门
[Interface]
PrivateKey = <手机私钥>
Address = 10.66.66.2/32
MTU = 1380
[Peer]
PublicKey = <服务端公钥>
Endpoint = home.example.com:51820
AllowedIPs = 10.66.66.0/24, 192.168.3.0/24
PersistentKeepalive = 25
精髓在 AllowedIPs:只有访问 10.66.66.0/24 和家里 192.168.3.0/24 的流量才走加密通道,刷视频、聊微信照常走本地网络——分流,不绕路,不拖慢。
手机导入最省事的方式,是把配置变成二维码:
qrencode -t ansiutf8
终端里直接蹦出一个二维码,手机上的 WireGuard App 一扫即可。(二维码里有私钥,千万别截图发群。)
到这里,理论上已经完工了。
理论上。
四、坑一:回包被 " 截胡 "——本机的 TUN 网卡偷走了流量
动手之前,我习惯先摸一遍现场。这一摸,摸出了一个隐藏变量:这台机器上跑着一个 TUN 模式的网络工具(很多加速器、流量管理软件都是这个套路)。它创建一块虚拟网卡,再用策略路由把本机发出的流量统统接管过去:
$ ip route get 1.1.1.1
1.1.1.1 via 198.18.0.2 dev tun0 table 2022 src 198.18.0.1
问题来了:手机的握手包从物理网卡 enp7s0 进门,WireGuard 回包时内核一查路由——走 tun0。回包被那个软件接走,换了源地址和端口再发出去。手机收到一封 " 陌生人 " 的回信,握手当场失败。
打个比方:你从正门寄来一封信,回信却被小区代收点截下,换了个信封再寄给你——你当然不认。
解药:给包打标记 + 策略路由。
FwMark = 0xc8
PostUp = ip rule show | grep -q 'fwmark 0xc8 lookup main' || ip rule add fwmark 0xc8 lookup main priority 100
意思是:WireGuard 自己发出的加密 UDP 包,一律贴上 0xc8 标签;ip rule 这个 " 分诊台 " 看到标签,直接指去 main 主路由表、走物理网卡。优先级 100 排在 TUN 软件那一串 9000+ 规则之前,抢在它截胡之前放行:
$ ip rule
0: from all lookup local
100: from all fwmark 0xc8 lookup main ← 我们的
9000: from all to 198.18.0.0/30 lookup 2022
9002: not from all iif lo lookup 2022
...
32766: from all lookup main
前面那个 grep -q ... || 是为了 幂等:ip rule add 重复执行会叠出一模一样的规则,服务重启几次,规则表就成了千层饼。
效果立竿见影:
$ ip route get 1.1.1.1 # 不打标记:被 TUN 接管
1.1.1.1 via 198.18.0.2 dev tun0 table 2022 src 198.18.0.1
$ ip route get 1.1.1.1 mark 0xc8 # 打了标记:直连物理网卡
1.1.1.1 via 192.168.3.1 dev enp7s0 src 192.168.3.10 mark 0xc8
同类陷阱还有一个:如果 DDNS 脚本跑在这台机器上,它查到的 " 我的公网 IP" 可能是 TUN 那头的出口,域名会被更新成错误地址。解法同理——查 IP 时绑定物理网卡:curl --interface enp7s0 <查 IP 的网址>。
五、坑二:手机说 " 已连接 ",服务器说 " 从没见过你 "
iPhone 扫码,开关一拨,绿了——" 已连接 "。
打开 http://192.168.3.1——转圈,超时。
第一反应:转发没开?NAT 写错了?……先别猜,看证据:
$ sudo wg show wg0
interface: wg0
public key: qvPU...(略)
listening port: 51820
fwmark: 0xc8
peer: Cami...(略)
allowed ips: 10.66.66.2/32
这个输出 安静得可怕。一个健康的 peer 应该有三行:endpoint(对方从哪来)、latest handshake(最近一次握手)、transfer(收发了多少字节)。三行全没有——服务器从来没收到过手机的一个有效包。
冷知识:WireGuard 是无连接的。 客户端上的 " 已连接 ",只代表 " 开关打开了、虚拟网卡建好了 ",并不代表和服务器握过手。判断通没通,唯一标准是
latest handshake。
包在哪丢的?去物理网卡上蹲守:
$ sudo tcpdump -ni enp7s0 'udp port 51820'(一片寂静)
包根本没进家门,嫌疑锁定路由器。人在家里,怎么验证 " 外网进不来 "?做个 对照实验——利用路由器的 NAT 回环(从内网访问自己的公网 IP,路由器会把包绕回来):
# 对照组:一个已知正常的 TCP 映射
$ nc -z -s 192.168.3.10 <公网 IP> 4000
Connection to <公网 IP> 4000 port [tcp/*] succeeded!
# 实验组:WireGuard 的 UDP 映射,一边发一边抓
$ echo probe | nc -u -s 192.168.3.10 <公网 IP> 51820
$ sudo tcpdump -ni enp7s0 'udp port 51820 and dst host 192.168.3.10'(依然什么都没有)
TCP 能绕回来,UDP 不行。回头一看路由器——映射规则的协议栏,赫然写着 TCP。
对,就是第三节埋的那个伏笔。不少路由器新建映射时协议默认就是 TCP,一不留神就滑过去了。
改成 UDP,手机重连,抓包窗口瞬间热闹起来:
10:43:07.164907 IP 223.153.x.x.60112 > 192.168.3.10.51820: UDP, length 148
10:43:07.189524 IP 223.153.x.x.60112 > 192.168.3.10.51820: UDP, length 32
10:43:07.189567 IP 223.153.x.x.60112 > 192.168.3.10.51820: UDP, length 96
再看 wg show,三行全回来了:
peer: Cami...(略)
endpoint: 223.153.x.x:60112
allowed ips: 10.66.66.2/32
latest handshake: 5 seconds ago
transfer: 12.63 KiB received, 5.21 KiB sent
彩蛋:看包长就能认出握手。 第一个包 148 字节,正是 WireGuard 握手发起报文的固定长度:
| 字段 | 字节 |
|---|---|
| 消息类型 + 保留位 | 4 |
| 发送方索引 | 4 |
| 临时公钥 | 32 |
| 加密的静态公钥(含认证标签) | 48 |
| 加密的时间戳(含认证标签) | 28 |
| mac1 + mac2 | 32 |
| 合计 | 148 |
紧随其后的 32 字节,是 16 字节头部加 16 字节认证标签、不带任何数据的 " 确认包 ";96 字节才是真正的业务数据。在一堆 UDP 包里一眼认出握手,这感觉,像在人群里认出老朋友。
排查心法:分层确认,从下往上。 先看包到没到(tcpdump)→ 再看握手成没成(wg show)→ 最后才轮到转发和 NAT。一上来就改 iptables,只会越改越乱。
还有一条:别在家里 Wi-Fi 下测 " 外网访问 "。能不能通,取决于路由器支不支持 NAT 回环,结论不可信。关掉 Wi-Fi 用 4G/5G,才是真正的 " 出门在外 "。
六、坑三:门牌号搬家了,客户端还在敲旧门
家宽公网 IP 隔几天就变,DDNS 负责把域名指向新地址。但是:
WireGuard 只在启动时解析一次域名,之后记住的是 IP。
家里 IP 一变,客户端就对着旧地址 " 咚咚咚 " 敲门,永远没人应。
更阴的是 DNS 缓存。我实测遇到过:权威 DNS 早已是新 IP,本机的 DNS 缓存却先吐出一个过期的旧 IP——有些 DNS 实现在记录过期后,会先返回旧值、再在后台刷新。
Linux 客户端的解法:一个 systemd 定时器每分钟巡检,握手超过 135 秒没更新 就按域名重设一次 Endpoint(WireGuard 大约每 2 分钟重新握手,超过这个阈值基本可以判定断了,官方示例脚本也用这个值):
#!/usr/bin/env bash
set -euo pipefail
iface="$1"
wg show "$iface" >/dev/null 2>&1 || exit 0
endpoint="$(sed -n's/^Endpoint *= *//p'"/etc/wireguard/$iface.conf" | head -1)"read -r pub last <<(wg show"$iface" latest-handshakes)
(($(date +%s) - last > 135 )) || exit 0
wg set "$iface" peer "$pub" endpoint "$endpoint"
wg set ... endpoint 域名: 端口 会触发一次重新解析,配合 OnUnitActiveSec=1min 的 timer,断线自愈。
手机端:家里 IP 变了之后,断开重连一次即可。另外建议把 DDNS 记录的 TTL 调到 60 秒,搬家消息传得更快。
七、从 " 能用 " 到 " 好用 ":脚本化,外加一部 " 虚拟手机 "
手工搭一遍是学习,重复搭就是体力活。我把整套流程收进了一个小仓库:
tunnel/
├── tunnel.conf # 全部参数:端口、域名、网段、网卡、MTU
├── gen-client.sh # 一键生成客户端配置
├── scripts/setup-server.sh # 幂等部署服务端,重跑不丢已有设备
├── scripts/check.sh # 一键体检
├── client/init.sh # 另一台 Ubuntu:clone 后一条命令接入
└── tests/ # 单元测试 + 端到端测试
加一台设备:
./gen-client.sh win-laptop # 生成配置,并打印各平台的导入命令
./gen-client.sh iphone --qr # 手机直接扫码
加设备不断线 的关键是这一行:
wg syncconf wg0 <(wg-quick strip wg0)
wg-quick strip 把 Address、PostUp 这些 wg-quick 专属字段剥掉,wg syncconf 只把差异热更新进内核——新设备秒上线,已经连着的手机毫无感知。
体检也只要一条命令:
$ sudo ./scripts/check.sh
[PASS] wg0 接口已启动
[PASS] 开机自启 wg-quick@wg0
[PASS] UDP 51820 在监听
[PASS] ip_forward = 1
[PASS] fwmark 0xc8 流量直连 enp7s0(绕过 TUN)[PASS] FORWARD 放行 wg0 入向
[PASS] FORWARD 放行 wg0 回包
[PASS] MASQUERADE 10.66.66.0/24 → enp7s0
[PASS] DDNS home.example.com = 当前出口 IP
---- 客户端(名称 IP 最近握手)----
iphone 10.66.66.2 12 秒前
最好玩的是端到端测试:不出门、不用第二台设备,在同一台机器里造一部 " 虚拟手机 "。靠的是 Linux 的 network namespace——一个完全独立的网络世界,有自己的网卡、路由表和防火墙:
# 造一个网络世界,再牵一根 "网线"(veth 对)过去
ip netns add tunnel-e2e
ip link add tun-e2e-h type veth peer name tun-e2e-n
ip link set tun-e2e-n netns tunnel-e2e
#(省略给两头配地址、起网卡的几行)# 在 "虚拟手机" 里建 WireGuard 网卡,拨向宿主机
ip -n tunnel-e2e link add wgt type wireguard
ip netns exec tunnel-e2e wg set wgt private-key ./client.key \
peer <服务端公钥> endpoint 10.199.0.1:51820 \
allowed-ips 10.66.66.0/24,192.168.3.0/24
# 从 "虚拟手机" 里访问家里内网
ip netns exec tunnel-e2e ping -c2 192.168.3.1
这里藏着一个很妙的内核细节:WireGuard 网卡的 UDP 套接字,属于创建它时所在的那个 namespace。在 " 虚拟手机 " 里创建 wgt,它的加密包就从 " 虚拟手机 " 那根网线发出去——和一台真正的外部设备别无二致。
跑一遍:
[PASS] 隧道内访问服务端 10.66.66.1
[PASS] 隧道内访问本机内网 IP 192.168.3.10
[PASS] 转发 +NAT 访问内网其他设备(网关 192.168.3.1)[PASS] 隧道内 SSH 端口可达
[PASS] 服务端记录到握手
[PASS] 吊销后 peer 与客户端配置均已删除
[PASS] 吊销后隧道不可用
E2E 全部通过
家里另一台 Ubuntu 笔记本接入,更是两行搞定:
git clone <你的仓库地址> && cd tunnel
./client/init.sh
脚本会 SSH 回服务端生成配置、拉回本机、装好开机自启和 " 敲门重试 " 定时器,最后 ping 一下 10.66.66.1 验收。
八、安全清单:门要好用,更要结实
- 路由器只开 一个 UDP 端口,SSH 等管理端口一个都不对外;
- WireGuard 对无效包零响应,扫描器看到的只是一个黑洞;
- 私钥只放
/etc/wireguard(root,权限 600),仓库用.gitignore挡住*.key,二维码别截图外传; - 一台设备一把钥匙,手机丢了,一条命令吊销:
sudo scripts/remove-client.sh iphone; - 访问本机统一用
10.66.66.1:外面的网络恰好也是192.168.3.x的概率不低(不少路由器默认就是这个网段),用通道地址永不撞车; - 有些公司网络会封 UDP,这套方案在那种环境下会失灵,需要另想办法。
九、写在最后
回头看,这次真正的收获不是几份配置文件,而是两条心法。
第一,拆到底再动手。 " 远程回家 " 拆成门牌、门牌号、门锁、走廊四块拼图,每一块缺什么一目了然,不用在几十篇教程里大海捞针。
第二,证据先于猜测。 手机说 " 已连接 ",服务器说 " 从没见过你 "——这句话本身就是线索。wg show 说没握手,tcpdump 说包没到,对照实验说 TCP 通、UDP 不通,三条证据一串,真相只剩一个:路由器映射选错了协议。
顺带一提,这次我全程和 AI 结对:它先摸清现场,揪出了 TUN 网卡这个隐藏变量;又设计对照实验,把锅精准地甩给了路由器。我做的,主要是点头,以及去路由器后台改了一个下拉框。
现在,我在任何地方掏出手机拨一下开关,家里那台 Ubuntu 就在身边。
你是怎么 " 远程回家 " 的?远程桌面、内网穿透,还是自己动手搭?评论区聊聊。觉得有用,点个 " 在看 ",下次折腾的时候就不用从头搜了。
(环境:Ubuntu 24.04 LTS,内核 6.8;客户端:iOS / Windows / Ubuntu 24.04。文中域名、公网地址、密钥均已脱敏。)