嵌入式路由器系统 DD-WRT

前言

关于 Merlin 的那篇文章已经说过了,我对上次 DD 的失败经历有点不甘心。其实也是有原因的,那时候已经是晚上准备睡觉了,可是问题还未解决。我又不想第二天早上赖床的时候没有 WIFI 用,于是放弃之,刷回去了。

这次,本着生命不休,折腾不止的精神,第二次尝试起了 DD-WRT。结果是令人满意的,全部成功,可谓一路顺风。所以我记录一下 DD 的配置过程,目标当然是当作一个自由的嵌入式 Linux 使用了!

对了,为什么非要用 DD?因为 DD 内核版本高,这是第一点。其次 DD 的功能、完善程度、社区支持等都高于 Merlin ,虽然 Merlin 也不错,但版本过低的内核一直是我心中的一个坎。

DD-WRT

DD-WRT 是基于 OpenWRT 的一个路由器 Linux 发型版。它只比 OpenWRT 晚诞生一年,应该是最早基于 OpenWRT 的开源路由器固件(关于这点我并没有调查过)。
注意:以下对 DD-WRT 简称 “DD” ,对 OpenWRT 简称 “OP” 。
它跟 OP 有一个重大的区别,在 DD 的文件系统上根目录并不可写,而 OP 的根目录以及大多数目录都是随意读写的(后面会有解释原因)。
当然,DD 并不是一个封闭的操作系统,根目录不可写不表示比 OP 扩展性差。相反,它其实是一种的进步。一方面是因为安全,不可写导致系统无法被轻易破坏。另一方面,因为路由器内置储存空间过小,不应该再徒增内容到 root 设备中。
以至于 DD 通常和 Optware 配合,搭建一个自由扩展的 Linux 环境。Optware 可以将扩展软件“安置”到 /opt 目录,而 /opt 目录是给外置存储设备挂载的,这样的话,可以在即使只读的系统目录下依然扩展系统功能,一举多得。
当然,现在的 Entware 完全可以取代 Optware 。所以此处的 Optware 我只是顺带提一提,下面的步骤使用的是 Entware 。

安装 DD-WRT

在这里找到你的型号,直接刷入 .chk 即可。过刷机过程不再赘述,请参考 Merlin 那篇文章。
待系统重启进入 DD 以后,第一件事是设置密码。如果不小心手滑直接默认密码点了确认,也没关系,默认用户认证是:root/admin 。
跟 Merlin 不同的是,Web 认证的帐号跟系统上不是同一个,也就是说,不管你 Web 管理界面的用户名是什么,SSH 用户名还是 root ,不过密码都一致的。

基础过程:设置好网络 -> 启用 SSH -> 启用 jffs -> 启用 USB 。下面一一说明一下:

  1. 设置网络过程略。

  2. 启用 SSH

    顶部 Tab: Service , Secure Shell 中的 SSHd 选择 enable 。然后就可以 SSH 登录系统了,用户名是 root 密码跟登录路由器 WEB UI 一致。

  3. 启用 jffs

    顶部 Tag: Administration , JFFS2 Support 中的 Internal Flash Storage 选择 enable 。

  4. 启用 USB

    首先给路由器插上 U 盘。重启路由器。然后 SSH 登录以后查看 U 盘设备的 UUID。例如我的设备如下:

    root@DD-WRT:~# blkid
    /dev/sda1: UUID="060cdbd1-c6b9-40ae-a8ab-6b911da86ec0" TYPE="ext4"
    

    WEB UI 上选择 Service - USB , 将 UUID 粘贴到 ‘Mount this Partition to /opt’ 项右边的输入框中。保存,重启。

上述过程做完以后,准备工作已经都做好了。

安装 Entware

在这之前,请确认一下 /opt 目录是否被 U 盘设备挂载,否则请重新确认上述准备工作是否都正确。如果确认无误,即可直接开始安装: 注意,DD 跟 Merlin 不一样,后者是内置 Entware 的安装脚本的,前者不内置。所以 DD 只能下载安装脚本执行:

wget -O - http://pkg.entware.net/binaries/armv7/installer/entware_install.sh | sh

过程不多说,跟 Merlin 是一样的。安装完成以后请 opkg update 更新一下索引。

其余的很多环境的坑请参考那篇文章,因为都是 Entware 环境,通用的。

开机启动服务

由于 Merlin 天生为 Entware 考虑了一下东西,所以在 Merlin 下包括开机服务脚本的启动命令都是内置的。然而 DD 却没有内置这些,也就是说即使你安装的软件在 /opt/etc/init.d 下生成了服务脚本,它仍然在开关机时不会得到执行。

