我美好的寒假的第一天,毁于那多灾多难的 OpenWRT:当我第七七四十九次折腾 OpenWRT 上的隧道而他又莫名其妙的开始爆炸时,我怒了。

说的就是你,xl2tpd

我实在是搞不明白,在 Windows 下面轻轻松松跑个三四百兆的 L2TP 连接,怎么到了 OpenWRT 上下载就只有 20M 不到,上传一跑直接爆炸。

冥冥之中,有一股神秘的声音(其实是西瓜)在我耳畔低语:为什么不试试上虚拟化的 RouterOS 呢?

尽管西瓜一再抨击 Proxmox VE 而向我推销他的 Xen,但是因为我对 PVE 更加熟悉,最后还是选择了 PVE (事实证明这是好的)

于是我做出了初步的计划:在 PVE 上跑 RouterOS + OpenWRT;RouterOS 负责打隧道(PPPoE、L2TP、PPTP、GRE),OpenWRT 负责做透明代理;用 DHCP 对不同的设备分配不同的网关达到分流和故障控制的效果。

上网下了一个PVE,找了一个 6.44.2 的 L5 授权破解版(和卖盗版电子盘的原理有异曲同工之妙,都是改硬盘序列号),写盘、装系统、部署一气呵成,是 BGP Player 中的豪杰

后续 AJ 大哥告诉我 CHR 版本也可以无限白嫖,是一个更好的选择,不过这时候我已经配好了,反正都是一样的用,如果没有什么安全问题就不换 CHR 了。

RouterOS 的配置十分简单便捷,J1900 软路由的四个接口分别对应一个 Linux Bridge 再分配到 ROS 的 VM 上就行了。在这个阶段我只踩到了一个坑:ether5 上的 vlan33 里面有一台主机 192.168.6.99,但是我给 vlan33 配了 192.168.6.1/24 之后还是 ping 不通这台机子,torch 能看到这台机子往外发包。排查了十分钟之后我把接口的 loop detect 给关了,问题解决。

接下来是 OpenWRT,本来也打算用 VM 的,但是我思考了半秒,发现:

既然 PVE 宿主机跑的是 Linux,OpenWRT 跑的也是 Linux,我可不可以用什么容器化技术跑它呢?

然后我想起来 PVE 的 LXC 容器,参考了一下张大妈这篇恩山这篇文章 ,根据以下几步,十分轻易地搭建了起来。

  • 选一个喜欢的 x86 固件,官方版也可以
  • 下载这个固件的 squashfs 版本,官方的好像可以直接下 rootfs
  • unsquashfs 把固件提取出来,再次打包成 rootfs.tar.gz
  • 把打包好的或者官方的 rootfs 上传到 PVE
  • 用控制台新建 LXC 容器(参考恩山文章)即可

搭建是搭建好了,我也能访问 LuCI,下一个问题接踵而来:居然没有 NAT?!

事出反常必有妖,这个问题被我从多角度全方位进行了排查:

  • 重启防火墙、调整防火墙区域:无效
  • 检查 OpenWRT 自己可不可以上网:可以
  • 关闭 RouterOS 让 OpenWRT 单独做网关:无效
  • 手动添加 SNAT MASQUERADE:可以

问题出现了进展,我开始怀疑这个固件的防火墙有 BUG,不会自动加 NAT

  • 换其他第三方固件:一样没有 NAT
  • 用 VM 模式启动 OpenWRT:可以

到这里我本来已经打算妥协,抛弃 LXC 投奔 VM 了。但是我最后又尝试了一次,把“启用 FullCone-NAT” 给取消了,你猜怎么着,添加成功了。

许多第三方固件都有的功能,官方没有

瞬间我又有了折腾的动力,顺着这条线索,我去看了看这个功能所依赖的 LGA1150/fullconenat-fw3-patch

if (zone->fullcone && (access("/usr/lib/iptables/libipt_FULLCONENAT.so", 0) == 0)) {
    r = fw3_ipt_rule_new(handle);
    fw3_ipt_rule_src_dest(r, msrc, mdest);
    fw3_ipt_rule_target(r, "FULLCONENAT");
    fw3_ipt_rule_append(r, "zone_%s_postrouting", zone->name);
    r = fw3_ipt_rule_new(handle);
    fw3_ipt_rule_src_dest(r, msrc, mdest);
    fw3_ipt_rule_target(r, "FULLCONENAT");
    fw3_ipt_rule_append(r, "zone_%s_prerouting", zone->name);
} else {
    r = fw3_ipt_rule_new(handle);
    fw3_ipt_rule_src_dest(r, msrc, mdest);
    fw3_ipt_rule_target(r, "MASQUERADE");

再手动尝试加载这个内核模块

root@CTOpenWrt:~# modprobe xt_FULLCONENAT
no module folders for kernel version 5.4.73-1-pve found

一切问题都浮出水面了。

让我来讲讲这里面的道理:

  • 如果在 LuCI 里面选择启用 FullCone NAT
  • 原来的行为是添加一个-j MASQUERADE,则改为添加一个-j FULLCONENAT
  • 我选择了使用,他尝试添加一个-j FULLCONENAT
  • 但是因为内核模块加载失败,所以没有对应的 target,也就添加失败了
  • 这个 patch 没有检测添加失败的情况,不会 fallback 到普通的 MASQUERADE
  • 我失去了 NAT

那么解决问题的第一步是让他加载上内核模块,我尝试过寻找对应 PVE 内核版本的编译好的 kmod,但是很显然并不存在这种东西,那么就自己编译吧!

  • 找到这个 patch 实际依赖的内核模块是这个
Chion82/netfilter-full-cone-nat
A kernel module to turn MASQUERADE into full cone SNAT - Chion82/netfilter-full-cone-nat
  • 尝试编译,编译出错,缺少头文件
  • 翻箱倒柜找到了对应版本的 Header(反正 apt 源里没有,好像要钱)
  • 再次编译,编译成功,insmod 成功

然后,这就是 ROS + OpenWRT 做的完美 Full Cone NAT

ROS 上的配置

要达到这样的效果,只需要在 RouterOS 防火墙的 NAT 规则最后加上一条把所有 (没有匹配 ROS 自己做的 NAT 及其他规则的) UDP 都转发到 OpenWRT 上去就可以了。

顺带一提我发现这个 OpenWRT 也没办法直接 PPPoE 拨号,因为 PPP 也依赖内核模块实现,同样自己编译然后加载应该就能解决问题。不过这部分工作我是交给 ROS 做的,所以没有实战解决,欢迎大家自己来踩坑试试(不是)

以上就是我的所有踩坑内容了,目前为止运行了 35 天都非常稳定,彻底摆脱了一周不重启就有各种各样奇奇怪怪毛病的日子。目前来看 J1900 是足够应付 200M 宽带 + 透明网关代理的,如果不够升级一下硬件就行,这套架构是没什么问题的。

以上内容,因为行文仓促,难免有谬误,欢迎大家在评论留言指正