前言

玩路由器的话,肯定少不了折腾软路由。除了基本的路由器功能以外,相对普通路由器来说,较强大的硬件完全可以将其作为一台小型的服务器使用。而在软路由系统的选择上,也一定会有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:

  1. 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.
  2. Use sysupgrade: this is default upgrading procedure, but the least recommended option for x86 machines. Proceed to Sysupgrade for details.
  3. Extracting boot partition image from ext4-combined.img.gz and writing it and ext4-rootfs.img.gz, leaving MBR partition table intact.
  4. 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
2
3
4
5
6
➜  ~ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 119.2G 0 disk
├─sda1 8:1 0 128M 0 part /boot
├─sda2 8:2 0 512M 0 part /
└─sda128 259:0 0 239K 0 part

其中sda128起始扇区为34至511,sda1为EFI分区,扇区为512至262655,sda2为根分区,扇区从263168开始,sda1和sda2的大小取决于编译时候设置的参数。

如果按照Android A/B分区无缝更新的思路,我们还应该创建一个boot分区和一个根分区,但是为了简单起见,可以只需要创建一个根分区,boot分区可以公用一个,因为系统在运行时不会占用boot分区文件资源,因此可以在运行时进行更新。

在创建另一个根分区之后,使用lsblk查看应该输出如下:

1
2
3
4
5
6
7
➜  ~ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 119.2G 0 disk
├─sda1 8:1 0 128M 0 part /boot
├─sda2 8:2 0 512M 0 part /
├─sda3 8:3 0 512M 0 part
└─sda128 259:0 0 239K 0 part

其中sda3为新创建的分区,还没有被挂载,大小和原有的根分区sda2一致。剩下的空间可以划分成data分区,用于保存Docker等应用的数据。所以最终我的分区如下:

1
2
3
4
5
6
7
8
➜  ~ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 119.2G 0 disk
├─sda1 8:1 0 128M 0 part /boot
├─sda2 8:2 0 512M 0 part /
├─sda3 8:3 0 512M 0 part
├─sda4 8:4 0 118.1G 0 part /mnt/data
└─sda128 259:0 0 239K 0 part

系统更新

分区准备就绪后,当下次需要更新系统时候,可以按照官方文档,准备好boot和rootfs的img镜像,分别使用dd命令写入硬盘即可。

假设当前系统分区为sda2,新系统要安装到sda3,那么则:

1
2
dd if=/path/to/boot.img of=/dev/sda1
dd if=/path/to/rootfs.img of=/dev/sda3

写入映像后,需要修改grub.cfg,使得下次启动sda3。
首先重新挂载boot分区,否则修改的文件无效:

1
2
# 挂载boot分区
mount -o remount,rw /boot

然后使用blkid命令查看sda3分区的PARTUUID,切记不要和UUID混淆:

1
2
3
4
➜  ~ blkid
...
/dev/sda3: LABEL="rootfs" UUID="ff313567-e9f1-5a5d-9895-3ba130b4a864" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="6bd26996-e16e-9843-a1d0-a11cf9daa480"
...

此处的6bd26996-e16e-9843-a1d0-a11cf9daa480就是需要的PARTUUID,复制备用。

接下来修改/boot/grub/grub.cfg,将两处menuentry中的root=PARTUUID=设置为上一步的PARTUUID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  ~ cat /boot/grub/grub.cfg
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1 --rtscts=off
terminal_input console serial; terminal_output console serial

set default="0"
set timeout="3"
set root='(hd0,gpt1)'

menuentry "OpenWrt" {
linux /boot/vmlinuz root=PARTUUID=6bd26996-e16e-9843-a1d0-a11cf9daa480 rootwait console=tty0 console=ttyS0,115200n8 noinitrd
}
menuentry "OpenWrt (failsafe)" {
linux /boot/vmlinuz failsafe=true root=PARTUUID=6bd26996-e16e-9843-a1d0-a11cf9daa480 rootwait console=tty0 console=ttyS0,115200n8 noinitrd
}

确认无误后,即可重新启动,不出意外的话,重启后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的信息,那么为什么不将他一起更新呢?

所以我最后采取的方案是:

  1. 获取最新系统镜像,将其使用dd或者rufus等软件写入U盘。
  2. 将U盘插入路由器,假设其为/dev/sdb,那么使用lsblk查看,sdb将会有三个分区:
1
2
3
4
5
6
7
8
9
10
11
12
➜  ~ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 119.2G 0 disk
├─sda1 8:1 0 128M 0 part /boot
├─sda2 8:2 0 512M 0 part /
├─sda3 8:3 0 512M 0 part
├─sda4 8:4 0 118.1G 0 part /mnt/data
└─sda128 259:0 0 239K 0 part
sdb 8:0 0 119.2G 0 disk
├─sdb1 8:1 0 128M 0 part
├─sdb2 8:2 0 512M 0 part
└─sdb3 259:0 0 239K 0 part

其中sdb1对应sda1,sdb2对应sda2和sda3,大小为239KB的sdb3恰好对应sda128。在更新的时候,将这三个分区分别对应更新即可:

1
2
3
dd if=/dev/sdb1 of=/dev/sda1
dd if=/dev/sdb2 of=/dev/sda3
dd if=/dev/sdb3 of=/dev/sda128

最后别忘了重新挂载boot分区并修改/boot/grub/grub.cfg文件。

使用此种方式更新系统,无需外接键盘和显示器,无需制作其他系统启动盘,全程SSH到路由器进行操作即可。

拾遗

一般折腾软路由的话,肯定会安装一堆工具和软件,但是OpenWrt更新系统的时候会删除所有的用户安装的软件。一般情况下可以通过升级前备份配置文件,升级后重新安装软件,最后恢复配置来实现,但这样未免太过麻烦。既然升级系统可以用这么优雅的方式,那安装的软件和配置有办法保留吗?答案是当然,只不过第一次稍显麻烦,后续则可以方便很多。

解决方法就是自己编译系统镜像,这样可以进行高度定制,比如分区大小、默认软件包、默认配置文件等,但是编译系统需要一定的能力,并且要有硬件配置尚可的编译环境。

幸运的是微软收购GitHub后,推出了GitHub Actions,可以借此使用GitHub提供的编译环境,来编译OpenWrt镜像。目前已经有较多提供该方案的开源项目,如P3TERX/Actions-OpenWrt。你只需要将自己需要的软件写入.configdiffconfig,将备份出来的配置文件放入files,这样每次编译出来的系统就包含了所需的软件和配置,直接刷入重启即可保持和原来一致。

需要注意的是,如果你的配置文件包含了敏感信息,比如宽带账号密码或者其他账号等,一定记得将仓库设为私有,以防隐私泄露。对于自行编译镜像的方式此处不再赘述,感兴趣的可以自行研究。