我们可以基于 DD 提供的 ‘Commands’ 功能做到这一点:

  1. 创建相关目录(启用 jffs 的目的是用来存放这些固定的永久储存的东西):

    cd /jffs && mkdir scripts && cd scripts && mkdir shutdown && mkdir startup
    
  2. 在 scripts 目录创建 operator.sh 脚本(控制 shutdown 目录或者 startup 目录的脚本执行):

    #!/usr/bin/env sh
    SCRIPTS_HOME=/jffs/scripts
    run_all(){
        for SCRIPT in "$SCRIPTS_HOME"/"$1"/*;
            do
                    if [ -f "$SCRIPT" -a -x "$SCRIPT" ]
                    then
                            $SCRIPT
                    fi
            done
    }
    run_all "$1"
    
  3. 在 startup 目录分别创建 S10service 和 S30swapon:

    #!/bin/sh
    RC='/opt/etc/init.d/rc.unslung'
    i=30
    until [ -x "$RC" ] ; do
      i=$(($i-1))
      if [ "$i" -lt 1 ] ; then
        logger "Could not start Entware"
        exit
      fi
      sleep 1
    done
    $RC start
    
    #!/bin/sh
    # Turn On Usage Of Swapfile
    if [ -f "/opt/swapfile" ];then
        swapon /opt/swapfile
        echo "Turning Swapfile On"
    fi
    

    它们分别是启动 init.d 中的服务和启用 swap (交换空间)的脚本。

  4. 设置开/关机执行脚本

    路由器 WEB UI -> Tag: Administration - Commands , 分别添加以下两个命令,先后保存为 Startup 和 Shutdown :

    /jffs/scripts/operator.sh startup
    
    /jffs/scripts/operator.sh shutdown
    

    其实就是开机执行 operator.sh 脚本添加 startup 参数 和 关机执行 operator.sh 添加 shutdown 参数。而 operator.sh 的流程其实就是遍历参数目录(例如 startup 和 shutdown)中所有的脚本并且依次执行。
    如果你看明白了,就会知道只要将脚本放进 startup 目录它就会开机执行,只要放进 shutdown 它就会关机时执行。

有关脚本执行顺序需要特意说明一下,这是我的目录结构:

root@DD-WRT:/jffs/scripts# tree -L 3
.
|-- operator.sh
|-- shutdown
|   |-- S10service
|   `-- S30swapoff
`-- startup
    |-- S10service
    `-- S30swapon

可以注意到,开机执行的脚本都是以 S + 数字 + 名称来命名的。这样做的目的是方便遍历时排序,数字低的会被先遍历并执行,也就是根据命名做到了启动优先级。
至于前缀的大写字母 S 可以用其他字母替代,但是要统一。是否后缀 .sh 也无所谓。其实这里我就是模仿的 init.d 里边的服务命名方式,因为它们就是用这种方式决定启动优先级的。

这样做了以后,就在路由器固件原生不内置相关功能的情况下做到了一样的效果。安装的所有服务可以正常开机启动,并且可以自己添加任意的脚本让它们自动执行。(例如从上面的文件中可以看出来,我还在关机时运行了两个脚本)

管理服务

假设你安装一个 nginx , 它会在 init.d 目录创建一个名为 S80nginx 的服务脚本(数字可能不一样)。配置上述的脚本以后它会开机启动。但是,如果手动操作这个服务脚本呢?只需要:

/opt/etc/init.d/S80nginx start|restart|stop

执行它,给它相应参数即可。

但是,这样做未免太麻烦了。因为不仅仅是 init.d 目录不在 PATH 中,而是每个服务由于优先级的原因命名很麻烦。如果想手动 启动/停止/重启 某个服务还需要记住它的数字,打上大写的 S 也很麻烦。总之,这种手动直接运行服务文件的方式很麻烦。
于是,我为了方便管理服务写了一个 lua 脚本,它基于这些服务文件,可以像其他发型版中的 service 一样根据正常服务名称管理服务:

root@DD-WRT:~# ser privoxy restart
 Shutting down privoxy...              done. 
 Starting privoxy...              done. 
root@DD-WRT:~# 

可以看到, ser 管理服务不需要绝对路径,不需要服务文件名,而是以服务应该有的名字(S + 数字之后的部分)。

我们在 /opt/bin 目录创建文件 ser :

#!/usr/bin/env lua

function os.capture(cmd, raw)
    local f = assert(io.popen(cmd, 'r'))
    local s = assert(f:read('*a'))
    f:close()
    if raw then return s end
    return string.sub(s, 0, string.len(s) - 1)
end

PATH = '/opt/etc/init.d/'

function handler(s_name, operation)
    local out = os.capture('ls ' .. PATH, false)
    local flag = false
    for s in string.gmatch(out, '%S+') do
        local prefix = string.match(s, '^S%d+')
        if prefix then
            local service_name = string.sub(s, string.len(prefix) + 1)
            if service_name == s_name then
                print(os.capture(PATH .. s .. ' ' .. operation))
                flag = true
            end
        end
    end
    if not flag then
        print('No service: ' .. s_name)
    end
end

handler(arg[1], arg[2])

添加执行权限:

chmod +x /opt/bin/ser

然后就可以使用它了。当然,由于它是一个 lua 脚本,你需要安装 lua :

opkg install lua

这样子管理服务就方便得多了。

配置 NFS

安装待写。解决权限问题在这篇文章有说明。

待写。

其他

我暂时就写到这里,如果之后有更多的新内容我会更新这篇文章。其余的一些环境问题、Swap 等设置我在 Merlin 那篇已经写过了,这里不再重复。

最后

DD 的完美使用当初的确让我兴奋了一阵,但是后续的使用中发现其实并没有什么区别。跟我那篇文章写的一样,在嵌入式 Linux 更何况是路由器上高版本的内核显式优点很少有能发挥作用的时候。
不过,DD 值得一用!它已经让我的路由器变得强大无比,我有很多服务器上的程序已经迁移到路由器上运行。例如那个知乎机器人。