文章目录[隐藏]
两台 Ubuntu 打洞组 OSPF 全记录:从零到双隧道冗余
家里有两台 Ubuntu 都在 NAT 后面,想组一个带冗余的私有网络。中间还有个 OpenWrt 硬路由也顺手拉进了 OSPF。本文记录了完整踩坑过程。
一、背景
三台设备:
| 设备 | 系统 | 角色 |
|---|---|---|
| 服务器 A | Ubuntu 24.04 | 有公网端口映射,作为隧道服务端 |
| 服务器 B | Ubuntu 24.04 | 纯 NAT 后面,只能主动出站 |
| 路由器 C | OpenWrt 23.05 (MT7621) | B 的网关,想拉进 OSPF |
A 和 B 之间已经有一条 WireGuard 隧道,但走的是中转 VPS,延迟 48ms,带宽不稳。目标:
- 用 A 的公网端口映射打直连隧道(不用 WG 中转)
- 两条异构隧道互为备份(TCP + QUIC/UDP)
- OSPF 自动切换,一条断了不影响
- 顺便把路由器 C 也接入 OSPF
二、网络环境分析
A 的公网端口映射:public.example.com:38525 → A 的内网 TCP+UDP
先做连通性探测:
# A 监听,B 主动发 UDP
# B → A 的映射端口
$ nc -u -l -p 38525 # A 监听
$ echo "hello" | nc -u -w1 public.example.com 38525 # B 发送
# ✅ A 收到了 "hello"结论:B → A 的 UDP 单向打通。A → B 不行(B 没有端口映射)。隧道必须是 B 主动连接 A 的模式。
三、方案设计
┌─────────────────────┐ ┌─────────────────────┐
│ A (有公网映射) │ │ B (纯NAT) │
│ │ ═══①══│ │
│ tun0: 10.99.0.1 │ SSL │ tun0: 10.99.0.2 │
│ 19ms │ TCP │ │
│ │ ═══②══│ │
│ tun-hy: 10.99.1.1 │ QUIC │ tun-hy: 10.99.1.2 │
│ 25ms │ UDP │ │
└──────────────────────┘ └──────────────────────┘两条隧道跑在同一个公网端口上(TCP 和 UDP 互不冲突),物理上同一条链路但协议栈完全不同——运营商 QoS 策略可能差别对待 TCP 和 UDP,一条被限另一条还能跑。
四、隧道一:socat TUN over SSL/TCP
这是最稳的方案,零额外依赖(socat + openssl 都是系统自带)。
4.1 生成自签证书
openssl req -x509 -newkey rsa:2048 \
-keyout /etc/tunnel/key.pem \
-out /etc/tunnel/cert.pem \
-days 3650 -nodes -subj "/CN=tunnel.local"4.2 A 端(服务端)
socat OPENSSL-LISTEN:38525,reuseaddr,fork,\
cert=/etc/tunnel/cert.pem,key=/etc/tunnel/key.pem,verify=0 \
TUN:10.99.0.1/30,iff-up,tun-name=tun04.3 B 端(客户端)
socat TUN:10.99.0.2/30,iff-up,tun-name=tun0 \
OPENSSL:public.example.com:38525,verify=0跑起来后 ping 10.99.0.2,通了。延迟 19ms,对比原来的 WG 中转 48ms 直接减半。带宽 ~42Mbps(接近 B 的上行 50M 上限)。
socat 的参数要点:
fork允许多个客户端同时连接(虽然我们就一个)reuseaddr重启时不会因为 TIME_WAIT 起不来iff-up让内核自动给 TUN 接口配上 IPtun-name=tun0固定接口名,方便后续 OSPF 配置引用
五、隧道二:Hysteria2 QUIC/UDP
Hysteria2 是 Go 写的 QUIC 隧道,自带拥塞控制(Brutal CC),专门对抗 UDP 限速。这里没有用它的代理功能,只用了 TCP 转发能力。
5.1 架构
Hysteria2 本身不创建内核 TUN,所以需要用 socat 做桥接:
B(client) A(server)
socat TUN ──TCP──► hy2 ──QUIC──► hy2 ──TCP──► socat TUN
10.99.1.2 :19900 :38525 :19800 10.99.1.1数据流:内核 TUN → socat → TCP:19900 → hy2 隧道 → TCP:19800 → socat → 内核 TUN
5.2 下载 Hysteria2
# 两边都做
curl -sLo /usr/local/bin/hysteria \
https://github.com/apernet/hysteria/releases/download/app/v2.9.3/hysteria-linux-amd64
chmod +x /usr/local/bin/hysteria5.3 A 端配置
# /etc/hysteria/server.yaml
listen: :38525 # UDP
tls:
cert: /etc/tunnel/cert.pem
key: /etc/tunnel/key.pem
auth:
type: password
password: <16字节随机hex>
tcpForwarding:
- listen: 19800 # hy2 把隧道里的 TCP 流转发到这里
remote: 127.0.0.1:19800A 侧 socat 桥接:
socat TCP-LISTEN:19800,reuseaddr,fork TUN:10.99.1.1/30,iff-up,tun-name=tun-hy5.4 B 端配置
# /etc/hysteria/client.yaml
server: public.example.com:38525
tls:
sni: tunnel.local
insecure: true
auth: <同密码>
tcpForwarding:
- listen: 127.0.0.1:19900
remote: 127.0.0.1:19800 # 注意:这里的 remote 是 hy2 对端地址B 侧 socat 桥接:
socat TUN:10.99.1.2/30,iff-up,tun-name=tun-hy TCP:127.0.0.1:19900踩坑:Hysteria2 v2.6.1 和 v2.9.3 的配置格式有差异。
tun.address在 v2.6 用ipv4:map,v2.9 用 array。我们最终用了 TCP 转发模式绕过了 TUN 配置差异,各版本通用。
六、持久化:systemd 服务
隧道跑通了只是第一步,重启得自动起来。
# A 端: /etc/systemd/system/tun-tcp-server.service
[Unit]
Description=socat TUN SSL tunnel (server)
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/socat OPENSSL-LISTEN:38525,reuseaddr,fork,\
cert=/etc/tunnel/cert.pem,key=/etc/tunnel/key.pem,verify=0 \
TUN:10.99.0.1/30,iff-up,tun-name=tun0
Restart=always
RestartSec=5
AmbientCapabilities=CAP_NET_ADMIN四组 socat 服务 + 两组 hysteria 服务,全部 systemctl enable。依赖关系:
tun-tcp-* 独立启动(socat 自带重连)
hy-* 独立启动(hysteria 自带重连)
tun-hy-* After=hy-* (等 TCP 转发端口就绪)
bird After=network-online七、OSPF 配置
选型:BIRD 2.14。FRR 太重,BIRD 简洁高效。
7.1 A 端 BIRD 配置
# /etc/bird/bird.conf
router id 192.168.24.17;
protocol device { scan time 10; }
protocol direct {
ipv4;
interface "tun0", "tun-hy"; # 隧道接口
}
protocol kernel {
kernel table 77; # OSPF 路由进独立路由表
ipv4 {
import none;
export filter {
if source = RTS_OSPF then accept;
if source = RTS_OSPF_IA then accept;
if source = RTS_OSPF_EXT then accept;
reject;
};
};
}
protocol ospf v2 {
ipv4 { export none; };
area 0.0.0.77 {
interface "tun0" {
type ptp; cost 5; hello 5; dead 20;
};
interface "tun-hy" {
type ptp; cost 8; hello 5; dead 20;
};
};
}几个关键点:
export none:A 只收路由不发布(保留路由控制权)kernel table 77:OSPF 路由进独立表,不污染 main 表cost 5 / cost 8:主隧道 cost 更低,OSPF 默认走主隧道。主隧道断了自动切备隧道。- 独立的 ip rule:
ip rule add prio 200 from all lookup 77,放在代理软件之前
7.2 B 端 BIRD 配置
router id 192.168.24.102;
protocol ospf v2 {
ipv4 {
export where source = RTS_DEVICE; # 发布直连网段
};
area 0.0.0.77 {
interface "tun0" { type ptp; cost 5; hello 5; dead 20; };
interface "tun-hy" { type ptp; cost 8; hello 5; dead 20; };
interface "enp5s0" { type broadcast; cost 1; }; # LAN 侧
};
}B 作为路由中转,把 OSPF 学到的路由注入 kernel main 表,同时把本地直连网段发布给 A。
7.3 验证
$ birdc show ospf neighbor
Router ID Pri State Interface
192.168.24.102 1 Full/PtP tun0 10.99.0.2 # 主隧道
192.168.24.102 1 Full/PtP tun-hy 10.99.1.2 # 备隧道两个 Full,完美。
八、WG + ROS OSPF 调试全记录
A 和 B 之间的隧道是新搭的,但 A 本来就有一条 WG 隧道连接着一台 MikroTik RouterOS(后文简称 ROS)。它已经配置好了 OSPF,参数是:
| 参数 | 值 |
|---|---|
| Router-ID | 192.168.25.45 |
| Area | 0.0.0.77 |
| Auth | None |
| Hello/Dead | 10s / 40s |
| 网络类型 | broadcast |
| DR 优先级 | 128 |
| WG 接口 IP | 192.168.24.18/30 |
目标:让 A 的 BIRD 跟 ROS 建立 OSPF 邻接,从而学到 ROS 背后的几十条路由。
听起来很简单——两边都配好 area 0.0.0.77,broadcast 模式,等着 Full 就行了。结果搞了整整一个小时。
第一回合:broadcast → 卡 Init
第一版配置:
protocol ospf v2 {
area 0.0.0.77 {
interface "3389-txy2" {
type broadcast;
hello 10; dead 40;
};
};
}抓包确认双向 Hello:
$ sudo tcpdump -i 3389-txy2 -n 'proto 89' -v
# ROS → 224.0.0.5: Hello, Router-ID 192.168.25.45, Area 0.0.0.77
# 我们 → 224.0.0.5: Hello, Router-ID 192.168.24.17, Area 0.0.0.77参数完全一致:Area 匹配、Auth 匹配、Hello/Dead 匹配、Mask 匹配。但邻接永远 Init。
关键细节藏在 ROS 的 Hello 包里:
192.168.24.18 > 224.0.0.5: OSPFv2 Hello
Router-ID 192.168.25.45
Neighbor List: # ← 空的!ROS 没看到我们而我们的 Hello:
192.168.24.17 > 224.0.0.5: OSPFv2 Hello
Router-ID 192.168.24.17
Neighbor List: 192.168.25.45 # ← 我们看到 ROS 了OSPF 建立邻居需要双向看到对方。ROS 的邻居列表始终为空——说明 ROS 根本没收到我们的 Hello。
但是 tcpdump 明明抓到我们发出去了啊?
第二回合:定位根因——WG 吃掉多播
这个问题困扰了好一阵。直到换了 FRR(另一个路由软件)做对照实验,FRR 直接报出了真相:
sendmsg in ospf_write failed to 224.0.0.5, interface wg0: Required key not availableWireGuard 不支持出方向多播。 原理解析:
- BIRD 通过 raw socket 构造 OSPF 包,目的地址
224.0.0.5 tcpdump在 WG 加密之前抓到包(所以能看到)- WG 收到包后,查 AllowedIPs 表找
224.0.0.5对应的 peer——当然没有 - WG 丢弃包,对端永远收不到
注意:入方向多播是正常的(ROS 的 Hello 能收到),因为 WG 解密后直接交给内核协议栈。问题只在出方向。
第三回合:ptp 也没用
试试切换到 ptp 模式(ptp 也不依赖多播对吧?):
interface "3389-txy2" {
type ptp;
hello 10; dead 40;
};还是 Init。因为 BIRD 的 ptp 模式依然往 224.0.0.5 发 Hello,只是省掉了 DR/BDR 选举。包还是被 WG 扔了。
第四回合:各种 hack 尝试
接下来的一个小时进入了经典阶段——明知问题在哪,但就是不认命:
- iptables DNAT 劫持:把 OUTPUT 链里目的地址
224.0.0.5的 OSPF 包 DNAT 成192.168.24.18。没效果——DNAT 对本地生成的 raw socket 包不生效。 - nft mangle OUTPUT:同样,本地生成的 OSPF 包不走 netfilter OUTPUT 链的大部分 hook。
- 手动
ip maddr add:想在 WG 接口上强制加入多播组,让内核把收到的多播包发给 BIRD socket。IP_ADD_MEMBERSHIP报错Required key not available。 - BIRD NBMA + ROS broadcast:BIRD 发 unicast Hello 到 ROS,ROS 能收到并回复,但 ROS 的 Hello 还是多播过来,BIRD NBMA 模式下收不到多播 Hello。
第五回合:NBMA 双向 Unicast——曙光
真正的破局点是 让 ROS 也切到 NBMA。NBMA(Non-Broadcast Multi-Access)模式下,双方都用 unicast 发包:
interface "3389-txy2" {
type nonbroadcast;
neighbors {
192.168.24.18 eligible;
};
hello 10; dead 40;
};BIRD 往 192.168.24.18 直接发 unicast Hello。ROS 那边也要切 NBMA 并配置 neighbor 192.168.24.17。
切完之后抓包——全是 unicast:
192.168.24.17 > 192.168.24.18: OSPFv2 Hello ✅ unicast 发出
192.168.24.18 > 192.168.24.17: OSPFv2 Hello ✅ unicast 收到邻接状态立刻跳到了 ExStart——数据库描述(DBD)交换开始了!
第六回合:MTU 血案
高兴了不到五秒。DBD 交换一直在重传,日志刷屏:
ospf1: MTU mismatch with nbr 192.168.25.45 (remote 1420, local 8920)ROS 的 WG 接口 MTU 是 1420(WG 标准传输 MTU),而我们的是 8920(内核默认给 WG 的最大值)。OSPF DBD 包要求 MTU 一致,否则直接丢包。
解决:
ip link set dev 3389-txy2 mtu 1420
systemctl restart bird邻接终于 Full。
$ birdc show ospf neighbor
Router ID Pri State Interface
192.168.25.45 128 Full/DR 3389-txy2 192.168.24.18瞬间学到 30+ 条路由,table 77 填满了。
最终可工作的 WG OSPF 配置模板
A 端(BIRD 2.14):
protocol ospf v2 {
ipv4 { export none; };
area 0.0.0.77 {
interface "wg-interface-name" {
type nonbroadcast;
neighbors {
<对端WGIP> eligible;
};
hello 10; dead 40;
};
};
}ROS 端(MikroTik RouterOS):
/routing ospf interface
set [find interface=wg-interface-name] network-type=nbma
/routing ospf nbma-neighbor
add address=<对端WGIP>核心教训
| 踩坑 | 根因 | 解法 |
|---|---|---|
| OSPF Init 不前进 | WG 丢弃出方向多播 | NBMA unicast |
| ROS 不认邻居 | 同上,且必须双方都 NBMA | 两边都切 |
| DBD 卡 ExStart | MTU 不一致 | ip link set mtu 1420 |
| FRR/BIRD 二选一 | FRR 多播报错更清晰 | BIRD 配置更简洁 |
记住:在任何隧道接口(WG、GRE、IPIP 等)上跑 OSPF,第一反应就应该是 NBMA 或 ptp,不要指望多播。如果对端是 MikroTik,记得两边都要切——单边 NBMA 没用。
九、拉 OpenWrt 入伙
路由器 C(MT7621, 120MB RAM)是 B 的网关。跑 BIRD 完全够用。
安装
# BIRD 1.6.8 for mipsel_24kc(OpenWrt 23.05)
wget https://downloads.openwrt.org/releases/23.05.5/packages/mipsel_24kc/routing/bird1-ipv4_1.6.8-2_mipsel_24kc.ipk
opkg install bird1-ipv4_1.6.8-2_mipsel_24kc.ipk配置
# /etc/bird4.conf
router id 192.168.24.82;
protocol ospf {
export where source = RTS_DEVICE;
area 0.0.0.77 {
interface "br-lan" {
type broadcast; # LAN 上有交换机,broadcast 正常
priority 0; # 不参与 DR 选举
hello 10; dead 40;
};
};
}B 和 C 在同一 LAN 上,直接用 broadcast,1ms 延迟。比走 WG relay 快 70 倍。
自启
/etc/init.d/bird4 enable
ls /etc/rc.d/*bird4*
# S99bird4 → ../init.d/bird4 ✅十、最终效果
OSPF area 0.0.0.77
┌───────────────────────────────────────────────────────────┐
│ │
│ ROS (MikroTik) ─── NBMA/WG ─── A │
│ 192.168.25.45 21ms (tun0 + tun-hy) │
│ (30+ stubnets) │ │
│ ① socat SSL 19ms, 42Mbps │
│ ② Hysteria2 QUIC 25ms │
│ │ │
│ B ─── LAN(broadcast) ── C│
│ <1ms │
│ OpenWrt 192.168.24.82│
└───────────────────────────────────────────────────────────┘性能对比:
| 隧道 | 延迟 | 带宽 | 协议 | OSPF 类型 |
|---|---|---|---|---|
| socat SSL | 19ms | ~42Mbps | TCP/TLS | ptp |
| Hysteria2 | 25ms | ~40Mbps | QUIC/UDP | ptp |
| WG → ROS | 21ms | ~40Mbps | UDP | NBMA unicast |
| WG 中转(原,不通OSPF) | 48ms | ~40Mbps | UDP | — |
OSPF 邻接 6 条,全网路由 35+ 条。任意一条隧道断,OSPF 在 20 秒内自动收敛。
此时拔掉主隧道测试故障切换:
# A 端
$ systemctl stop tun-tcp-server
# OSPF 自动收敛
$ birdc show ospf neighbor
# tun0 的 Full 消失,tun-hy 还在,通往 B 的路由切到 10.99.1.2
$ ip route show table 77
# 所有下一跳自动从 tun0 变成 tun-hy,不丢包恢复主隧道:
$ systemctl start tun-tcp-server
# OSPF 自动切回(tun0 cost=5 < tun-hy cost=8),无感知时间线回顾
○ 00:00 探测公网端口映射 UDP 可达性 → ✅
○ 00:15 socat SSL 隧道打通 → 19ms ✅
○ 00:30 Hysteria2 QUIC 隧道打通 → 25ms ✅
○ 00:40 双隧道 systemd 持久化 ✅
○ 01:00 BIRD OSPF 部署,隧道间邻接 Full ✅
● 02:00 WG + ROS OSPF 全剧终 ─────────────────
│ ├ broadcast → Init
│ ├ ptp → Init
│ ├ iptables DNAT hack → 无效
│ ├ nft mangle → 语法报错
│ ├ 手动 maddr → Required key not available
│ ├ NBMA单边 → 收不到ROS多播Hello
│ ├ FRR对照实验 → 证实WG丢弃多播
│ └ ROS切NBMA + MTU 1420 → Full!
○ 03:00 OpenWrt BIRD 1.6 编译安装 → OSPF Full
○ 03:30 故障切换测试通过,全网路由 35+ 条十一、总结
核心经验
socat TUN + SSL 是性价比最高的点对点隧道方案。零依赖、systemd 一行命令、重启不掉。适合 50Mbps 以下场景。
WG 上跑 OSPF = NBMA 是唯一答案。WG 拦截出方向多播,broadcast 和 ptp 都不行。必须双方都切 NBMA + 显式 neighbor。我们为此试了六种方案才最终确认。
MTU 不一致会卡 OSPF DBD 交换。WG 接口 MTU 降到 1420 是必须的。BIRD 2.14 不支持
mtu关键字,只能用ip link set。OSPF 软件选择:FRR 报错信息更友好(直接告诉你 WG 丢多播),BIRD 配置更简洁。如果环境复杂推荐先上 FRR 排障再切 BIRD。
单边 NBMA 不行,两边都得切。这个容易被忽略——你以为自己发了 unicast,但对端还在发多播,BIRD NBMA 模式下收不到多播包。
Hysteria2 做备用隧道很合适。QUIC 在部分运营商那里 UDP 限速更宽松。但它的 TUN 支持不完善(v2.6~v2.9),用 TCP 转发 + socat 桥接更稳。
OpenWrt 跑 BIRD 1.6 完全没问题。120MB RAM 的 MT7621 跑全网 OSPF 内存占用不到 2MB。
持久化 > 手动命令。花 10 分钟写 systemd service,比每次重启手动拉隧道强一百倍。
还能玩什么
- 加 VXLAN 让两边的 Docker/LXC 容器互相通信
- 在隧道上跑 BGP 而不是 OSPF(如果有跨 AS 需求)
- 把 Hysteria2 的 Brutal CC 打开对抗 UDP 限速
- 加第三条隧道(比如 WireGuard over WebSocket)防止 TCP 和 UDP 同时被墙