前言
玩路由器的话,肯定少不了折腾软路由。除了基本的路由器功能以外,相对普通路由器来说,较强大的硬件完全可以将其作为一台小型的服务器使用。而在软路由系统的选择上,也一定会有OpenWrt x86的身影。
安装
官方提供了两种x86架构的OpenWrt镜像,此处使用的是ext4-combined.img.gz
,需要注意的是,这是一个磁盘映像,安装时候会清空硬盘上的分区表,所有数据都会丢失,因此一定要做好备份。
安装方式也非常简单,准备一个Linux的启动盘,将解压出来的img镜像复制到U盘中,然后从启动盘启动。我是用的是Arch Linux,启动后会直接进入到命令行界面,使用dd命令将img镜像释放到硬盘即可:
1 | dd if=/path/to/openwrt-xxx.img of=/dev/sdX |
其中sdX为系统盘,注意是指定整个磁盘而不是磁盘上的分区(sdX1)。
具体的安装步骤可以参考官方文档:https://openwrt.org/docs/guide-user/installation/openwrt_x86
升级
官方推荐的升级方式
引用官方原话:
If you had used a ext4-combined.img.gz type of image to install, there are 4 options for upgrading:
- Write a new ext4-combined.img.gz image: this is the simplest option and is identical to first installation: all data, configs, packages and extra partitions will be wiped and you’ll have a brand new OpenWrt system with default packages and configs. Then you can reinstall all packages and copy config files back and create extra partitions.
- Use sysupgrade: this is default upgrading procedure, but the least recommended option for x86 machines. Proceed to Sysupgrade for details.
- Extracting boot partition image from ext4-combined.img.gz and writing it and ext4-rootfs.img.gz, leaving MBR partition table intact.
- Extracting boot partition image from ext4-combined.img.gz and writing it, then uncompressing rootfs.tar.gz to existing rootfs partition.
其中3和4是推荐的方式,两种方式也大同小异,都是分别刷入boot分区和rootfs,这样不会清空分区表。但是无论是3还是4,都需要像安装时一样,外接显示器和键盘,插入启动盘来进行升级。对于经常放在角落吃灰的路由器来说,这样未免也太麻烦了点。那么,有没有更好的升级方案呢?在全网搜索无果后,我突然有了一个想法。
更优雅的升级方式
用过Android系统的应该知道,在Android 7以前,升级系统需要等待很长时间。但是从Android 7开始,引入了A/B分区无缝更新。系统会在后台下载系统并安装到另一个分区,重启时候直接从安装新系统的分区引导即可。
分区准备
于是从此得到灵感,尝试使用A/B分区无缝升级的方式,来升级OpenWrt x86。我使用的是UEFI的镜像加GPT分区,如果是BIOS + MBR,分区表可能不一致,具体已实际为准。
系统安装完成后,默认存在三个分区:
1 | ➜ ~ lsblk |
其中sda128起始扇区为34至511,sda1为EFI分区,扇区为512至262655,sda2为根分区,扇区从263168开始,sda1和sda2的大小取决于编译时候设置的参数。
如果按照Android A/B分区无缝更新的思路,我们还应该创建一个boot分区和一个根分区,但是为了简单起见,可以只需要创建一个根分区,boot分区可以公用一个,因为系统在运行时不会占用boot分区文件资源,因此可以在运行时进行更新。
在创建另一个根分区之后,使用lsblk
查看应该输出如下:
1 | ➜ ~ lsblk |
其中sda3为新创建的分区,还没有被挂载,大小和原有的根分区sda2一致。剩下的空间可以划分成data分区,用于保存Docker等应用的数据。所以最终我的分区如下:
1 | ➜ ~ lsblk |
系统更新
分区准备就绪后,当下次需要更新系统时候,可以按照官方文档,准备好boot和rootfs的img镜像,分别使用dd
命令写入硬盘即可。
假设当前系统分区为sda2,新系统要安装到sda3,那么则:
1 | dd if=/path/to/boot.img of=/dev/sda1 |
写入映像后,需要修改grub.cfg
,使得下次启动sda3。
首先重新挂载boot分区,否则修改的文件无效:
1 | 挂载boot分区 |
然后使用blkid
命令查看sda3分区的PARTUUID,切记不要和UUID混淆:
1 | ➜ ~ blkid |
此处的6bd26996-e16e-9843-a1d0-a11cf9daa480
就是需要的PARTUUID,复制备用。
接下来修改/boot/grub/grub.cfg
,将两处menuentry
中的root=PARTUUID=
设置为上一步的PARTUUID:
1 | ➜ ~ cat /boot/grub/grub.cfg |
确认无误后,即可重新启动,不出意外的话,重启后sda3将被挂载为根分区,同时boot分区也已经更新。
令人疑惑的是: 这个239K的sda128分区,是做什么用途的?我没有找到详细的文档去解释这个分区,但是在官方的upgrade文档中,使用3和4方式进行升级的时候,提到有一种情况需要重新更新整个分区表:
The only exception is when new OpenWrt image brings a newer version of GRUB2. Part of GRUB2 is stored close to MBR and outside of partitions area, so we need to write a full ext4-combined.img.gz to update it.
由于我是用的是UEFI + GPT,我猜想这239K可能也是存放了GRUB的信息,那么为什么不将他一起更新呢?
所以我最后采取的方案是:
- 获取最新系统镜像,将其使用
dd
或者rufus等软件写入U盘。 - 将U盘插入路由器,假设其为/dev/sdb,那么使用
lsblk
查看,sdb将会有三个分区:
1 | ➜ ~ lsblk |
其中sdb1对应sda1,sdb2对应sda2和sda3,大小为239KB的sdb3恰好对应sda128。在更新的时候,将这三个分区分别对应更新即可:
1 | dd if=/dev/sdb1 of=/dev/sda1 |
最后别忘了重新挂载boot分区并修改/boot/grub/grub.cfg
文件。
使用此种方式更新系统,无需外接键盘和显示器,无需制作其他系统启动盘,全程SSH到路由器进行操作即可。
拾遗
一般折腾软路由的话,肯定会安装一堆工具和软件,但是OpenWrt更新系统的时候会删除所有的用户安装的软件。一般情况下可以通过升级前备份配置文件,升级后重新安装软件,最后恢复配置来实现,但这样未免太过麻烦。既然升级系统可以用这么优雅的方式,那安装的软件和配置有办法保留吗?答案是当然,只不过第一次稍显麻烦,后续则可以方便很多。
解决方法就是自己编译系统镜像,这样可以进行高度定制,比如分区大小、默认软件包、默认配置文件等,但是编译系统需要一定的能力,并且要有硬件配置尚可的编译环境。
幸运的是微软收购GitHub后,推出了GitHub Actions,可以借此使用GitHub提供的编译环境,来编译OpenWrt镜像。目前已经有较多提供该方案的开源项目,如P3TERX/Actions-OpenWrt。你只需要将自己需要的软件写入.config
或diffconfig
,将备份出来的配置文件放入files
,这样每次编译出来的系统就包含了所需的软件和配置,直接刷入重启即可保持和原来一致。
需要注意的是,如果你的配置文件包含了敏感信息,比如宽带账号密码或者其他账号等,一定记得将仓库设为私有,以防隐私泄露。对于自行编译镜像的方式此处不再赘述,感兴趣的可以自行研究。