强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 8 章:Dispatcher 事件钩子

第 8 章:Dispatcher 事件钩子

8.1 Dispatcher 概述

NetworkManager Dispatcher 是一个事件触发系统,当网络状态发生变化时自动执行用户定义的脚本。这是实现网络自动化任务的强大机制。

工作原理

网络事件发生
    │
    ▼
NetworkManager 检测到变化
    │
    ▼
Dispatcher 服务触发
    │
    ▼
按字母顺序执行 /etc/NetworkManager/dispatcher.d/ 下的脚本
    │
    ▼
传递参数:接口名、事件类型、连接名

支持的事件类型

事件触发时机说明
up连接激活接口获得 IP 并可用
down连接断开接口失去连接
pre-up连接激活前IP 配置前触发
pre-down连接断开前断开前触发
dhcp4-changeDHCPv4 租约变化获取新的 IPv4 配置
dhcp6-changeDHCPv6 租约变化获取新的 IPv6 配置
vpn-pre-upVPN 连接前VPN 隧道建立前
vpn-upVPN 连接后VPN 隧道已建立
vpn-pre-downVPN 断开前VPN 隧道断开前
vpn-downVPN 断开后VPN 隧道已断开
hostname主机名变化系统主机名变更
connectivity-change连通性变化网络连通状态改变

8.2 创建 Dispatcher 脚本

基本脚本结构

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/99-my-script
# NM Dispatcher 脚本模板

INTERFACE=$1     # 网络接口名(如 eth0, wlan0)
EVENT=$2         # 事件类型(如 up, down)

# 接口状态信息(仅 up/down/dhcp*-change 时可用)
CONNECTION_ID=$(echo "$CONNECTION_ID")    # 连接名称
CONNECTION_UUID=$(echo "$CONNECTION_UUID") # 连接 UUID
IP4_ADDRESS=$(echo "$IP4_ADDRESS_0")       # IPv4 地址
IP4_GATEWAY=$(echo "$IP4_GATEWAY")         # IPv4 网关

case "$EVENT" in
    up)
        echo "$(date): Interface $INTERFACE connected" >> /var/log/nm-dispatcher.log
        ;;
    down)
        echo "$(date): Interface $INTERFACE disconnected" >> /var/log/nm-dispatcher.log
        ;;
    dhcp4-change)
        echo "$(date): DHCP lease changed on $INTERFACE" >> /var/log/nm-dispatcher.log
        ;;
esac

exit 0

脚本规范

# 权限要求
sudo chmod 755 /etc/NetworkManager/dispatcher.d/99-my-script
sudo chown root:root /etc/NetworkManager/dispatcher.d/99-my-script

# 文件命名
# 必须是可执行文件
# 不能以 .bak, .rpmnew, .rpmsave, .swp 结尾
# 按字母顺序执行,建议用数字前缀控制顺序
# 00-49: NM 内部保留
# 50-99: 用户自定义

# 两个执行目录
/etc/NetworkManager/dispatcher.d/pre-up.d/    # 连接前执行
/etc/NetworkManager/dispatcher.d/no-wait.d/   # 不等待完成(异步)

注意:脚本必须由 root 拥有且可执行,否则 NM 会忽略它。

8.3 常用 Dispatcher 脚本示例

脚本 1:网络连接日志

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/50-connection-logger

INTERFACE=$1
EVENT=$2
LOG_FILE="/var/log/nm-connections.log"

if [ -z "$INTERFACE" ] || [ -z "$EVENT" ]; then
    exit 0
fi

echo "$(date '+%Y-%m-%d %H:%M:%S') | $EVENT | $INTERFACE | ${CONNECTION_ID:-N/A} | ${IP4_ADDRESS_0:-N/A}" >> "$LOG_FILE"

exit 0

脚本 2:连接到公司网络时自动启动 VPN

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/60-auto-vpn

INTERFACE=$1
EVENT=$2

# 只在特定网络连接时触发
if [ "$EVENT" = "up" ] && [ "$CONNECTION_ID" = "Office-WiFi" ]; then
    logger -t nm-dispatcher "Office WiFi detected, starting VPN..."
    sleep 3  # 等待网络稳定
    nmcli connection up "Corp-VPN" &
fi

exit 0

脚本 3:网络变化时更新防火墙规则

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/55-firewall-update

INTERFACE=$1
EVENT=$2

case "$EVENT" in
    up)
        # 只对有线网络应用防火墙规则
        if [ "$INTERFACE" = "eth0" ]; then
            /usr/local/bin/update-firewall.sh "$IP4_ADDRESS_0"
            logger -t nm-dispatcher "Firewall updated for $INTERFACE ($IP4_ADDRESS_0)"
        fi
        ;;
    down)
        if [ "$INTERFACE" = "eth0" ]; then
            /usr/local/bin/cleanup-firewall.sh
        fi
        ;;
esac

exit 0

脚本 4:WiFi 切换时调整 DNS

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/52-wifi-dns

INTERFACE=$1
EVENT=$2

if [ "$INTERFACE" != "wlan0" ]; then
    exit 0
fi

case "$EVENT" in
    up)
        if [ "$CONNECTION_ID" = "HomeWiFi" ]; then
            # 家庭网络使用 Pi-hole
            nmcli connection modify "$CONNECTION_ID" ipv4.dns "192.168.1.5"
            logger -t nm-dispatcher "Switched to Pi-hole DNS"
        fi
        ;;
esac

exit 0

脚本 5:连接变化时发送通知

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/60-notify

INTERFACE=$1
EVENT=$2

