使用 systemd + NetworkManager 保持 USB Wi-Fi 网卡连接到固定 BSSID
目标
系统使用 NetworkManager,并以 iwd 作为 Wi-Fi 后端。
对于一张可热插拔 USB Wi-Fi 网卡,需要保证:
- USB 网卡插入后自动恢复连接;
- NetworkManager 始终管理该接口;
- 固定连接指定 SSID;
- 固定连接指定 BSSID,避免连接到同 SSID 的其他 AP;
- 连接断开后能够自动恢复;
- 不干扰系统中的其他 Wi-Fi 接口;
- 不直接操作 iwd,而由 NetworkManager 统一管理连接状态。
本文配置如下:
Interface: wlxe0e1a9101c1c
MAC: xx:xx:xx:xx:xx:xx
Driver: mt76x2u
Connection: qdzx
UUID: ca45b79a-1ea5-4658-b72d-2f23d0ef4612
SSID: qdzx
BSSID: 4e:77:66:98:4f:49
1. 固定 NetworkManager Connection Profile
首先将已有的 NetworkManager profile 绑定到指定接口、MAC、SSID 和 BSSID:
sudo nmcli connection modify uuid ca45b79a-1ea5-4658-b72d-2f23d0ef4612 \
connection.interface-name wlxe0e1a9101c1c \
connection.autoconnect yes \
connection.autoconnect-retries 0 \
connection.autoconnect-priority 100 \
802-11-wireless.ssid qdzx \
802-11-wireless.bssid 4e:77:66:98:4f:49 \
802-11-wireless.mac-address xx:xx:xx:xx:xx:xx\
802-11-wireless.powersave 2
其中:
connection.autoconnect=yes
允许 NetworkManager 自动激活该 profile。
connection.autoconnect-retries=0
表示自动连接失败后持续重试,不限制尝试次数。
802-11-wireless.bssid=4e:77:66:98:4f:49
将 connection profile 锁定到指定 AP。即使存在其他相同 SSID 的 AP,该 profile 也不会切换过去。
802-11-wireless.mac-address=xx:xx:xx:xx:xx:xx
限制该 profile 只能由指定 USB 网卡使用。
802-11-wireless.powersave=2
关闭该连接的 Wi-Fi power saving,避免 USB 无线网卡因节能机制进入不期望的低功耗状态。
可以使用以下命令检查结果:
nmcli -f \
connection.id,\
connection.uuid,\
connection.interface-name,\
connection.autoconnect,\
connection.autoconnect-retries,\
802-11-wireless.ssid,\
802-11-wireless.bssid,\
802-11-wireless.mac-address,\
802-11-wireless.powersave \
connection show uuid ca45b79a-1ea5-4658-b72d-2f23d0ef4612
2. Wi-Fi Keeper
创建:
/usr/local/sbin/usb-wifi-lock
内容如下:
#!/bin/bash
set -u
IFACE="wlxe0e1a9101c1c"
UUID="ca45b79a-1ea5-4658-b72d-2f23d0ef4612"
SSID="qdzx"
BSSID="4e:77:66:98:4f:49"
MAC="xx:xx:xx:xx:xx:xx"
last_state=""
log()
{
echo "usb-wifi-lock: $*"
}
set_state()
{
if [[ "$last_state" != "$1" ]]; then
log "$2"
last_state="$1"
fi
}
while true; do
#
# USB 网卡不存在。
#
if [[ ! -e "/sys/class/net/$IFACE" ]]; then
set_state "absent" \
"$IFACE absent; waiting for USB adapter"
sleep 2
continue
fi
#
# 确认接口对应目标物理网卡。
#
CURRENT_MAC="$(
cat "/sys/class/net/$IFACE/address" 2>/dev/null || true
)"
if [[ "${CURRENT_MAC,,}" != "${MAC,,}" ]]; then
set_state "mac-error" \
"MAC mismatch: expected $MAC, got ${CURRENT_MAC:-unknown}"
sleep 5
continue
fi
#
# 查询 NetworkManager 对设备的实际状态。
#
NM_STATE_RAW="$(
LC_ALL=C nmcli -g GENERAL.STATE \
device show "$IFACE" 2>/dev/null |
head -n1 || true
)"
NM_STATE="${NM_STATE_RAW%% *}"
#
# sysfs 中已经出现接口,但 NetworkManager 尚未识别。
#
if [[ -z "$NM_STATE" ]]; then
set_state "waiting-nm" \
"$IFACE exists; waiting for NetworkManager"
sleep 2
continue
fi
case "$NM_STATE" in
#
# NM_DEVICE_STATE_UNMANAGED
#
10)
set_state "unmanaged" \
"$IFACE is unmanaged; waiting for NetworkManager"
sleep 2
continue
;;
#
# NM_DEVICE_STATE_UNAVAILABLE
#
# 设备虽然已经由 NetworkManager 管理,
# 但 Wi-Fi backend/driver 初始化尚未结束。
#
# 此阶段不得执行 nmcli connection up。
#
20)
set_state "unavailable" \
"$IFACE is unavailable; waiting for iwd/NM initialization"
sleep 2
continue
;;
#
# NM_DEVICE_STATE_DISCONNECTED
#
# 设备已经完成初始化,可以进行连接。
#
30)
set_state "disconnected" \
"$IFACE ready/disconnected; activating $SSID"
LC_ALL=C nmcli --wait 20 connection up \
uuid "$UUID" \
ifname "$IFACE" || true
sleep 2
continue
;;
#
# NetworkManager 正在执行连接流程。
#
# 40 = prepare
# 50 = config
# 60 = need-auth
# 70 = ip-config
# 80 = ip-check
# 90 = secondaries
#
40|50|60|70|80|90)
set_state "activating-$NM_STATE" \
"$IFACE NetworkManager activation in progress (state=$NM_STATE)"
sleep 2
continue
;;
#
# NM_DEVICE_STATE_ACTIVATED
#
100)
ACTIVE_UUID="$(
nmcli -g GENERAL.CON-UUID \
device show "$IFACE" 2>/dev/null |
head -n1 || true
)"
CURRENT_BSSID="$(
iw dev "$IFACE" link 2>/dev/null |
awk '/^Connected to / {print $3; exit}'
)"
CURRENT_SSID="$(
iw dev "$IFACE" link 2>/dev/null |
sed -n 's/^[[:space:]]*SSID: //p' |
head -n1
)"
IPV4="$(
nmcli -g IP4.ADDRESS \
device show "$IFACE" 2>/dev/null |
head -n1 || true
)"
#
# 必须同时满足:
#
# NetworkManager == activated
# Connection UUID 正确
# SSID 正确
# BSSID 正确
# IPv4 已获取
#
# 才认为链路真正可用。
#
if [[ "$ACTIVE_UUID" == "$UUID" ]] &&
[[ "$CURRENT_SSID" == "$SSID" ]] &&
[[ "${CURRENT_BSSID,,}" == "${BSSID,,}" ]] &&
[[ -n "$IPV4" ]]; then
set_state "connected" \
"OK: $IFACE -> $SSID / $BSSID / $IPV4"
sleep 5
continue
fi
#
# NetworkManager 已经认为接口 activated,
# 但实际连接并非指定 profile/AP。
#
log "activated but target mismatch:"
log " UUID=${ACTIVE_UUID:-none}"
log " SSID=${CURRENT_SSID:-none}"
log " BSSID=${CURRENT_BSSID:-none}"
log " IPv4=${IPV4:-none}"
log "switching to fixed profile"
last_state="switching"
LC_ALL=C nmcli --wait 20 connection up \
uuid "$UUID" \
ifname "$IFACE" || true
sleep 3
continue
;;
#
# NM_DEVICE_STATE_DEACTIVATING
#
110)
set_state "deactivating" \
"$IFACE is deactivating; waiting"
sleep 2
continue
;;
#
# NM_DEVICE_STATE_FAILED
#
# NetworkManager 通常会自行转换回 disconnected,
# 因此这里只等待,不立即重复操作。
#
120)
set_state "failed" \
"$IFACE activation failed; waiting for NM recovery"
sleep 3
continue
;;
*)
set_state "unknown-$NM_STATE" \
"$IFACE unknown NetworkManager state: $NM_STATE_RAW"
sleep 3
continue
;;
esac
done
赋予执行权限:
sudo chmod 755 /usr/local/sbin/usb-wifi-lock
3. systemd Service
创建:
/etc/systemd/system/usb-wifi-lock.service
内容:
[Unit]
Description=Keep MediaTek USB Wi-Fi connected to fixed AP
Requires=NetworkManager.service
After=NetworkManager.service
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStart=/usr/local/sbin/usb-wifi-lock
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
加载并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now usb-wifi-lock.service
检查状态:
systemctl status usb-wifi-lock.service
正常状态应为:
Active: active (running)
查看运行日志:
journalctl -fu usb-wifi-lock.service
连接完成后会出现:
usb-wifi-lock: OK: wlxe0e1a9101c1c -> qdzx / 4e:77:66:98:4f:49 / 192.168.0.x/24
4. 不使用 udev 启动 Service
该方案不需要通过 udev 在 USB 插入事件上启动 systemd unit。
如果存在此前创建的规则,可以删除:
sudo rm -f /etc/udev/rules.d/80-usb-wifi-lock.rules
sudo rm -f /etc/systemd/system/usb-wifi-lock@.service
sudo udevadm control --reload-rules
sudo systemctl daemon-reload
usb-wifi-lock.service 本身是常驻服务。
USB 网卡不存在时,它只进入等待状态:
wlxe0e1a9101c1c absent; waiting for USB adapter
USB 网卡重新插入后,Linux、udev、iwd 和 NetworkManager 完成设备枚举,keeper 再根据 NetworkManager 的设备状态决定是否执行连接。
这样可以避免两个问题:
- USB 无线网卡刚创建时,其临时接口名可能是
wlanN,随后才按照 persistent naming policy 重命名为wlxe0e1a9101c1c; - 接口已经出现在 sysfs 中,并不意味着 NetworkManager/iwd 已经完成初始化。
因此,不应把“网卡出现在 /sys/class/net”等同于“网卡可以立即连接”。
5. NetworkManager 状态机
这个配置的关键是严格遵守 NetworkManager 的设备状态,而不是仅检查 iw link。
简化后的状态转换为:
USB absent
|
v
interface created
|
v
UNMANAGED (10)
|
v
UNAVAILABLE (20)
|
v
DISCONNECTED (30)
|
v
PREPARE (40)
|
v
CONFIG (50)
|
v
IP-CONFIG (70)
|
v
IP-CHECK (80)
|
v
SECONDARIES (90)
|
v
ACTIVATED (100)
keeper 的策略为:
10 unmanaged
-> wait
20 unavailable
-> wait
30 disconnected
-> nmcli connection up
40-90
-> NetworkManager 正在连接,wait
100 activated
-> 验证 UUID / SSID / BSSID / IPv4
110 deactivating
-> wait
120 failed
-> wait for NM recovery
尤其需要注意 UNAVAILABLE 和 DISCONNECTED 的区别。
UNAVAILABLE 表示 NetworkManager 已经看到设备,但底层 Wi-Fi backend、驱动或者设备初始化尚未达到可连接状态。此时主动执行 nmcli connection up 容易与 iwd/NetworkManager 初始化产生竞态。
DISCONNECTED 才表示设备已经完成初始化,并且当前没有连接,可以安全请求 NetworkManager 激活 profile。
6. 不能只检查 iw link
以下状态:
Connected to 4e:77:66:98:4f:49
SSID: qdzx
IEEE 802.11 层已经完成 association。
它不能证明:
- NetworkManager connection 已成功激活;
- DHCP 已完成;
- IPv4 地址已经获得;
- 路由已经建立;
- DNS 已安装;
- 当前 connection UUID 正确。
因此 keeper 只有在以下条件全部成立时才报告 OK:
NetworkManager state == 100
Connection UUID == target UUID
SSID == qdzx
BSSID == 4e:77:66:98:4f:49
IPv4 address != empty
也就是说,判断标准不是“无线已经关联”,而是“NetworkManager 管理下的网络连接已经完整建立”。
7. 验证
NetworkManager
nmcli -f \
GENERAL.STATE,\
GENERAL.CONNECTION,\
GENERAL.CON-UUID,\
IP4.ADDRESS \
device show wlxe0e1a9101c1c
正常应满足:
GENERAL.STATE: 100
GENERAL.CONNECTION: qdzx
GENERAL.CON-UUID: ca45b79a-1ea5-4658-b72d-2f23d0ef4612
IP4.ADDRESS: 192.168.0.x/24
无线链路
iw dev wlxe0e1a9101c1c link
应包含:
Connected to 4e:77:66:98:4f:49
SSID: qdzx
IPv4 路由
ip route
应存在类似:
192.168.0.0/24 dev wlxe0e1a9101c1c ...
如果 AP 提供默认网关,还会存在:
default via 192.168.0.1 dev wlxe0e1a9101c1c ...
8. 多网卡环境中的默认路由
如果机器同时连接多个 Wi-Fi,NetworkManager 会根据 route metric 选择默认路由。
例如:
default via 192.168.1.1 dev wlan0 metric 600
default via 192.168.0.1 dev wlxe0e1a9101c1c metric 601
表示:
wlan0是首选默认出口;- USB Wi-Fi 仍然正常在线;
wlxe0e1a9101c1c可以正常访问其192.168.0.0/24网络;- 默认流量不会优先走 USB 网卡。
因此,“保持 USB Wi-Fi 在线”和“让 USB Wi-Fi 成为默认出口”是两个不同的问题。
本文配置只解决前者,不修改系统默认路由优先级。
结论
在 NetworkManager + iwd 环境中,维护 USB Wi-Fi 热插拔连接时,合理的职责划分是:
kernel / driver
-> 创建设备
udev
-> 设备命名
iwd
-> Wi-Fi backend
NetworkManager
-> association / authentication / DHCP / route / DNS
usb-wifi-lock
-> 观察 NetworkManager 状态并确保目标 profile 最终处于 activated
守护程序只在 NetworkManager 明确处于可操作状态时发起连接,其余阶段保持等待。
这样既能处理 USB 网卡拔插,也能避免 systemd 脚本、NetworkManager 和 iwd 同时操作无线设备所产生的竞态。