Netifrc

简述 #

Netifrc 是 Gentoo 在运行 OpenRC 的系统上,用于配置和管理网络接口的默认框架。

源码 #

由 kimi-k3 注释,仅供参考。

文件/etc/init.d/net.lo
   1#!/sbin/openrc-run
   2# 声明这是 OpenRC 服务脚本,由 openrc-run 解释执行(而不是普通的 sh/bash)
   3# shellcheck shell=sh disable=SC1008
   4# Copyright (c) 2007-2009 Roy Marples <roy@marples.name>
   5# Copyright (c) 2010-2016 Gentoo Foundation
   6# Released under the 2-clause BSD license.
   7
   8# ---- 全局常量定义 ----
   9SHDIR="/lib/netifrc/sh"              # 共享 shell 函数库目录(functions.sh 等)
  10MODULESDIR="/lib/netifrc/net"        # 网络模块目录(dhcpcd.sh、iproute2.sh、wireless.sh 等)
  11_config_vars="config metric routes"  # 允许按"接口类型/接口名"覆盖的配置变量列表
  12
  13# IN_BACKGROUND 表示本服务是否由后台事件(如 devd/udev 热插拔)触发启动。
  14# 后台模式下 stop 时不会真正关闭接口(例如 PPP 按需拨号场景)
  15# -z : 判断字符串长度为 0
  16[ -z "${IN_BACKGROUND}" ] && IN_BACKGROUND="NO"
  17
  18# shellcheck disable=SC2034
  19# OpenRC 服务描述(rc-service ... describe 显示)
  20description="Configures network interfaces."
  21
  22# 定义一个包含"换行符"的变量,用于之后把 IFS 设为换行符,
  23# 从而实现"按行分割配置数组"(netifrc 用换行分隔多值配置项)
  24__IFS="
  25"
  26
  27# 如果此文件被直接调用(而非经由 OpenRC),默认 INIT 系统为 openrc,
  28# 这个变量会被 functions.sh 中的函数引用
  29# 当 INIT 未设置或为空,返回 openrc 并设置 INIT=openrc
  30: "${INIT:=openrc}"
  31
  32# ---- 加载共享函数库 ----
  33# functions.sh 提供 _exists、_up、_down、_add_address、_add_route、
  34# get_interface、shell_var、yesno、einfo/ewarn/eerror 包装等核心函数
  35if [ -f "$SHDIR/functions.sh" ]; then
  36    # shellcheck disable=SC1090
  37    . "$SHDIR/functions.sh"
  38else
  39    echo "$SHDIR/functions.sh missing. Exiting"
  40    exit 1
  41fi
  42
  43# 为每个接口生成独立的"模块排序缓存文件"路径。
  44# get_interface 从 RC_SVCNAME(如 net.eth0)中提取接口名(eth0)。
  45# 按接口分开缓存可避免多接口并发启动时的竞态条件,
  46# 也允许每个接口使用不同的自定义模块集合。
  47# RC_* 的变量由 OpenRC 设置,net.lo 只是直接继承使用
  48MODULESLIST="${RC_SVCDIR}/nettree$(get_interface)"
  49
  50# ====================================================================
  51# depend() —— OpenRC 依赖声明函数
  52# 由 rc 依赖解析器在生成依赖树时调用(不是运行时调用)
  53# ====================================================================
  54depend()
  55{
  56    local IFACE IFVAR
  57    IFACE=$(get_interface)               # 接口名,如 eth0
  58    IFVAR=$(shell_var "${IFACE}")        # 转成合法 shell 变量名,如 eth0( wlan0-1 -> wlan0_1 )
  59
  60    # Linux 下非 lo 接口:必须先挂载 sysfs(读取 /sys/class/net),
  61    # 且在内核模块加载(modules 服务)之后启动
  62    if [ "$RC_UNAME" = Linux ] && [ "$IFACE" != lo ]; then
  63        need sysfs
  64        after modules
  65    fi
  66    after bootmisc                       # 在 bootmisc 之后(保证 /run 等就绪)
  67    keyword -jail -prefix -vserver       # 声明在 jail/prefix/vserver 环境中不自动加入运行级别
  68
  69    case "${IFACE}" in
  70        lo|lo0) ;;                       # 回环接口:无额外依赖(最底层)
  71        *)                               # net.lo0 是 BSD 系统上的回环(loopback)接口服务,兼容 BSD 系统
  72            after net.lo net.lo0 dbus    # 其他接口在 lo 和 dbus 之后
  73            need localmount              # 需要本地文件系统已挂载(模块文件在磁盘上)
  74            provide net                  # 提供虚拟服务 "net",让依赖 net 的服务(如 sshd)可以启动
  75            ;;
  76    esac
  77
  78    # 允许用户为特定接口定义 depend_<接口> 函数来追加依赖
  79    if [ "$(command -v "depend_${IFVAR}")" = "depend_${IFVAR}" ]; then
  80        "depend_${IFVAR}"
  81    fi
  82
  83    # 兼容旧式变量 rc_need_eth0 / rc_use_eth0 等,转换为新式 rc_net_eth0_need 形式
  84    local dep prov
  85    for dep in need use before after provide keyword; do
  86        eval prov=\$rc_${dep}_${IFVAR}
  87        if [ -n "${prov}" ]; then
  88            "${dep}" "${prov}"
  89            ewarn "rc_${dep}_${IFVAR} is deprecated."
  90            ewarn "Please use rc_net_${IFVAR}_${dep} instead."
  91        fi
  92    done
  93}
  94
  95# ====================================================================
  96# 数组处理辅助函数
  97# netifrc 新式配置用"换行分隔的字符串"模拟数组;
  98# 老配置可能是 bash 数组或空格分隔字符串,这里做兼容。
  99# ====================================================================
 100
 101# _array_helper <变量名>
 102# 读取变量值,去掉每行首尾空白、删除空行、把行内连续空白压缩成单个空格,
 103# 然后按行输出(供调用方以 IFS=换行 方式读回)
 104_array_helper()
 105{
 106    local _a=
 107
 108    eval _a=\$$1
 109    _a=$(echo "${_a}" | sed -e 's:^[[:space:]]*::' -e 's:[[:space:]]*$::' -e '/^$/d' -e 's:[[:space:]]\{1,\}: :g')
 110
 111    [ -n "${_a}" ] && printf "%s\n" "${_a}"
 112}
 113
 114# _get_array <变量名>
 115# 以"每行一个元素"的形式输出配置变量的内容。
 116# 若运行在 bash 且该变量是 bash 数组(旧式写法),则给出弃用警告并逐元素输出。
 117_get_array()
 118{
 119    local _a=''
 120    if [ -n "${BASH}" ]; then
 121        # shellcheck disable=SC2039
 122        case "$(declare -p "$1" 2>/dev/null)" in
 123            "declare -a "*)
 124                ewarn "You are using a bash array for $1."
 125                ewarn "This feature will be removed in the future."
 126                ewarn "Please see net.example for the correct format for $1."
 127                eval "set -- \"\${$1[@]}\""     # 把数组元素展开为位置参数
 128                for _a; do
 129                    printf "%s\n" "${_a}"     # 每元素一行输出
 130                done
 131                return 0
 132                ;;
 133        esac
 134    fi
 135
 136    _array_helper "$1"                          # 非 bash 数组:走普通字符串路径
 137}
 138
 139# _flatten_array <变量名>
 140# 把(可能是 bash 数组的)变量压平为单行 shell 引号包裹的字符串,
 141# 用于把配置原样传递/嵌入到 eval 语句中(注意对单引号的转义处理)
 142_flatten_array()
 143{
 144    if [ -n "${BASH}" ]; then
 145        # shellcheck disable=SC2039
 146        case "$(declare -p "$1" 2>/dev/null)" in
 147            "declare -a "*)
 148                ewarn "You are using a bash array for $1."
 149                ewarn "This feature will be removed in the future."
 150                ewarn "Please see net.example for the correct format for $1."
 151                eval "set -- \"\${$1[@]}\""
 152                for x; do
 153                    # shellcheck disable=SC2059
 154                    # 先 printf 解释转义,再把 ' 替换为 '\'' 以安全嵌入单引号串
 155                    printf "'%s' " "$(printf "$x" | sed "s:':'\\\'':g")"
 156                done
 157                return 0
 158                ;;
 159        esac
 160    fi
 161
 162    _array_helper "$1"
 163}
 164
 165# ====================================================================
 166# _wait_for_presence —— 等待接口出现在系统中
 167# 用于 USB 网卡等可能需要时间才被内核创建的设备
 168# 超时由 presence_timeout_<IFVAR> 或全局 presence_timeout 控制(秒)
 169# ====================================================================
 170_wait_for_presence()
 171{
 172    local timeout=
 173    local rc=
 174
 175    _exists && return 0                       # 已存在则直接返回
 176
 177    eval timeout=\$presence_timeout_${IFVAR}
 178    timeout=${timeout:-${presence_timeout:-0}}
 179
 180    [ ${timeout} -le 0 ] && return 1          # 未配置等待 -> 直接失败
 181
 182    einfon "Waiting for ${IFACE} to show up (${timeout} seconds)"
 183    while [ ${timeout} -gt 0 ]; do
 184        _exists
 185        rc=$?
 186        [ $rc -eq 0 ] && break
 187
 188        sleep 1
 189        printf "."
 190        : $(( timeout -= 1 ))
 191    done
 192
 193    if [ ${timeout} -le 0 ]; then
 194        _exists                               # 最后再检查一次
 195        rc=$?
 196    fi
 197    echo
 198    eend $rc                                  # 输出 [ ok ] / [ !! ] 并返回状态码
 199}
 200
 201# ====================================================================
 202# _wait_for_carrier —— 等待链路载波(网线插入/无线关联成功)
 203# 超时由 carrier_timeout_<IFVAR> 或全局 carrier_timeout 控制
 204# 注意:超时<=0 时返回 0(成功),即"用户不想等就不等",
 205# 与 _wait_for_presence 的语义相反
 206# ====================================================================
 207_wait_for_carrier()
 208{
 209    local timeout=
 210
 211    _has_carrier  && return 0
 212
 213    eval timeout=\$carrier_timeout_${IFVAR}
 214    timeout=${timeout:-${carrier_timeout:-0}}
 215
 216    # 如果用户不需要这个特性
 217    [ ${timeout} -le 0 ] && return 0
 218
 219    einfon "Waiting for carrier (${timeout} seconds) "
 220    while [ ${timeout} -gt 0 ]; do
 221        if _has_carrier; then
 222            echo
 223            eend 0
 224            return 0
 225        fi
 226        sleep 1
 227        : $(( timeout -= 1 ))
 228        printf "."
 229    done
 230
 231    echo
 232    eend 1
 233    return 1
 234}
 235
 236# ====================================================================
 237# _netmask2cidr —— 把子网掩码转换为 CIDR 前缀长度
 238# 支持点分十进制(255.255.255.0)和十六进制(0xffffff00)两种形式
 239# 算法:对掩码的每个字节统计二进制中 1 的个数并累加
 240# ====================================================================
 241_netmask2cidr()
 242{
 243    # 某些 shell(FreeBSD sh、dash、busybox)不能正确处理十六进制算术,
 244    # 这里把 0xffffff00 拆成 0xff.0xff.0xff.0x00 的形式绕过该 bug。
 245    # bash 和 NetBSD sh 不需要此处理。
 246    case $1 in
 247        0x*)
 248        local hex=${1#0x*} quad=
 249        while [ -n "${hex}" ]; do
 250            local lastbut2=${hex#??*}                    # 去掉前两个十六进制字符
 251            quad=${quad}${quad:+.}0x${hex%${lastbut2}*}  # 取出前两位拼成 0xNN 段
 252            hex=${lastbut2}
 253        done
 254        # shellcheck disable=SC2086
 255        set -- ${quad}                  # 重新设置为点分形式继续处理
 256        ;;
 257    esac
 258
 259    local i='' len=''
 260    local IFS=.                         # 按点分段
 261    for i in $1; do
 262        case $i in
 263            0x*)    i=$((i)) ;;         # 十六进制段先转十进制
 264        esac
 265        while [ ${i} -ne 0 ]; do        # 统计该字节中 1 的位数
 266            : $(( len += i % 2 ))
 267            : $(( i >>= 1 ))
 268        done
 269    done
 270
 271    echo "${len}"                       # 最后 len 的值是掩码二进制中 1 的个数
 272}
 273
 274# ====================================================================
 275# _configure_variables —— 实现配置变量的"回退查找"
 276# 对 _config_vars 里的每个变量(config/metric/routes),
 277# 依次按位置参数 $1 $2 ...(通常是 接口类型、接口名 之类的层级)
 278# 查找 config_$1、config_$2 ...,找到第一个非空的就赋给 config_<IFVAR>
 279# 用法示例:_configure_variables $iface_type  → 允许按接口类型统一配置
 280# ====================================================================
 281_configure_variables()
 282{
 283    local var='' v='' t=''
 284
 285    for var in ${_config_vars}; do
 286        local v=
 287        for t; do
 288            eval v="\"\$${var}_${t}\""
 289            if [ -n "${v}" ]; then
 290                eval "${var}_${IFVAR}=\"\$${var}_${t}\""
 291                continue 2              # 跳到下一个 var
 292            fi
 293        done
 294    done
 295}
 296
 297# ====================================================================
 298# _which —— 在 PATH 中查找可执行文件,输出完整路径
 299# (不依赖外部 which 命令,纯 shell 实现)
 300# ====================================================================
 301_which()
 302{
 303    local i OIFS
 304    # Empty
 305    [ -z "$1" ] && return
 306    # check paths
 307    OIFS="$IFS"
 308    IFS=:
 309    for i in $PATH ; do
 310        [ -x "$i/$1" ] && echo "$i/$1" && break
 311    done
 312    IFS=$OIFS
 313}
 314
 315# ====================================================================
 316# _program_available —— 检查程序是否可用(支持多个候选、shell 内建命令)
 317# 参数为空返回 0;找到第一个可用的候选就输出它并返回 0
 318# 绝对路径直接测 -x;裸命令名用 type(能识别 builtin/alias/函数)
 319# ====================================================================
 320_program_available()
 321{
 322    [ -z "$1" ] && return 0
 323    local x=
 324    # for x; do 是 for x in "$@"; do 的省略写法
 325    for x; do
 326        case "${x}" in
 327            # 以 / 开头 → 绝对路径
 328            # 直接测"文件存在且可执行",成功就 break 跳出循环
 329            /*) [ -x "${x}" ] && break;;
 330            # 用 shell 内建的 type 探测(屏蔽它的输出和报错),成功就 break
 331            *) type "${x}" >/dev/null 2>&1 && break;;
 332        esac
 333        x=
 334    done
 335    [ -n "${x}" ] && echo $x && return 0
 336    return 1
 337}
 338
 339# 显示接口当前获取到的 IPv4 / IPv6 地址(供 DHCP 等模块回调打印日志)
 340_show_address()
 341{
 342    einfo "received address $(_get_inet_address "${IFACE}")"
 343}
 344
 345# ====================================================================
 346# _get_errorhandler_behavior —— 查询"错误处理策略"配置
 347# 当添加地址/路由遇到 EEXIST(已存在)等错误时,用户可配置忽略/告警/失败。
 348# 按从具体到一般的顺序查找变量,找到第一个非空值即返回:
 349#   errh_<IFVAR>_<对象>_<错误>  →  errh_<IFVAR>_<对象>_DEFAULT
 350#   → errh_<IFVAR>_DEFAULT_<错误> → errh_<IFVAR>_DEFAULT_DEFAULT
 351#   → errh_DEFAULT_... → 最后回退到传入的 fallback
 352# 典型配置:
 353#   errh_eth0_address_EEXIST=warn   # 地址已存在只警告
 354#   errh_eth0_route_EEXIST=warn
 355# ====================================================================
 356_get_errorhandler_behavior() {
 357    IFVAR="$1"
 358    object="$2"
 359    error="$3"
 360    fallback="$4"
 361    value=
 362    for key in \
 363        "errh_${IFVAR}_${object}_${error}" \
 364        "errh_${IFVAR}_${object}_DEFAULT" \
 365        "errh_${IFVAR}_DEFAULT_${error}" \
 366        "errh_${IFVAR}_DEFAULT_DEFAULT" \
 367        "errh_DEFAULT_${object}_${error}" \
 368        "errh_DEFAULT_${object}_DEFAULT" \
 369        "errh_DEFAULT_DEFAULT_${error}" \
 370        "errh_DEFAULT_DEFAULT_DEFAULT" \
 371        "errh" \
 372        "fallback" ; do
 373        eval value="\${${key}}"
 374        if [ -n "$value" ]; then
 375            echo "$value" && break
 376        fi
 377    done
 378}
 379
 380_show_address6()
 381{
 382    einfo "received address $(_get_inet6_address "${IFACE}")"
 383}
 384
 385# ====================================================================
 386# _gen_module_list —— 扫描所有模块并做拓扑排序,缓存结果
 387#
 388# 每个模块文件(/lib/netifrc/net/xxx.sh)里定义了 <mod>_depend() 函数,
 389# 其中调用 after/before/program/provide 声明自己的依赖关系:
 390#   after dhcp        —— 本模块要在提供 "dhcp" 的模块之后
 391#   before net.lo     —— (before 是反向的 after)
 392#   program dhcpcd    —— 本模块需要系统里存在 dhcpcd 程序
 393#   provide dhcp      —— 本模块提供 "dhcp" 这个虚拟能力
 394#
 395# 本函数:
 396#  1) 检查缓存(nettree 文件)是否比模块目录新,新则直接复用
 397#  2) 否则在子 shell 中加载所有模块、收集依赖、DFS 拓扑排序
 398#  3) 把排序结果以 shell 变量赋值形式原子地写入缓存文件
 399# ====================================================================
 400_gen_module_list()
 401{
 402    local x='' f='' force="$1"
 403    if ! ${force} ; then
 404        # 缓存文件比本进程还新?说明时间戳异常(如系统时钟回拨),弃用
 405        if [ -s "${MODULESLIST}" ] && [ "${MODULESLIST}" -nt /proc/$$/status ]; then
 406            ewarn "Discarding cached module list ($MODULESLIST) as it's newer current time!"
 407        # 缓存比模块目录新时,再逐个检查有没有比缓存更新的模块文件
 408        elif [ -s "${MODULESLIST}" ] && [ "${MODULESLIST}" -nt "${MODULESDIR}" ]; then
 409            local update=false
 410            for x in "${MODULESDIR}"/*.sh; do
 411                [ -e "${x}" ] || continue
 412                if [ "${x}" -nt "${MODULESLIST}" ]; then
 413                    update=true
 414                    break
 415                fi
 416            done
 417            ${update} || return 0             # 缓存有效,直接返回
 418        fi
 419    fi
 420
 421    einfo "Caching network module dependencies"
 422    # 在子 shell 中执行,保护主脚本的变量命名空间不被模块污染
 423    (
 424    # ---- 供模块 <mod>_depend() 调用的依赖声明原语 ----
 425
 426    # after X:把 X 追加到当前模块的 after 列表
 427    after() {
 428        eval ${MODULE}_after="\"\${${MODULE}_after}\${${MODULE}_after:+ }$*\""
 429    }
 430
 431    # before Y:等价于让 Y after 当前模块
 432    before() {
 433        local mod=${MODULE}
 434        local MODULE=
 435        for MODULE; do
 436            after "${mod}"
 437        done
 438    }
 439
 440    # program [start|stop] PROG...:声明模块依赖的外部程序
 441    # 可以分别声明启动/停止阶段需要的程序
 442    program() {
 443        if [ "$1" = "start" ] || [ "$1" = "stop" ]; then
 444            local s="$1"
 445            shift
 446            eval ${MODULE}_program_${s}="\"\${${MODULE}_program_${s}}\${${MODULE}_program_${s}:+ }$*\""
 447        else
 448            eval ${MODULE}_program="\"\${${MODULE}_program}\${${MODULE}_program:+ }$*\""
 449        fi
 450    }
 451
 452    # provide V:声明本模块提供虚拟能力 V;
 453    # 同时记录反向映射 V_providedby(谁提供了 V)
 454    provide() {
 455        eval ${MODULE}_provide="\"\${${MODULE}_provide}\${${MODULE}_provide:+ }$*\""
 456        local x
 457        for x in "$@"; do
 458            eval ${x}_providedby="\"\${${MODULE}_providedby}\${${MODULE}_providedby:+ }${MODULE}\""
 459        done
 460    }
 461
 462    # ---- 第一阶段:加载所有模块并执行其 <mod>_depend ----
 463    for MODULE in "${MODULESDIR}"/*.sh; do
 464        sh -n "${MODULE}" || continue         # 先做语法检查,失败跳过
 465        # shellcheck disable=SC1090
 466        . "${MODULE}" || continue             # source 模块以定义其 _depend 函数
 467        MODULE=${MODULE#${MODULESDIR}/}       # 去掉目录前缀
 468        MODULE=${MODULE%.sh}                  # 去掉 .sh 后缀 → 模块名
 469        eval "${MODULE}_depend"               # 执行依赖声明
 470        MODULES="${MODULES} ${MODULE}"
 471    done
 472
 473    # ---- 第二阶段:DFS 拓扑排序 ----
 474    VISITED=
 475    SORTED=
 476    visit() {
 477        case " ${VISITED} " in
 478            *" $1 "*) return;;                # 已访问过,防环
 479        esac
 480        VISITED="${VISITED} $1"
 481
 482        # 先递归访问本模块 after 的所有目标
 483        # (如果 after 的是虚拟能力名,则展开为提供它的所有模块)
 484        eval AFTER=\$${1}_after
 485        for MODULE1 in ${AFTER}; do
 486            eval PROVIDEDBY=\$${MODULE1}_providedby
 487            if [ -n "${PROVIDEDBY}" ]; then
 488                for MODULE2 in ${PROVIDEDBY}; do
 489                    visit "${MODULE2}"
 490                done
 491            else
 492                visit "${MODULE1}"
 493            fi
 494        done
 495
 496        # 再递归访问本模块 provide 的虚拟能力(保证提供者先于使用"能力名"者被处理?——
 497        # 实际效果是建立 provide 关系的遍历完整性)
 498        eval PROVIDE=\$${1}_provide
 499        for MODULE in ${PROVIDE}; do
 500            visit "${MODULE}"
 501        done
 502
 503        # 只有"真实模块"(不提供任何虚拟能力的叶子,或未被登记为提供者)
 504        # 才被加入排序结果;提供虚拟能力的模块会通过 providedby 间接进入
 505        eval PROVIDEDBY=\$${1}_providedby
 506        [ -z "${PROVIDEDBY}" ] && SORTED="${SORTED} $1"
 507    }
 508
 509    for MODULE in ${MODULES}; do
 510        visit "${MODULE}"
 511    done
 512
 513    # ---- 第三阶段:原子写入缓存文件 ----
 514    # 缓存格式为 shell 变量赋值,之后直接 source 即可
 515    TMPMODULESLIST=${MODULESLIST}.$$
 516    printf "" > "${TMPMODULESLIST}"
 517    i=0
 518    for MODULE in ${SORTED}; do
 519        eval PROGRAM=\$${MODULE}_program
 520        eval PROGRAM_START=\$${MODULE}_program_start
 521        eval PROGRAM_STOP=\$${MODULE}_program_stop
 522        eval PROVIDE=\$${MODULE}_provide
 523        echo "module_${i}='${MODULE}'"
 524        echo "module_${i}_program='${PROGRAM}'"
 525        echo "module_${i}_program_start='${PROGRAM_START}'"
 526        echo "module_${i}_program_stop='${PROGRAM_STOP}'"
 527        echo "module_${i}_provide='${PROVIDE}'"
 528        : $(( i += 1 ))
 529    done >> "${TMPMODULESLIST}"
 530    echo "module_${i}=" >> "${TMPMODULESLIST}"   # 空值作为列表终止标记
 531    mv -f "${TMPMODULESLIST}" "${MODULESLIST}"   # mv 保证原子替换
 532    )
 533
 534    return 0
 535}
 536
 537# ====================================================================
 538# _load_modules —— 按排序结果实际加载模块,处理虚拟能力包装
 539# 参数 $1:true=启动阶段加载,false=停止阶段加载
 540# ====================================================================
 541_load_modules()
 542{
 543    local starting=$1 mymods=
 544
 545    # Ensure our list is up to date
 546    _gen_module_list false                    # 生成/复用模块排序缓存
 547    # shellcheck disable=SC1090
 548    if ! . "${MODULESLIST}"; then             # source 缓存失败则强制重建
 549        _gen_module_list true
 550        # shellcheck disable=SC1090
 551        . "${MODULESLIST}"
 552    fi
 553
 554    MODULES=
 555    # 非回环接口:读取用户配置 modules_<IFVAR>(如 modules_eth0="dhcpcd")
 556    # 未配置则用全局 modules 变量,作为"用户偏好模块"列表
 557    if [ "${IFACE}" != "lo" ] && [ "${IFACE}" != "lo0" ]; then
 558        eval mymods=\$modules_${IFVAR}
 559        # shellcheck disable=SC2154
 560        [ -z "${mymods}" ] && mymods=${modules}
 561    fi
 562
 563    local i=-1 x='' mod='' f='' provides=''
 564    while true; do
 565        : $(( i += 1 ))
 566        eval mod=\$module_${i}
 567        [ -z "${mod}" ] && break              # 空值 = 列表结束
 568        [ -e "${MODULESDIR}/${mod}.sh" ] || continue
 569
 570        # 检查模块声明的通用程序依赖是否满足,不满足则跳过该模块
 571        eval set -- \$module_${i}_program
 572        if [ -n "$1" ]; then
 573            if ! _program_available "$@" >/dev/null; then
 574                vewarn "Skipping module $mod due to missing program: $*"
 575                continue
 576            fi
 577        fi
 578        # 再检查阶段特定(start/stop)的程序依赖
 579        if ${starting}; then
 580            eval set -- \$module_${i}_program_start
 581        else
 582            eval set -- \$module_${i}_program_stop
 583        fi
 584        if [ -n "$1" ]; then
 585            if ! _program_available "$@" >/dev/null; then
 586                vewarn "Skipping module $mod due to missing program: $*"
 587                continue
 588            fi
 589        fi
 590
 591        eval provides=\$module_${i}_provide
 592        if ${starting}; then
 593            # 用户可用 "!模块名" 或 "!虚拟能力" 显式禁用某模块
 594            # 例如 modules_eth0="!wpa_supplicant"
 595            case " ${mymods} " in
 596                *" !${mod} "*) continue;;
 597                *" !${provides} "*) [ -n "${provides}" ] && continue;;
 598            esac
 599        fi
 600        MODULES="${MODULES}${MODULES:+ }${mod}"
 601
 602        # 真正 source 模块文件,把 <mod>_pre_start/_start/_stop 等函数载入
 603        # shellcheck disable=SC1090
 604        if ! . "${MODULESDIR}/${mod}.sh"; then
 605            eend 1 "${RC_SVCNAME}: error loading module \`${mod}'"
 606            exit 1
 607        fi
 608
 609        [ -z "${provides}" ] && continue
 610
 611        # ---- 虚拟能力包装 ----
 612        # 如果模块提供虚拟能力(如 dhcp),则定义 dhcp_pre_start/dhcp_start/
 613        # dhcp_post_start 包装函数,转发到真实模块的实现。
 614        # 这样 config_eth0="dhcp" 时,主流程调用 dhcp_start 即可,
 615        # 无需关心底层是 dhcpcd 还是 dhclient。
 616        local f=
 617        for f in pre_start start post_start; do
 618            inner=$(command -v "${mod}_${f}")
 619            eval "${provides}_${f}() { [ '${inner}' = '${mod}_${f}' ] || return 0; ${mod}_${f} \"\$@\"; }"
 620        done
 621
 622        eval module_${mod}_provides="${provides}"        # 记录 mod -> 能力
 623        eval module_${provides}_providedby="${mod}"      # 记录 能力 -> mod(当前提供者)
 624    done
 625
 626    # ---- 用户偏好优先 ----
 627    # 如果 mymods 里显式列出了某模块(如 modules_eth0="dhcpcd"),
 628    # 则把该模块提供的虚拟能力包装改指向它(覆盖默认提供者),
 629    # 实现"多种 dhcp 客户端中用户指定用哪个"
 630    for mod in ${mymods}; do
 631        case " ${MODULES} " in
 632            *" ${mod} "*)
 633            eval x=\$module_${mod}_provides
 634            [ -z "${x}" ] && continue
 635            for f in pre_start start post_start; do
 636                inner=$(command -v "${mod}_${f}")
 637                eval "${x}_${f}() { [ '${inner}' = '${mod}_${f}' ] || return 0; ${mod}_${f} \"\$@\"; }"
 638            done
 639            eval module_${x}_providedby="${mod}"         # 修改能力提供者
 640            ;;
 641        esac
 642    done
 643
 644    # ---- 收尾 ----
 645    # 启动时:去掉"被别的模块替代提供虚拟能力"的重复模块
 646    #   (例如用户指定 dhcpcd,则 dhclient 被从列表剔除)
 647    # 停止时:反转列表,让模块以相反顺序停止(类似栈的 LIFO)
 648    local LIST="${MODULES}" p=
 649    MODULES=
 650    if ${starting}; then
 651        for mod in ${LIST}; do
 652            eval x=\$module_${mod}_provides
 653            if [ -n "${x}" ]; then
 654                eval p=\$module_${x}_providedby
 655                [ "${mod}" != "${p}" ] && continue    # 该能力已由别的模块提供 → 丢弃
 656            fi
 657            MODULES="${MODULES}${MODULES:+ }${mod}"
 658        done
 659    else
 660        for mod in ${LIST}; do
 661            MODULES="${mod}${MODULES:+ }${MODULES}"   # 头插法反转
 662        done
 663    fi
 664
 665    veinfo "Loaded modules: ${MODULES}"
 666}
 667
 668# ====================================================================
 669# _load_config —— 解析接口配置到 config_0, config_1, ... 伪数组
 670# 配置来源(/etc/conf.d/net):
 671#   config_eth0="192.168.1.10/24
 672#   192.168.2.10/24"
 673#   fallback_eth0="dhcp"     ← 主配置失败时的后备
 674# 结果存为 config_0='...' config_1='...' config_2= (空为终止)
 675# ====================================================================
 676_load_config()
 677{
 678    local config='' fallback=''
 679    config="$(_get_array "config_${IFVAR}")"
 680    fallback="$(_get_array fallback_${IFVAR})"
 681
 682    config_index=0
 683    local IFS="$__IFS"                        # IFS=换行:按行拆分
 684    set -- ${config}
 685
 686    # 兼容"空格分隔的多个 CIDR 写在同一行"的旧式写法:
 687    # config_eth0="10.0.0.1/24 10.0.0.2/24"
 688    # 但如果空格后面的内容不是 IP 地址(而是参数,如 "dhcp" 的选项),
 689    # 则不按空格拆分,仍按整行处理
 690    if [ $# = 1 ]; then
 691        unset IFS
 692        set -- ${config}
 693        # 如果第 2 个字段既不含 . 也不含 :,说明它不是 IP 地址,
 694        # 而是第一个配置的参数 → 恢复按换行拆分
 695        if [ "${2#*.*}" = "${2}" ] && [  "${2#*:*}" = "${2}" ]; then
 696            # Not an IPv4/IPv6
 697            local IFS="$__IFS"
 698            set -- ${config}
 699        fi
 700    fi
 701
 702    # 回环接口强制配置 127.0.0.1/8(除非用户显式写了 "null")
 703    if [ "${IFACE}" = "lo" ] || [ "${IFACE}" = "lo0" ]; then
 704        if [ "$1" != "null" ]; then
 705               config_0="127.0.0.1/8"
 706            config_index=1
 707        fi
 708    else
 709        # 没写配置 → 默认 DHCP,并给出警告
 710        if [ -z "$1" ]; then
 711            ewarn "config_${IFVAR} not specified; defaulting to DHCP"
 712            config_0="dhcp"
 713            config_index=1
 714        fi
 715    fi
 716
 717
 718    # 把每条配置存入 config_<n> 变量(伪数组),
 719    # 用变量而非真实数组是为了让模块可以动态修改配置
 720    for cmd; do
 721        eval config_${config_index}="'${cmd}'"
 722        : $(( config_index += 1 ))
 723    done
 724    # Terminate the list
 725    eval config_${config_index}=              # 空字符串作为结束标志
 726
 727    # 同样处理 fallback 列表
 728    config_index=0
 729    for cmd in ${fallback}; do
 730        eval fallback_${config_index}="'${cmd}'"
 731        : $(( config_index += 1 ))
 732    done
 733    # Terminate the list
 734    eval fallback_${config_index}=
 735
 736    # 置为 -1 而不是 0:调用方使用前先 +1,
 737    # 这样模块即使不重置索引也能正常工作
 738    config_index=-1
 739}
 740
 741# ====================================================================
 742# 供模块/用户钩子调用的辅助函数
 743# ====================================================================
 744
 745# _run_if CMD [IFACE] —— 在"指定的另一个接口"上下文里执行 CMD,
 746# 同时保存/恢复当前的 IFACE/IFVAR,防止互相覆盖
 747_run_if()
 748{
 749    local cmd=$1 iface=$2 ifr=${IFACE} ifv=${IFVAR}
 750    # Ensure that we don't stamp on real values
 751    local IFACE='' IFVAR=''
 752    shift
 753    if [ -n "${iface}" ]; then
 754        IFACE="${iface}"
 755        [ "${iface}" != "${ifr}" ] && IFVAR=$(shell_var "${IFACE}")
 756    else
 757        IFACE=${ifr}
 758        IFVAR=${ifv}
 759    fi
 760    ${cmd}
 761}
 762# 以下四个是模块常用接口:可以操作别的接口(如网桥先 up 成员口)
 763interface_exists()
 764{
 765    _run_if _exists "$@"
 766}
 767interface_up()
 768{
 769    _run_if _up "$@"
 770}
 771interface_down()
 772{
 773    _run_if _down "$@"
 774}
 775# 接口类型记录/查询(存入 service 的 kv 存储,跨函数调用持久)
 776set_interface_type()
 777{
 778    service_set_value iface_type "$@"
 779}
 780get_interface_type()
 781{
 782    ( RC_SVCNAME="net.$IFACE" service_get_value iface_type )
 783}
 784is_interface_type()
 785{
 786    [ "$(get_interface_type)" = "$1" ]
 787}
 788
 789# ====================================================================
 790# start() —— 启动接口(OpenRC 服务入口)
 791# ====================================================================
 792start()
 793{
 794    local IFACE=''
 795    local IFVAR=''
 796    local oneworked=false          # 是否至少有一条配置成功
 797    local fallback=false           # 是否正在使用 fallback 配置
 798    local module=''
 799    local cmd=''
 800    local our_metric=''
 801    local metric=0
 802    local _up_before_preup=''
 803    IFACE=$(get_interface)
 804    IFVAR=$(shell_var "${IFACE}")
 805    # up_before_preup_<IFVAR>:执行 preup 钩子前是否先 ifconfig up 接口
 806    eval _up_before_preup="\$up_before_preup_${IFVAR}"
 807    # shellcheck disable=SC2154
 808    [ -z "${_up_before_preup}" ] && _up_before_preup=$up_before_preup
 809
 810    einfo "Bringing up interface ${IFACE}"
 811    eindent                                   # 输出缩进一级
 812
 813    # 加载模块(若上层流程尚未加载)
 814    if [ -z "${MODULES}" ]; then
 815        local MODULES=
 816        _load_modules true
 817    fi
 818
 819    # 阶段 1:各模块的 <mod>_pre_up(如创建 tun/tap 设备、vlan 接口)
 820    for module in ${MODULES}; do
 821        if [ "$(command -v "${module}_pre_up")" = "${module}_pre_up" ]; then
 822            ${module}_pre_up || exit $?
 823        fi
 824    done
 825
 826    # 阶段 2:用户自定义 preup() 钩子
 827    # 在 preup 前后各 up 一次接口:先 up 是为了 preup 里能操作接口;
 828    # preup 后再 up 是防止用户在 preup 里误把它 down 掉
 829    if [ "$(command -v preup)" = "preup" ]; then
 830        yesno "${_up_before_preup:-yes}" && _up 2>/dev/null
 831        ebegin "Running preup"
 832        eindent
 833        preup || return 1
 834        eoutdent
 835    fi
 836
 837    _up 2>/dev/null                           # 正式把接口置为 UP
 838
 839    # 阶段 3:各模块 <mod>_pre_start(如启动 wpa_supplicant)
 840    for module in ${MODULES}; do
 841        if [ "$(command -v "${module}_pre_start")" = "${module}_pre_start" ]; then
 842            ${module}_pre_start || exit $?
 843        fi
 844    done
 845
 846    # 阶段 4:确认接口真实存在(可能 pre_start 刚创建了它,或等待 USB 设备出现)
 847    if ! _wait_for_presence; then
 848        eerror "ERROR: interface ${IFACE} does not exist"
 849        eerror "Ensure that you have loaded the correct kernel module for your hardware"
 850        return 1
 851    fi
 852
 853    # 阶段 5:等待载波。若系统有 devd(热插拔守护)在跑,
 854    # 没有载波也不算致命——把服务标记为 inactive,等插线后 devd 会重新触发启动
 855    if ! _wait_for_carrier; then
 856        if service_started devd; then
 857            ewarn "no carrier, but devd will start us when we have one"
 858            mark_service_inactive "${RC_SVCNAME}"
 859        else
 860            eerror "no carrier"
 861        fi
 862        return 1
 863    fi
 864
 865    # 阶段 6:加载配置(config_<IFVAR> → config_0, config_1, ...)
 866    local config='' config_index=''
 867    _load_config
 868    config_index=0
 869
 870    # 计算路由 metric:用户显式配置优先;
 871    # 否则非 lo 接口用接口索引号(ifindex),保证多接口默认路由有确定优先级
 872    eval our_metric=\$metric_${IFVAR}
 873    if [ -n "${our_metric}" ]; then
 874        metric=${our_metric}
 875    elif [ "${IFACE}" != "lo" ] && [ "${IFACE}" != "lo0" ]; then
 876        : $(( metric += $(_ifindex) ))
 877    fi
 878
 879    # 阶段 7:逐条执行配置
 880    while true; do
 881        eval config=\$config_${config_index}
 882        [ -z "${config}" ] && break           # 空值 = 结束
 883
 884        set -- ${config}
 885        if [ "$1" != "null" ] && [ "$1" != "noop" ]; then
 886            ebegin "$1"                       # 打印 " * <config> ..."
 887        fi
 888        eindent
 889        case "$1" in
 890            noop)
 891                # noop:只检查接口是否已有地址(用于"别的工具已配好"的场景)
 892                if [ -n "$(_get_inet_address)" ]; then
 893                    oneworked=true
 894                    break
 895                fi
 896                ;;
 897            null) :;;                         # null:什么都不做
 898            [0-9]*|*:*)
 899                # 以数字开头 → IPv4/CIDR;含冒号 → IPv6:直接添加静态地址
 900                _add_address ${config};;
 901            *)
 902                # 其他关键字(如 dhcp、pppoe)→ 调用虚拟能力函数 <关键字>_start
 903                # (即 _load_modules 里包装好的,转发到具体模块)
 904                if [ "$(command -v "${config}_start")" = "${config}_start" ]; then
 905                    "${config}"_start
 906                else
 907                    eerror "nothing provides \`${config}'"
 908                fi
 909                ;;
 910        esac
 911        # ---- 失败处理与 fallback ----
 912        if eend $?; then
 913            oneworked=true
 914        else
 915            eval config=\$fallback_${config_index}
 916            if [ -n "${config}" ]; then
 917                fallback=true
 918                eoutdent
 919                ewarn "Trying fallback configuration ${config}"
 920                eindent
 921                # 用 fallback 配置替换当前条目,索引 -1 使其下一轮重新执行
 922                eval config_${config_index}=\$config
 923                unset fallback_${config_index}
 924                : $(( config_index -= 1 ))
 925            fi
 926        fi
 927        eoutdent
 928        : $(( config_index += 1 ))
 929    done
 930
 931    # 一条配置都没成功 → 执行 failup 钩子(若定义)后返回失败
 932    if ! ${oneworked}; then
 933        if [ "$(command -v failup)" = "failup" ]; then
 934            ebegin "Running failup"
 935            eindent
 936            failup
 937            eoutdent
 938        fi
 939        return 1
 940    fi
 941
 942    # 阶段 8:添加路由
 943    # 若使用了 fallback 配置,优先读 fallback_routes_<IFVAR>
 944    local first=true routes=
 945    if ${fallback}; then
 946        routes="$(_get_array "fallback_routes_${IFVAR}")"
 947    fi
 948    if [ -z "${routes}" ]; then
 949        routes="$(_get_array "routes_${IFVAR}")"
 950    fi
 951    # lo 接口自动补 127.0.0.0/8 的本地路由
 952    if [ "${IFACE}" = "lo" ] || [ "${IFACE}" = "lo0" ]; then
 953        if [ "${config_0}" != "null" ]; then
 954            routes="127.0.0.0/8 via 127.0.0.1
 955${routes}"
 956        fi
 957    fi
 958
 959    service_set_value "nodev_routes" ""       # 清空"非本设备路由"记录(供后续查询)
 960
 961    local OIFS="${IFS}" SIFS="${IFS-y}"
 962    local IFS="$__IFS"                        # 路由按行拆分
 963    local cmd_head fam
 964    for cmd in ${routes}; do
 965        unset IFS                             # 行内再按空格分词
 966        if ${first}; then
 967            first=false
 968            einfo "Adding routes"
 969        fi
 970
 971        # 行首可写 "-6 "/"-4 " 强制指定地址族
 972        case ${cmd} in
 973            -6" "*) fam="-6"; cmd=${cmd#-6 };;
 974            -4" "*) fam="-4"; cmd=${cmd#-4 };;
 975        esac
 976
 977        # 识别特殊路由类型关键字(blackhole/prohibit/throw/unreachable)
 978        cmd_head=
 979        case ${cmd%% *} in
 980            blackhole|prohibit|throw|unreachable) cmd_head="${cmd_head} ${cmd%% *}"; cmd=${cmd#* };;
 981        esac
 982
 983        eindent
 984        ebegin ${cmd_head} ${cmd}
 985        # 用户没写 -net/-host 时,根据目标形式自动推断:
 986        #   带 netmask 关键字 / 带前缀长度的 IPv4 / default → -net
 987        #   IPv4/32 或 IPv6/128 → -host
 988        #   其余裸 IP → -host
 989        case ${cmd} in
 990            -net\ *|-host\ *);;
 991            *\ netmask\ *)                     cmd="-net ${cmd}";;
 992            *.*.*.*/32*)                       cmd="-host ${cmd}";;
 993            *.*.*.*/*|0.0.0.0|0.0.0.0\ *)     cmd="-net ${cmd}";;
 994            default|default\ *)               cmd="-net ${cmd}";;
 995            *:*/128*)                          cmd="-host ${cmd}";;
 996            *:*/*)                             cmd="-net ${cmd}";;
 997            *)                                 cmd="-host ${cmd}";;
 998        esac
 999        _add_route ${fam} ${cmd_head} ${cmd}
1000        eend $?
1001        eoutdent
1002    done
1003    # 恢复 IFS 原状
1004    if [ "${SIFS}" = "y" ]; then
1005        unset IFS
1006    else
1007        IFS="${OIFS}"
1008    fi
1009
1010    # 阶段 9:各模块 <mod>_post_start(如启动防火墙规则、通告)
1011    for module in ${MODULES}; do
1012        if [ "$(command -v "${module}_post_start")" = "${module}_post_start" ]; then
1013            ${module}_post_start || exit $?
1014        fi
1015    done
1016
1017    # 阶段 10:用户 postup() 钩子
1018    if [ "$(command -v postup)" = "postup" ]; then
1019        ebegin "Running postup"
1020        eindent
1021        postup
1022        eoutdent
1023    fi
1024
1025    return 0
1026}
1027
1028# ====================================================================
1029# stop() —— 停止接口(OpenRC 服务入口)
1030# ====================================================================
1031stop()
1032{
1033    # 系统关机时不真正关闭网络(根文件系统可能挂在 NFS 上等场景);
1034    # 不使用 noshutdown 关键字是为了从单用户模式切回多用户时能被正确重启。
1035    # keep_network=NO 可覆盖此行为。
1036    yesno ${keep_network:-YES} && yesno $RC_GOINGDOWN && return 0
1037
1038    local IFACE='' module=''
1039    local IFVAR=''
1040    IFACE=$(get_interface)
1041    IFVAR=$(shell_var "${IFACE}")
1042
1043    einfo "Bringing down interface ${IFACE}"
1044    eindent
1045
1046    # 停止模式加载模块(列表会被反转)
1047    if [ -z "${MODULES}" ]; then
1048        local MODULES=''
1049        _load_modules false
1050    fi
1051
1052    # 用户 predown() 钩子优先;
1053    # 未定义钩子时做安全检查:根分区是网络文件系统则禁止停网
1054    if [ "$(command -v predown)" = "predown" ]; then
1055        ebegin "Running predown"
1056        eindent
1057        predown || return 1
1058        eoutdent
1059    else
1060        if is_net_fs /; then
1061            eerror "root filesystem is network mounted -- can't stop ${IFACE}"
1062            return 1
1063        fi
1064    fi
1065
1066    # 各模块 <mod>_pre_stop(如断开 wpa_supplicant 前先通知)
1067    for module in ${MODULES}; do
1068        if [ "$(command -v "${module}_pre_stop")" = "${module}_pre_stop" ]; then
1069            ${module}_pre_stop || exit $?
1070        fi
1071    done
1072
1073    # 各模块 <mod>_stop(停 dhcpcd、kill wpa_supplicant 等)
1074    for module in ${MODULES}; do
1075        if [ "$(command -v "${module}_stop")" = "${module}_stop" ]; then
1076            ${module}_stop
1077        fi
1078    done
1079
1080    # 删除接口上的所有地址(仅当接口仍存在;
1081    # PPP 后台模式除外——"demand" 按需拨号时地址由 pppd 自己管理)
1082    if _exists; then
1083        # PPP can manage it's own addresses when IN_BACKGROUND
1084        # Important in case "demand" set on the ppp link
1085        if ! (yesno ${IN_BACKGROUND} && is_ppp) ; then
1086            _delete_addresses "${IFACE}"
1087        fi
1088    fi
1089
1090    # 各模块 <mod>_post_stop
1091    for module in ${MODULES}; do
1092        if [ "$(command -v "${module}_post_stop")" = "${module}_post_stop" ]; then
1093            ${module}_post_stop
1094        fi
1095    done
1096
1097    # 非后台模式且非回环口:把接口 DOWN 掉
1098    # 可用 ifdown_<IFVAR>=no 或全局 ifdown=no 禁止
1099    if ! yesno ${IN_BACKGROUND} && \
1100    [ "${IFACE}" != "lo" ] && [ "${IFACE}" != "lo0" ]; then
1101        eval module=\$ifdown_${IFVAR}
1102        module=${module:-${ifdown:-YES}}
1103        yesno ${module} && _down 2>/dev/null
1104    fi
1105
1106    # 通知 resolvconf 删除该接口注册的 DNS 配置
1107    command -v resolvconf >/dev/null && resolvconf -d "${IFACE}" 2>/dev/null
1108
1109    # 用户 postdown() 钩子
1110    if [ "$(command -v "postdown")" = "postdown" ]; then
1111        ebegin "Running postdown"
1112        eindent
1113        postdown
1114        eoutdent
1115    fi
1116
1117    return 0
1118}

接口 #

服务脚本 #

/etc/init.d/net.lo 是 netifrc 自带的服务脚本(通用模板), 当你需要管理其它网络接口,只需创建一个指向 net.lo 的符号链接:

# ln -s /etc/init.d/net.lo /etc/init.d/net.<interface_name>

net.<interface_name> 运行时能从服务名称中获取接口名称:

文件/etc/init.d/net.lo
SHDIR="/lib/netifrc/sh"          # 脚本路径
MODULESDIR="/lib/netifrc/net"    # 模块路径

if [ -f "$SHDIR/functions.sh" ]; then
    . "$SHDIR/functions.sh"      # 加载脚本
else
    echo "$SHDIR/functions.sh missing. Exiting"
    exit 1
fi

start() {
    ...
    IFACE=$(get_interface)       # 获取接口名称
    ...
}
文件/lib/netifrc/sh/functions.sh
get_interface() {
    case $INIT in
        openrc)
        printf ${RC_SVCNAME#*.};;    # 打印接口名称
    systemd)
        printf ${RC_IFACE};;
    *)
        eerror "Init system not supported. Aborting"
        exit 1;;
    esac
}

RC_SVCNAME 就是服务的名称,是 OpenRC 在执行服务脚本时自动设置的环境变量。

配置文件 #

文件/etc/conf.d/net
modules_wlp4s0="wpa_supplicant dhcpcd"
config_wlp4s0="dhcp"
modules_<interface_name>指定优先使用的模块,模块路径在 /lib/netifrc/net。
config_<interface_name>网络接口配置,可以是 dhcp、null、192.168.1.10/24(静态 IP)等。

禁用模块,在模块前加 !:

modules_wlp4s0="!ifconfig"

查看 net 示例配置文件(包含详细说明):

$ less /usr/share/doc/netifrc-*/net.example.bz2