if [ "$EVENT" = "up" ]; then
    # 如果有桌面环境,发送桌面通知
    if [ -n "$DISPLAY" ]; then
        sudo -u $(logname) DISPLAY=$DISPLAY \
            notify-send "网络已连接" "接口: $INTERFACE\n连接: ${CONNECTION_ID:-unknown}\nIP: ${IP4_ADDRESS_0:-unknown}" \
            --icon=network-transmit-receive
    fi

    # 发送消息到 Slack/钉钉(如果有 webhook)
    if [ -f /etc/nm-webhook.conf ]; then
        WEBHOOK_URL=$(cat /etc/nm-webhook.conf)
        curl -s -X POST "$WEBHOOK_URL" \
            -H 'Content-Type: application/json' \
            -d "{\"text\": \"Network up: $INTERFACE ($CONNECTION_ID) ${IP4_ADDRESS_0}\"}" &
    fi
fi

exit 0

脚本 6:连接时自动挂载 NFS

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/65-nfs-mount

INTERFACE=$1
EVENT=$2

if [ "$EVENT" = "up" ] && [ "$CONNECTION_ID" = "Office-LAN" ]; then
    logger -t nm-dispatcher "Office LAN connected, mounting NFS shares..."
    sleep 2
    mount -a -t nfs4
    mount -a -t cifs
elif [ "$EVENT" = "down" ] && [ "$CONNECTION_ID" = "Office-LAN" ]; then
    logger -t nm-dispatcher "Office LAN disconnected, unmounting NFS shares..."
    umount -a -t nfs4 -f -l
    umount -a -t cifs -f -l
fi

exit 0

脚本 7:DHCP 租约变化时更新 dnsmasq

#!/bin/bash
# /etc/NetworkManager/dispatcher.d/53-dnsmasq-update

INTERFACE=$1
EVENT=$2

if [ "$EVENT" = "dhcp4-change" ] && [ "$INTERFACE" = "eth0" ]; then
    # 更新 dnsmasq 的上游 DNS
    NEW_DNS=$(nmcli -t -f IP4.DNS device show "$INTERFACE" | head -1 | cut -d: -f2)
    if [ -n "$NEW_DNS" ]; then
        echo "server=$NEW_DNS" > /etc/dnsmasq.d/upstream.conf
        systemctl reload dnsmasq
        logger -t nm-dispatcher "Updated upstream DNS to $NEW_DNS"
    fi
fi

exit 0

8.4 Dispatcher 可用环境变量

Dispatcher 脚本可以使用以下环境变量(并非所有事件都有所有变量):

环境变量说明
$1 (INTERFACE)网络接口名
$2 (EVENT)事件类型
$CONNECTION_UUID连接 UUID
$CONNECTION_ID连接名称
$CONNECTION_FILENAME连接文件路径
$CONNECTION_EXTERNAL是否外部连接
$DEVICE_IFACE设备接口名
$DEVICE_IP_IFACEIP 接口名
$IP4_ADDRESS_0第一个 IPv4 地址(CIDR)
$IP4_GATEWAYIPv4 网关
$IP4_NAMESERVERSIPv4 DNS 服务器(空格分隔)
$IP4_DOMAINSIPv4 搜索域
$IP6_ADDRESS_0第一个 IPv6 地址
$IP6_GATEWAYIPv6 网关
$IP6_NAMESERVERSIPv6 DNS 服务器
# 查看所有环境变量(调试用)
#!/bin/bash
# /etc/NetworkManager/dispatcher.d/99-debug-env
env | sort > /tmp/nm-dispatcher-env-$(date +%s).txt
exit 0

8.5 安全最佳实践

# 1. 脚本必须由 root 拥有
sudo chown root:root /etc/NetworkManager/dispatcher.d/*

# 2. 脚本必须可执行
sudo chmod 755 /etc/NetworkManager/dispatcher.d/*

# 3. 验证输入参数
if [ -z "$INTERFACE" ] || [ -z "$EVENT" ]; then
    exit 0
fi

# 4. 使用完整路径
/usr/bin/curl ...
/usr/sbin/iptables ...

# 5. 处理错误
if ! nmcli connection up "VPN" 2>/dev/null; then
    logger -t nm-dispatcher "Failed to start VPN"
fi

# 6. 避免长时间运行的操作
# 使用后台执行或 no-wait.d 目录
long_running_task &

# 7. 脚本测试
# 手动模拟调用
INTERFACE=eth0 EVENT=up CONNECTION_ID="Wired" \
    /etc/NetworkManager/dispatcher.d/50-my-script eth0 up

8.6 排查 Dispatcher 问题

# 查看 dispatcher 日志
journalctl -u NetworkManager-dispatcher

# 查看脚本执行错误
journalctl -u NetworkManager-dispatcher | grep -i "error\|fail"

# 手动测试脚本
INTERFACE=eth0 EVENT=up \
    /etc/NetworkManager/dispatcher.d/50-logger eth0 up

# 检查脚本权限
ls -la /etc/NetworkManager/dispatcher.d/

# 检查 dispatcher 服务状态
systemctl status NetworkManager-dispatcher

# 启用 dispatcher 服务
sudo systemctl enable NetworkManager-dispatcher
sudo systemctl start NetworkManager-dispatcher

# 常见问题:脚本不执行
# 1. 文件不是可执行文件
# 2. 文件不是 root 所有
# 3. 文件以 .bak/.swp 结尾
# 4. 脚本语法错误
# 5. dispatcher 服务未运行

8.7 本章小结

要点说明
目录/etc/NetworkManager/dispatcher.d/
执行顺序按文件名字母顺序
权限要求root 拥有,755 权限
主要事件up, down, pre-up, pre-down, dhcp4-change, vpn-up
环境变量$INTERFACE, $EVENT, $CONNECTION_ID, $IP4_ADDRESS_0
异步执行放在 no-wait.d/ 子目录
日志journalctl -u NetworkManager-dispatcher

扩展阅读