两台 Ubuntu 打洞组 OSPF 全记录:从零到双隧道冗余

两台 Ubuntu 打洞组 OSPF 全记录:从零到双隧道冗余

家里有两台 Ubuntu 都在 NAT 后面,想组一个带冗余的私有网络。中间还有个 OpenWrt 硬路由也顺手拉进了 OSPF。本文记录了完整踩坑过程。


一、背景

三台设备:

设备系统角色
服务器 AUbuntu 24.04有公网端口映射,作为隧道服务端
服务器 BUbuntu 24.04纯 NAT 后面,只能主动出站
路由器 COpenWrt 23.05 (MT7621)B 的网关,想拉进 OSPF

A 和 B 之间已经有一条 WireGuard 隧道,但走的是中转 VPS,延迟 48ms,带宽不稳。目标:

  1. 用 A 的公网端口映射打直连隧道(不用 WG 中转)
  2. 两条异构隧道互为备份(TCP + QUIC/UDP)
  3. OSPF 自动切换,一条断了不影响
  4. 顺便把路由器 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=tun0

4.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 接口配上 IP
  • tun-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/hysteria

5.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:19800

A 侧 socat 桥接:

socat TCP-LISTEN:19800,reuseaddr,fork TUN:10.99.1.1/30,iff-up,tun-name=tun-hy

5.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 ruleip 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-ID192.168.25.45
Area0.0.0.77
AuthNone
Hello/Dead10s / 40s
网络类型broadcast
DR 优先级128
WG 接口 IP192.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 available

WireGuard 不支持出方向多播。 原理解析:

  1. BIRD 通过 raw socket 构造 OSPF 包,目的地址 224.0.0.5
  2. tcpdump 在 WG 加密之前抓到包(所以能看到)
  3. WG 收到包后,查 AllowedIPs 表找 224.0.0.5 对应的 peer——当然没有
  4. 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 卡 ExStartMTU 不一致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 SSL19ms~42MbpsTCP/TLSptp
Hysteria225ms~40MbpsQUIC/UDPptp
WG → ROS21ms~40MbpsUDPNBMA unicast
WG 中转(原,不通OSPF)48ms~40MbpsUDP

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+ 条

十一、总结

核心经验

  1. socat TUN + SSL 是性价比最高的点对点隧道方案。零依赖、systemd 一行命令、重启不掉。适合 50Mbps 以下场景。

  2. WG 上跑 OSPF = NBMA 是唯一答案。WG 拦截出方向多播,broadcast 和 ptp 都不行。必须双方都切 NBMA + 显式 neighbor。我们为此试了六种方案才最终确认。

  3. MTU 不一致会卡 OSPF DBD 交换。WG 接口 MTU 降到 1420 是必须的。BIRD 2.14 不支持 mtu 关键字,只能用 ip link set

  4. OSPF 软件选择:FRR 报错信息更友好(直接告诉你 WG 丢多播),BIRD 配置更简洁。如果环境复杂推荐先上 FRR 排障再切 BIRD。

  5. 单边 NBMA 不行,两边都得切。这个容易被忽略——你以为自己发了 unicast,但对端还在发多播,BIRD NBMA 模式下收不到多播包。

  6. Hysteria2 做备用隧道很合适。QUIC 在部分运营商那里 UDP 限速更宽松。但它的 TUN 支持不完善(v2.6~v2.9),用 TCP 转发 + socat 桥接更稳。

  7. OpenWrt 跑 BIRD 1.6 完全没问题。120MB RAM 的 MT7621 跑全网 OSPF 内存占用不到 2MB。

  8. 持久化 > 手动命令。花 10 分钟写 systemd service,比每次重启手动拉隧道强一百倍。

还能玩什么

  • 加 VXLAN 让两边的 Docker/LXC 容器互相通信
  • 在隧道上跑 BGP 而不是 OSPF(如果有跨 AS 需求)
  • 把 Hysteria2 的 Brutal CC 打开对抗 UDP 限速
  • 加第三条隧道(比如 WireGuard over WebSocket)防止 TCP 和 UDP 同时被墙
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