模块 #

net.lo 会遍历 /lib/netifrc/net 下的所有模块,并执行其 depend 函数来建立顺序关系。

以 wpa_supplicant 和 iwconfig 模块为例:

文件/lib/netifrc/net/wpa_supplicant.sh
wpa_supplicant_depend()
{
    after macnet plug
    before interface
    provide wireless

    # 比起 iwconfig 我们更偏好当前模块
    after iwconfig
}
...
文件/lib/netifrc/net/iwconfig.sh
iwconfig_depend()
{
    program iwconfig
    after plug
    before interface
    provide wireless
}
...
语句含义
program iwconfig只有当系统中存在 iwconfig 二进制文件时,此模块才生效。
provide wireless表示提供 wireless 功能。
after plug当前模块排在 plug 模块后面。
before interface当前模块排在 interface 模块前面。
after/before 表示的是顺序关系,而不是依赖关系,只影响模块的排序结果。对于 provide 相同功能的模块,排在后面的模块会优先被使用。

查看提供功能的模块:

$ cd /lib/netifrc/net
$ grep -w provide * | sort -k3
dhclient.sh:         provide dhcp
dhcpcd.sh:           provide dhcp
udhcpc.sh:           provide dhcp
dhclientv6.sh:       provide dhcpv6
ifconfig.sh:         provide interface
iproute2.sh:         provide interface
ipppd.sh:            provide isdn
ifplugd.sh:          provide plug
netplugd.sh:         provide plug
pppd.sh:             provide ppp
iwconfig.sh:         provide wireless
iwd.sh:              provide wireless
iw.sh:               provide wireless
wpa_supplicant.sh:   provide wireless

