本文是系列第4篇。前一篇介绍了编译体系的全链路。本文聚焦于一个实际需求:当固件刷入设备后第一次启动时,系统如何自动识别硬件、初始化网络接口、设置MAC地址、配置默认参数——让设备开箱即用。

一、为什么需要定制化

SD-WAN终端在出厂部署时,需要满足以下”开箱即用”的要求:

  • 管理地址为预设值(如192.168.1.254),运维人员按该地址登录
  • 管理密码为预设值,无需额外配置
  • 时区设置为本地时区(如CST-8),日志时间戳才有意义
  • WiFi SSID和密码有默认值,方便初次连接调试
  • MAC地址从芯片的factory分区读取,确保每台设备唯一
  • 主机名称、NTP服务器等系统参数有合理的默认值

这些定制化需求需要在源码层面实现,而非每台设备手动配置。

二、网络配置的自动生成流程

在第2篇中我们提到,系统启动时S10boot脚本会调用/bin/config_generate来生成初始网络配置。这里深入展开这个流程。

2.1 完整调用链

/etc/init.d/boot 启动
    ↓
/bin/config_generate 执行
    ├── 读取 /etc/board.json(硬件板级信息)
    │   ├── 如果 board.json 不存在:
    │   │   调用 /bin/board_detect 生成
    │   │   └── board_detect 遍历 /etc/board.d/ 下的脚本
    │   └── 如果 board.json 已存在且 /etc/config/network 已存在:
    │       跳过(不覆盖已有配置)
    ↓
根据 board.json 中的信息生成:
    ├── /etc/config/network(网络接口配置)
    └── /etc/config/system(系统基础配置)
    ↓
uci_apply_defaults 执行
    └── 执行 /etc/uci-defaults/ 下的所有脚本

2.2 board_detect的探测机制

/bin/board_detect是硬件识别的核心。它会遍历/etc/board.d/目录下的所有脚本,按照编号顺序执行:

