关键词:usbnet、cdc_ether、接口命名、MAC 地址、U/L 位、Android GKI
一、问题现象
调试一款 USB 网卡(基于 usbnet/cdc_ether 框架的驱动),接入 Android 系统:
- 默认状态:网卡识别为
eth1,一切正常; - 修改 MAC 地址后重新上电:网卡名称变成了
usb0,Android 以太网服务不再正常管理它(系统默认只管理匹配eth\d的接口,usb0会被当成 USB 网络共享设备处理)。
修改前后的 MAC:
| 状态 | MAC 地址 |
|---|---|
| 修改前 | dc:xx:xx:xx:xx:xx |
| 修改后 | ee:xx:xx:xx:xx:xx |
同一台设备、同一个驱动,只改了 MAC,接口命名规则就变了,说明命名和 MAC 地址本身有关。
二、根因:usbnet_probe() 的命名启发式
usbnet 框架的 USB 网卡(cdc_ether、cdc_ncm 及各类厂商衍生驱动),接口名在 drivers/net/usb/usbnet.c 的 usbnet_probe() 中决定,逻辑分两步:
第 1 步:默认命名 usb%d
net = alloc_etherdev(sizeof(*dev));
...
strcpy (net->name, "usb%d");
第 2 步:启发式判断是否改名为 eth%d(旧版内核代码)[1]:
// heuristic: "usb%d" for links we know are two-host,
// else "eth%d" when there's reasonable doubt. userspace
// can rename the link if it knows better.
if ((dev->driver_info->flags & FLAG_ETHER) != 0 &&
((dev->driver_info->flags & FLAG_POINTTOPOINT) == 0 ||
(net->dev_addr [0] & 0x02) == 0))
strcpy (net->name, "eth%d");
cdc_ether 类驱动的 flags 是 FLAG_ETHER | FLAG_POINTTOPOINT,于是判定就落在 MAC 地址第一字节的 0x02 位(U/L 位) 上:
- U/L 位 = 0(全局管理地址,带合法 OUI)→ 改名
eth%d - U/L 位 = 1(本地管理地址)→ 保持
usb%d
新版内核(5.15/6.x GKI,含 2024 年 12 月补丁)逻辑等价,写得更直白[2]:
static bool usbnet_needs_usb_name_format(struct usbnet *dev, struct net_device *net)
{
/* Point to point devices which don't have a real MAC address
* (or report a fake local one) have historically used the usb%d
* naming. Preserve this..
*/
return (dev->driver_info->flags & FLAG_POINTTOPOINT) != 0 &&
(is_zero_ether_addr(net->dev_addr) ||
is_local_ether_addr(net->dev_addr));
}
即:点对点设备,如果驱动没给 MAC(全零)或者给的是本地管理 MAC,保持历史惯例用 usb%d 命名。
三、为什么我的 MAC 踩坑了:U/L 位分析
MAC 第一字节的两个关键标志位:
- bit0(0x01,I/G 位):0 = 单播,1 = 组播(网卡 MAC 必须为 0,否则
register_netdev直接报 invalid MAC); - bit1(0x02,U/L 位):0 = 全局管理地址,1 = 本地管理地址。
逐位分析两个 MAC 的第一字节:
| MAC 第一字节 | 二进制 | bit0 (I/G) | bit1 (U/L) | 判定结果 | 接口名 |
|---|---|---|---|---|---|
dc | 1101 1100 | 0 | 0 | 全局地址 | eth1 ✅ |
ee | 1110 1110 | 0 | 1 | 本地管理地址 | usb0 ❌ |
我随手编的 ee:...,第二字符是 E(二进制 1110),正好把 U/L 位置成了 1,命中了 is_local_ether_addr() 的例外分支。
速记规律:MAC 第一字节的第二个十六进制字符是 2/3/6/7/A/B/E/F → U/L 位为 1(本地地址);是 0/1/4/5/8/9/C/D → 全局地址。手工编 MAC 时务必选后者。
四、验证方法
# 1. 看当前 MAC 第一字节是否落在 x2/x6/xA/xE 上
cat /sys/class/net/usb0/address
# 2. 确认绑定的是哪个驱动(是否走的 cdc_ether/usbnet 框架)
readlink /sys/class/net/usb0/device/driver
# 3. 看 probe 时驱动读到的 MAC 和注册日志
dmesg | grep -i register
把 MAC 改成 ec:3d:4e:00:32:12(只把 ee 换成 ec)后重新上电,接口名恢复 eth1,验证通过。
五、延伸:这段命名逻辑的"回归窗口期"
这段代码近几年被反复改过,不同版本内核行为不完全一致,跨版本调试时要注意:
| 时间 | 提交 | 行为变化 |
|---|---|---|
| ~2022 前 | (原始逻辑) | 默认 usb%d;FLAG_ETHER 且(非点对点 或 U/L=0)→ eth%d |
| 2022 左右 | bab8eb0dd4cb(usbnet: modern method to get random MAC) | 随机 MAC 分配时机改变,命名检查时读到的是清零地址,U/L 检查恒"通过"——带 FLAG_ETHER 的设备不管 MAC 是什么都叫 eth%d(第一次回归) |
| 2024.10 | 8a7d12d674ac(fix name regression,随后回合各 stable) | 条件放宽为"MAC 非零即 eth%d",本地地址也会变成 eth%d,反而把一批依赖 usb0 名字的 LTE 模组/OpenWrt 配置搞挂(第二次回归) |
| 2024.12 | net: usb: usbnet: restore usb%d name exception for local mac addresses(主线及 stable 回合,如 5.4.292) | 恢复"点对点 + 本地/全零 MAC → 保持 usb%d"的旧行为[2-1][3] |
结论:内核从来不保证接口名稳定(Greg KH 原话:"Device names have NEVER been stable"),不要把产品逻辑建立在 eth1/usb0 这类默认名字上。
六、解决方案汇总
- 改 MAC 时保证第一字节为合法全局单播地址:bit0=0 且 bit1=0,例如保留原 OUI 前缀
dc:04:5a:xx:xx:xx,或把ee改成ec——最省事的做法; - 用户态改名:
ip link set usb0 down && ip link set usb0 name eth1;Android 上可用 udev 规则按 MAC 固定名称(如system/core/rootdir/etc/udev/rules.d/70-persistent-net.rules); - 驱动层固定命名:在驱动的
bind()里显式strcpy(net->name, "eth%d"),跳过启发式判断; - 模块参数:usbnet 提供
eth_device_name参数(默认"eth%d")可改改名用的模式串,注意它只影响"改名后叫什么",不影响"是否改名"的判断。
七、经验总结
- USB 网卡在 Linux/Android 下的接口名不是随机的,是 usbnet 框架根据驱动 flags + MAC 地址属性决定的;
- 手工编 MAC 牢记两个约束:第一字节 bit0=0(单播)、bit1=0(全局),即第二个十六进制字符取
0/1/4/5/8/9/C/D; - 遇到"名字变了"类问题,先看
dmesg和驱动绑定关系,再想是不是 MAC 合法性/属性影响了内核的命名/注册路径; - 产品化时接口名要用 MAC/设备路径绑定(udev 规则或驱动固定),别依赖内核默认命名。

275

被折叠的 条评论
为什么被折叠?