加载顺序 #

简化了下 net.lo 脚本,方便查看模块排序:

文件load_modules.sh
  1#!/usr/bin/sh
  2
  3SHDIR="/lib/netifrc/sh"
  4MODULESDIR="/lib/netifrc/net"
  5. "$SHDIR/functions.sh"
  6MODULESLIST=./nettree
  7IFACE=wlp4s0
  8IFVAR=$IFACE
  9
 10_program_available()
 11{
 12    [ -z "$1" ] && return 0
 13    local x=
 14    for x; do
 15        case "${x}" in
 16            /*) [ -x "${x}" ] && break;;
 17            *) type "${x}" >/dev/null 2>&1 && break;;
 18        esac
 19        x=
 20    done
 21    [ -n "${x}" ] && echo $x && return 0
 22    return 1
 23}
 24
 25_gen_module_list()
 26{
 27    local x='' f='' force="$1"
 28    if ! ${force} ; then
 29        if [ -s "${MODULESLIST}" ] && [ "${MODULESLIST}" -nt /proc/$$/status ]; then
 30            echo "Discarding cached module list ($MODULESLIST) as it's newer current time!"
 31        elif [ -s "${MODULESLIST}" ] && [ "${MODULESLIST}" -nt "${MODULESDIR}" ]; then
 32            local update=false
 33            for x in "${MODULESDIR}"/*.sh; do
 34                [ -e "${x}" ] || continue
 35                if [ "${x}" -nt "${MODULESLIST}" ]; then
 36                    update=true
 37                    break
 38                fi
 39            done
 40            ${update} || return 0
 41        fi
 42    fi
 43
 44    # Run in a subshell to protect the main script
 45    (
 46    after() {
 47        eval ${MODULE}_after="\"\${${MODULE}_after}\${${MODULE}_after:+ }$*\""
 48    }
 49
 50    before() {
 51        local mod=${MODULE}
 52        local MODULE=
 53        for MODULE; do
 54            after "${mod}"
 55        done
 56    }
 57
 58    program() {
 59        if [ "$1" = "start" ] || [ "$1" = "stop" ]; then
 60            local s="$1"
 61            shift
 62            eval ${MODULE}_program_${s}="\"\${${MODULE}_program_${s}}\${${MODULE}_program_${s}:+ }$*\""
 63        else
 64            eval ${MODULE}_program="\"\${${MODULE}_program}\${${MODULE}_program:+ }$*\""
 65        fi
 66    }
 67
 68    provide() {
 69        eval ${MODULE}_provide="\"\${${MODULE}_provide}\${${MODULE}_provide:+ }$*\""
 70        local x
 71        for x in "$@"; do
 72            eval ${x}_providedby="\"\${${MODULE}_providedby}\${${MODULE}_providedby:+ }${MODULE}\""
 73        done
 74    }
 75
 76    for MODULE in "${MODULESDIR}"/*.sh; do
 77        sh -n "${MODULE}" || continue
 78        # shellcheck disable=SC1090
 79        . "${MODULE}" || continue
 80        MODULE=${MODULE#${MODULESDIR}/}
 81        MODULE=${MODULE%.sh}
 82        eval "${MODULE}_depend"
 83        MODULES="${MODULES} ${MODULE}"
 84    done
 85
 86    VISITED=
 87    SORTED=
 88    visit() {
 89        case " ${VISITED} " in
 90            *" $1 "*) return;;
 91        esac
 92        VISITED="${VISITED} $1"
 93
 94        eval AFTER=\$${1}_after
 95        for MODULE1 in ${AFTER}; do
 96            eval PROVIDEDBY=\$${MODULE1}_providedby
 97            if [ -n "${PROVIDEDBY}" ]; then
 98                for MODULE2 in ${PROVIDEDBY}; do
 99                    visit "${MODULE2}"
100                done
101            else
102                visit "${MODULE1}"
103            fi
104        done
105
106        eval PROVIDE=\$${1}_provide
107        for MODULE in ${PROVIDE}; do
108            visit "${MODULE}"
109        done
110
111        eval PROVIDEDBY=\$${1}_providedby
112        [ -z "${PROVIDEDBY}" ] && SORTED="${SORTED} $1"
113    }
114
115    for MODULE in ${MODULES}; do
116        visit "${MODULE}"
117    done
118
119    echo -e "\n按顺序关系排序后的模块列表:"
120    for mod in ${SORTED}; do
121        echo -e "\t$mod"
122    done
123
124    # Create atomically
125    TMPMODULESLIST=${MODULESLIST}.$$
126    printf "" > "${TMPMODULESLIST}"
127    i=0
128    for MODULE in ${SORTED}; do
129        eval PROGRAM=\$${MODULE}_program
130        eval PROGRAM_START=\$${MODULE}_program_start
131        eval PROGRAM_STOP=\$${MODULE}_program_stop
132        eval PROVIDE=\$${MODULE}_provide
133        echo "module_${i}='${MODULE}'"
134        echo "module_${i}_program='${PROGRAM}'"
135        echo "module_${i}_program_start='${PROGRAM_START}'"
136        echo "module_${i}_program_stop='${PROGRAM_STOP}'"
137        echo "module_${i}_provide='${PROVIDE}'"
138        : $(( i += 1 ))
139    done >> "${TMPMODULESLIST}"
140    echo "module_${i}=" >> "${TMPMODULESLIST}"
141    mv -f "${TMPMODULESLIST}" "${MODULESLIST}"
142    )
143
144    return 0
145}
146
147_load_modules()
148{
149    local starting=$1 mymods=
150
151    _gen_module_list true
152    . "${MODULESLIST}"
153
154    MODULES=
155    if [ "${IFACE}" != "lo" ] && [ "${IFACE}" != "lo0" ]; then
156        eval mymods=\$modules_${IFVAR}
157        # shellcheck disable=SC2154
158        [ -z "${mymods}" ] && mymods=${modules}
159    fi
160
161    local i=-1 x='' mod='' f='' provides=''
162    echo -e "\n跳过的模块:"
163    while true; do
164        : $(( i += 1 ))
165        eval mod=\$module_${i}
166        [ -z "${mod}" ] && break
167        [ -e "${MODULESDIR}/${mod}.sh" ] || printf "\t%-20s:模块不存在 %s\n" $mod "${MODULESDIR}/${mod}.sh"
168        [ -e "${MODULESDIR}/${mod}.sh" ] || continue
169
170        eval set -- \$module_${i}_program
171        if [ -n "$1" ]; then
172            if ! _program_available "$@" >/dev/null; then
173                printf "\t%-20s:缺失可执行程序 %s\n" $mod "$*"
174                continue
175            fi
176        fi
177        if ${starting}; then
178            eval set -- \$module_${i}_program_start
179        else
180            eval set -- \$module_${i}_program_stop
181        fi
182        if [ -n "$1" ]; then
183            if ! _program_available "$@" >/dev/null; then
184                printf "\t%-20s:缺失可执行程序 %s\n" $mod "$*"
185                continue
186            fi
187        fi
188
189        eval provides=\$module_${i}_provide
190        if ${starting}; then
191            case " ${mymods} " in
192                *" !${mod} "*) continue;;
193                *" !${provides} "*) [ -n "${provides}" ] && continue;;
194            esac
195        fi
196        MODULES="${MODULES}${MODULES:+ }${mod}"
197
198        # Now load and wrap our functions
199        # shellcheck disable=SC1090
200        if ! . "${MODULESDIR}/${mod}.sh"; then
201            echo "${RC_SVCNAME}: error loading module \`${mod}'"
202            exit 1
203        fi
204
205        [ -z "${provides}" ] && continue
206
207        # Wrap our provides
208        local f=
209        for f in pre_start start post_start; do
210            inner=$(command -v "${mod}_${f}")
211            eval "${provides}_${f}() { [ '${inner}' = '${mod}_${f}' ] || return 0; ${mod}_${f} \"\$@\"; }"
212        done
213
214        eval module_${mod}_provides="${provides}"
215        eval module_${provides}_providedby="${mod}"
216    done
217
218    # Wrap our preferred modules
219    for mod in ${mymods}; do
220        case " ${MODULES} " in
221            *" ${mod} "*)
222            eval x=\$module_${mod}_provides
223            [ -z "${x}" ] && continue
224            for f in pre_start start post_start; do
225                inner=$(command -v "${mod}_${f}")
226                eval "${x}_${f}() { [ '${inner}' = '${mod}_${f}' ] || return 0; ${mod}_${f} \"\$@\"; }"
227            done
228            eval module_${x}_providedby="${mod}"
229            ;;
230        esac
231    done
232
233    # Finally remove any duplicated provides from our list if we're starting
234    # Otherwise reverse the list
235    local LIST="${MODULES}" p=
236    MODULES=
237    if ${starting}; then
238        for mod in ${LIST}; do
239            eval x=\$module_${mod}_provides
240            if [ -n "${x}" ]; then
241                eval p=\$module_${x}_providedby
242                [ "${mod}" != "${p}" ] && printf "\t%-20s:功能重复 %s -> %s\n" ${mod} ${x} ${p} && continue
243            fi
244            MODULES="${MODULES}${MODULES:+ }${mod}"
245        done
246    else
247        for mod in ${LIST}; do
248            MODULES="${mod}${MODULES:+ }${MODULES}"
249        done
250    fi
251
252    echo -e "\n最终的模块列表:"
253
254    for mod in ${MODULES}; do
255        echo -e "\t$mod"
256    done
257}
258
259_load_modules true

运行结果:

$ ./load_modules.sh
按顺序关系排序后的模块列表:
	adsl
	apipa
	arping
	bonding
	br2684ctl
	l2tp
	tuntap
	bridge
	ccwgroup
	clip
	ethtool
	dummy
	hsr
	macvlan
	macchanger
	macnet
	rename
	netplugd
	ifplugd
	ipppd
	iwconfig
	iwd
	iw
	qmi
	wpa_supplicant
	ssidnet
	wireguard
	ifconfig
	iproute2
	firewalld
	pppd
	system
	vlan
	udhcpc
	dhclient
	dhclientv6
	dhcpcd
	ip6rd
	ip6to4
	ip6token
	veth

跳过的模块:
	adsl                :缺失可执行程序 adsl-start pppoe-start
	br2684ctl           :缺失可执行程序 br2684ctl
	clip                :缺失可执行程序 atmsigd
	ethtool             :缺失可执行程序 ethtool
	rename              :模块不存在 /lib/netifrc/net/rename.sh
	netplugd            :缺失可执行程序 netplugd
	ifplugd             :缺失可执行程序 ifplugd
	ipppd               :缺失可执行程序 ipppd
	iwconfig            :缺失可执行程序 iwconfig
	iwd                 :缺失可执行程序 /usr/libexec/iwd
	firewalld           :缺失可执行程序 firewall-cmd
	pppd                :缺失可执行程序 pppd
	udhcpc              :缺失可执行程序 busybox
	dhclient            :缺失可执行程序 dhclient
	dhclientv6          :缺失可执行程序 dhclient
	iw                  :功能重复 wireless -> wpa_supplicant
	ifconfig            :功能重复 interface -> iproute2

最终的模块列表:
	apipa
	arping
	bonding
	l2tp
	tuntap
	bridge
	ccwgroup
	dummy
	hsr
	macvlan
	macchanger
	macnet
	qmi
	wpa_supplicant
	ssidnet
	wireguard
	iproute2
	system
	vlan
	dhcpcd
	ip6rd
	ip6to4
	ip6token
	veth

模拟 net 配置文件,优先使用 ifconfig 而不是 iproute2:

$ modules_wlp4s0="ifconfig" ./load_modules.sh
	...
	iproute2            :功能重复 interface -> ifconfig

排序好的模块在 start 函数中会被遍历执行 ${module}_pre_up、${module}_pre_start 等函数, 一些模块需要在 /etc/conf.d/net 中额外配置参数(不需要就不配置,模块会直接返回)。

wpa_supplicant #

配置文件 #

文件/etc/wpa_supplicant/wpa_supplicant.conf
# 允许 wheel 组的用户控制 wpa_supplicant
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=wheel

# 允许 wpa_gui / wpa_cli 可写该文件
update_config=1

添加无线网络,名称不能包含中文字符或其它特殊字符,只能是 ascii 字符:

# wpa_passphrase 网络名称 密码 >> /etc/wpa_supplicant/wpa_supplicant.conf

上面命令会在 wpa_supplicant.conf 中添加如下内容:

network={
        ssid="wifi1"
        psk=6ad077ee70b4541967ee9c0bf0dc902
}

如果你添加了多个网络,可以使用 priority 变量设置连接优先级,数字越大,优先级越高:

network={
        ssid="wifi1"
        psk=6ad077ee70b4541967ee9c0bf0dc90
        priority=10
}

其它可用的变量见 wpa_supplicant.conf(5) 中的 NETWORK BLOCKS 一节。

手动连接网络:

# wpa_supplicant -B -i wlp4s0 -c /etc/wpa_supplicant/wpa_supplicant.conf

获取 IP,可用 dhclient 或 dhcpcd:

# dhclient -i wlp4s0 -v
# dhcpcd wlp4s0

netifrc 配置 #

文件/etc/conf.d/net
modules_wlp4s0="wpa_supplicant dhcpcd"
config_wlp4s0="dhcp"

添加开机启动:

# ln -s /etc/init.d/net.lo /etc/init.d/net.wlp4s0
# rc-update add net.wlp4s0 default

dhcpd 不用添加,由 net.wlp4s0 启动。

其它 #

wpa_cli 交互式命令行工具 #

直接在终端执行 wpa_cli 命令,即可进入交互模式:

$ wpa_cli -i wlp4s0
Interactive mode
> help           # 列出所有指令,及其用法
略...
> list_networks  # 打印已添加的网络
network id / ssid / bssid / flags
0       wifi1    any
1       wifi2    any     [CURRENT]
> scan           # 扫描网络
OK
<3>CTRL-EVENT-SCAN-STARTED
<3>CTRL-EVENT-SCAN-RESULTS
> scan_results   # 查看扫描结果
1c:16:a1:12:1d:a8       5765    -27     [WPA-PSK-CCMP][WPA2-PSK-CCMP][ESS]      wifi1
10:1b:13:d4:14:6c       5200    -62     [WPA-PSK-CCMP][WPA2-PSK-CCMP][ESS]      wifi2
1c:16:a7:12:4d:a6       2412    -23     [WPA-PSK-CCMP][WPA2-PSK-CCMP][ESS]      wifi3
> add_network    # 添加网络
2
<3>CTRL-EVENT-NETWORK-ADDED 2
> set_network 2 ssid "wifi3"       # 设置网络2的ssid (配置文件中 network 的 ssid 变量)
> set_network 2 psk "passphrase"   # 设置网络2的密码
> enable_network 2                 # 使能网络2
> save_config                      # 保存到配置文件
> select_network 2                 # 连接到网络2

非交互模式,直接在命令后面跟指令,例如:

$ wpa_cli -i wlp4s0 list_networks

动作脚本 #

Gentoo 上的 wpa_supplicant 附带了一个动作脚本 /etc/wpa_supplicant/wpa_cli.sh, wpa_cli 会以守护进程方式监听 wpa_supplicant 发出的网络事件,当事件发生时自动执行该脚本。

可以在模块中看到相关代码,通过 -a 参数指定动作脚本:

文件/lib/netifrc/net/wpa_supplicant.sh
    local actfile=/etc/wpa_supplicant/wpa_cli.sh
    ...
    ebegin "Starting wpa_cli on" "${IFACE}"
    start-stop-daemon --start --exec "${wpac}" \
        --pidfile "/run/wpa_cli-${IFACE}.pid" \
        -- ${cliopts} -a "${actfile}" -p "${ctrl_dir}" -i "${IFACE}" \
        -P "/run/wpa_cli-${IFACE}.pid" -B

动作脚本如下:

文件/etc/wpa_supplicant/wpa_cli.sh
#!/bin/sh
# Copyright 1999-2011 Gentoo Foundation
# Written by Roy Marples <uberlord@gentoo.org>
# Distributed under the terms of the GNU General Public License v2
# Alternatively, this file may be distributed under the terms of the BSD License

if [ -z "$1" -o -z "$2" ]; then
	logger -t wpa_cli "Insufficient parameters"
	exit 1
fi

INTERFACE="$1"
ACTION="$2"

# Note, the below action must NOT mark the interface down via ifconfig, ip or
# similar. Addresses can be removed, changed and daemons can be stopped, but
# the interface must remain up for wpa_supplicant to work.

if [ -f /etc/gentoo-release ]; then
	EXEC="/etc/init.d/net.${INTERFACE} --quiet"
else
	logger -t wpa_cli "I don't know what to do with this distro!"
	exit 1
fi

case ${ACTION} in
	CONNECTED)
		EXEC="${EXEC} start"
		;;
	DISCONNECTED)
		EXEC="${EXEC} --nodeps stop"
		;;
	*)
		logger -t wpa_cli "Unknown action ${ACTION}"
		exit 1
		;;
esac

# ${EXEC} can use ${IN_BACKGROUND} so that it knows that the user isn't
# stopping the interface and a background process - like wpa_cli - is.
export IN_BACKGROUND=true

logger -t wpa_cli "interface ${INTERFACE} ${ACTION}"
${EXEC} || logger -t wpa_cli "executing '${EXEC}' failed"

wpa_cli 会根据事件类型(如 CONNECTED 或 DISCONNECTED), 将接口名和动作名作为参数传给脚本。

可以在 wpa_cli 交互模式下看到事件提示,如:

$ wpa_cli
> disable_network 0
OK
<3>CTRL-EVENT-DISCONNECTED bssid=3c:06:a7:a2:4d:a8 reason=3 locally_generated=1

ssid 中文字符无法显示 #

$ wpa_cli -i wlp4s0 scan
$ wpa_cli -i wlp4s0 scan_result | sed 's@\\@\\\\@g' | xargs -L1 echo -e

或者使用 iw 命令扫描:

# iw dev wlp4s0 scan | grep -i ssid | sed 's@\\@\\\\@g' | xargs -L1 echo -e

找到对应的中文 ssid 后,在 wpa_cli 中按之前的步骤操作。

注意:不能直接在 wpa_supplicant.conf 中添加中文字符的 ssid。

参考链接 #