#!/bin/sh
CFG=$1
[ -n "$CFG" ] || CFG=/etc/board.json
[ -d "/etc/board.d/" -a ! -s "$CFG" ] && {
    for a in $(ls /etc/board.d/*); do
        [ -x $a ] || continue    # 只执行有可执行权限的脚本
        $(. $a)
    done
}

每个board.d脚本通过调用/lib/functions/uci-defaults.sh中定义的函数来写入板级信息:

  • 01_leds:识别并注册LED指示灯配置
  • 02_network:识别并注册网络接口布局(哪个口是LAN、哪个口是WAN)

这些脚本内部通过/lib/ramips.sh中的ramips_board_name()函数来获取板型名称,然后根据板型名称做switch-case判断。

2.3 源码中的文件对应关系

上述文件在源码工程中有明确的位置:

运行时路径 源码路径 作用
/bin/board_detect package/base-files/files/bin/board_detect 板型探测入口
/bin/config_generate package/base-files/files/bin/config_generate 配置生成程序
/lib/functions/uci-defaults.sh package/base-files/files/lib/functions/uci-defaults.sh UCI默认值函数库
/etc/board.d/02_network target/linux/ramips/base-files/etc/board.d/02_network 网络接口探测
/etc/board.d/01_leds target/linux/ramips/base-files/etc/board.d/01_leds LED配置探测
/lib/ramips.sh target/linux/ramips/base-files/lib/ramips.sh 平台识别函数

三、MAC地址的初始化

MAC地址是网络设备的”身份证”。每台路由器出厂时,厂商会将唯一的MAC地址写入factory分区。系统启动时需要从该分区读取MAC地址,并设置到对应的网络接口上。

3.1 factory分区与MAC地址

factory分区(通常是/dev/mtd2)保存了芯片出厂时写入的硬件参数。可以通过hexdump查看其内容:

hexdump -C /dev/mtd2

对于MT7628平台,MAC地址存储在特定的偏移位置:

  • LAN MAC地址:偏移0x28,6字节
  • WAN MAC地址:偏移0x2e,6字节

3.2 02_network脚本中的MAC初始化

02_network脚本中,通过ramips_setup_macs()函数完成MAC地址的初始化。不同平台的实现方式有所不同:

ramips_setup_macs() {
    local board="$1"
    local lan_mac=""
    local wan_mac=""

    case $board in
    mt7628)
        # MT7628:从factory分区的固定偏移读取
        lan_mac=$(hexdump -v -s 0x28 -n 6 -e '2/1 "%02x:"' /dev/mtd2)
        wan_mac=$(hexdump -v -s 0x2e -n 6 -e '2/1 "%02x:"' /dev/mtd2)
        [ -n "$lan_mac" ] && ucidef_set_interface_macaddr "lan" ${lan_mac%:}
        [ -n "$wan_mac" ] && ucidef_set_interface_macaddr "wan" ${wan_mac%:}
        ;;
    *)
        # 其他平台:从eth0的MAC地址推算
        lan_mac=$(cat /sys/class/net/eth0/address)
        wan_mac=$(macaddr_add "$lan_mac" 1)    # WAN MAC = LAN MAC + 1
        [ -n "$lan_mac" ] && ucidef_set_interface_macaddr "lan" $lan_mac
        [ -n "$wan_mac" ] && ucidef_set_interface_macaddr "wan" $wan_mac
        ;;
    esac
}

这里有两种策略:

  1. 直接读取(MT7628):从factory分区的已知偏移位置用hexdump读取原始字节,拼接为MAC地址格式(xx:xx:xx:xx:xx:xx)。${lan_mac%:}的作用是去掉末尾多余的冒号
  2. 推算生成(其他平台):读取eth0网卡的当前MAC地址作为LAN MAC,然后将最后一位加1作为WAN MAC。这种方式的前提是芯片已经在启动阶段从factory分区加载了eth0的MAC地址

3.3 MAC地址的唯一性保证

MAC地址的前三个字节称为OUI(Organizationally Unique Identifier),由IEEE分配给设备制造商,标识厂商身份。后三个字节由厂商自行分配。

对于SD-WAN终端,MAC地址在factory分区中的写入通常是在生产环节完成的(由生产工具通过JTAG或串口写入),确保每台设备的MAC地址唯一。如果需要在小批量场景下自行分配MAC地址,也可以通过IEEE的IAB(Individual Address Block)机制申请。

四、默认配置的修改

4.1 管理密码

管理密码存储在/etc/shadow文件中。要设置默认密码,需要先生成密码哈希值:

openssl passwd -1 -salt <随机盐值> <密码>

例如生成密码admin123的哈希:

openssl passwd -1 -salt rjJvOAYi admin123
# 输出:$1$rjJvOAYi$51BBvejOE6Gc/o95ybr5M0

然后将生成的哈希写入源码中的shadow文件模板。由于shadow文件包含敏感信息,通常不建议直接提交到版本控制,而是在编译过程中通过脚本生成。

4.2 管理地址

默认的OpenWrt管理地址是192.168.1.1。要修改为192.168.1.254(或其他值),需要修改config_generate脚本中设置LAN接口地址的代码。

修改位置:package/base-files/files/bin/config_generate

4.3 时区与NTP

默认时区是UTC,对于国内设备需要修改为CST-8。同时需要设置国内可用的NTP服务器:

# 修改时区
set system.@system[-1].timezone='CST-8'

# 配置NTP服务器
delete system.ntp
set system.ntp='timeserver'
set system.ntp.enabled='1'
set system.ntp.enable_server='0'
add_list system.ntp.server='cn.ntp.org.cn'
add_list system.ntp.server='0.pool.ntp.org'
add_list system.ntp.server='1.pool.ntp.org'

4.4 WiFi SSID和密码

WiFi配置的修改位置取决于使用的无线驱动:

开源驱动(mac80211): 修改位置:package/kernel/mac80211/files/lib/wifi/mac80211.sh

闭源驱动(MTK官方驱动): 修改位置:package/mtk/mt7628/files/mt7628.sh

4.5 WiFi初始化流程

WiFi的启动是网络初始化的一部分,调用链如下:

/etc/init.d/boot
    ↓
/sbin/wifi detect(或 config,取决于版本)
    ↓
调用 /lib/wifi/ 下的脚本
    ├── mac80211.sh(开源驱动)
    └── mt7628.sh(闭源驱动)
    ↓
生成 /etc/config/wireless 配置

五、添加新设备型号的支持

当需要在一款新的路由器上运行OpenWrt时,需要为该型号添加设备支持。以ramips架构的MT76x8系列为例,需要完成以下步骤:

5.1 添加DTS设备树文件

设备树(Device Tree Source)文件描述了硬件的具体参数——GPIO引脚映射、Flash大小、内存大小、LED和按键的位置等。

文件位置:target/linux/ramips/dts/<型号>.dts

该文件会include平台通用的dtsi文件(如mt7628an.dtsi),然后只定义本型号特有的硬件参数。

5.2 编写board.d脚本

在board.d目录下添加或修改脚本,为新设备注册LED和网络接口配置:

target/linux/ramips/base-files/etc/board.d/01_leds    # 添加LED配置
target/linux/ramips/base-files/etc/board.d/02_network  # 添加网络接口配置

5.3 在Image Makefile中注册设备

最后,需要在Image Makefile中注册新设备,编译系统才能为其生成固件:

文件位置:target/linux/ramips/image/mt76x8.mk

define Device/zbt-we2805ac
    DTS := ZBT-WE2805AC
    IMAGE_SIZE := $(ralink_default_fw_size_16M)
    DEVICE_TITLE := ZBT-WE2805AC
endef
TARGET_DEVICES += zbt-we2805ac
  • DTS:指定使用哪个DTS文件
  • IMAGE_SIZE:指定固件镜像的大小
  • DEVICE_TITLE:设备显示名称
  • TARGET_DEVICES:将设备添加到编译目标列表

5.4 内存大小的自适应

在DTS中如果不显式指定内存大小,内核会自动检测:

memory@0 {
    device_type = "memory";
    reg = <0x0 0x8000000>;    // 0x8000000 = 128MB
};

如果删除reg属性,内核会通过探测硬件来确定实际可用内存大小。

六、uc-defaults:一次性初始化脚本

/etc/uci-defaults/目录下的脚本只在第一次启动时执行,执行成功后自动删除。这种机制非常适合完成一些一次性的初始化工作。

典型用途:

  • 设置管理密码
  • 配置默认的网络参数
  • 注册远程管理服务
  • 安装并启用自启动服务

如果初始化逻辑比较复杂,可以编写独立的Shell脚本,通过uci-defaults脚本调用。

七、Shell脚本编程要点

在OpenWrt的定制化开发中,会大量编写Shell脚本。这里总结几个容易踩的坑:

7.1 if语句的字符串比较

# 错误:空字符串会导致语法错误
if [ $var == "test" ]; then

# 正确:变量加双引号
if [ "$var" == "test" ]; then

# 双中括号支持模糊匹配
if [[ "$source" == dest* ]]; then

7.2 短路求值

[ -f /tmp/lock ] && echo "locked"    # 文件存在则执行
[ -f /tmp/lock ] || echo "not found"  # 文件不存在则执行

7.3 字符串截取

# 从左边第0个字符开始,取5个字符
echo ${var:0:5}

7.4 重定向

command > file     # 覆盖写入
command >> file    # 追加写入
command 2>/dev/null  # 丢弃错误输出

下一篇将深入4G模块集成的具体实践——从内核驱动修改、补丁制作到自动拨号脚本,解决”让路由器插上SIM卡就能上网”的工程问题。