一、前言
最近在做 android 多用户功能适配时,发现一个问题:
在主用户下可以正常获取有线网(ethernet)的 mac 地址,但切换到子用户后,通过 `networkinterface.gethardwareaddress()` 获取到的 mac 地址为 null。 获取mac地址,需要系统应用或者系统权限应用;普通应用是获取不到的; 目前问题是系统权限应用,在子用户下也是无法获取到有线网节点eth0的mac地址。
这个问题影响了子用户下的网络信息展示、设备标识、某些应用激活等功能。
经过分析,问题出在 bionic 库的 ifaddrs.cpp 中,对多用户场景的 uid 判断代码。
本文记录完整的分析过程和解决方案。
android13 之后好像就有这个问题,本文的代码具体代码展示是android16的。
二、问题现象
在 android 设备上创建子用户(userid=10)后,子用户中的系统应用调用以下代码获取 mac 地址:
networkinterface ni = networkinterface.getbyname("eth0");
byte[] mac = ni.gethardwareaddress(); // 子用户下返回 null| 场景 | 结果 |
|---|---|
| 主用户(userid=0)系统应用 | ✅ 正常返回 mac 地址 |
| 主用户(userid=0)普通应用 | ❌ 返回 null(正常,安全限制) |
| 子用户(userid=10)系统应用 | ❌ 返回 null(异常,本文要解决的问题) |
| 子用户(userid=10)普通应用 | ❌ 返回 null(正常,安全限制) |
三、原因分析
1、networkinterface.gethardwareaddress() 调用链路
从应用层到内核层的完整调用链路如下:
应用层: networkinterface.gethardwareaddress() ↓ java 层: libcore/ojluni/src/main/java/java/net/networkinterface.java ↓ 返回 ni.hardwareaddr 字段 jni 层: libcore/luni/src/main/native/libcore_io_linux.cpp ↓ 调用 getifaddrs() bionic 层: bionic/libc/bionic/ifaddrs.cpp ← 【问题所在】 ↓ 通过 netlink 发送 rtm_getlink 请求 内核层: netlink socket → 返回 af_packet 类型地址(包含 mac)
gethardwareaddress() 的 java 层代码本身没有做 uid 权限判断,它只是返回 hardwareaddr 字段:
// libcore/ojluni/src/main/java/java/net/networkinterface.java
public byte[] gethardwareaddress() throws socketexception {
networkinterface ni = getbyname(name);
if (ni == null) {
throw new socketexception("networkinterface doesn't exist anymore");
}
if (ni.hardwareaddr == null && !"lo".equals(name)
&& !compatibility.ischangeenabled(return_null_hardware_address)) {
return default_mac_address.clone(); // 02:00:00:00:00:00
}
return ni.hardwareaddr; // 关键:这个值来自 native 层
}hardwareaddr 的值是在 native 层通过 getifaddrs() 获取并填充的。如果 getifaddrs() 没有返回 af_packet 类型的地址信息,hardwareaddr 就是 null。
2、getifaddrs() 中的 uid 判断逻辑
问题的根源在 bionic/libc/bionic/ifaddrs.cpp 中的 getifaddrs() 函数:
// bionic/libc/bionic/ifaddrs.cpp
int getifaddrs(ifaddrs** out) {
*out = nullptr;
netlinkconnection nc;
// 关键判断:只有 uid < 10000 的进程才发送 rtm_getlink 请求
bool getlink_success = false;
if (getuid() < first_application_uid) { // first_application_uid = 10000
getlink_success =
nc.sendrequest(rtm_getlink) && nc.readresponses(__getifaddrs_callback, out);
}
bool getaddr_success =
nc.sendrequest(rtm_getaddr) && nc.readresponses(__getifaddrs_callback, out);
// ...
}这里的逻辑是:
rtm_getlink:获取网络接口的链路层信息(包含 mac 地址),只对 uid < 10000 的进程发送rtm_getaddr:获取网络接口的 ip 地址信息,所有进程都可以发送
注释也说明了原因:selinux policy only allows rtm_getlink messages to be sent by system apps。
3、android 多用户 uid 计算规则
android 多用户下,uid 的计算公式为:
uid = userid * 100000 + appid
其中:
userid:用户 id,主用户为 0,子用户从 10 开始appid:应用 id,系统进程 < 10000,普通应用 ≥ 10000100000:用户偏移量(aid_user_offset)
各场景下的 uid 值:
| 场景 | userid | appid | getuid() 返回值 |
|---|---|---|---|
| 主用户 system_server | 0 | 1000 | 1000 |
| 主用户 shell | 0 | 2000 | 2000 |
| 主用户普通应用 | 0 | 10068 | 10068 |
| 子用户 system | 10 | 1000 | 1001000 |
| 子用户 shell | 10 | 2000 | 1002000 |
| 子用户普通应用 | 10 | 10068 | 1010068 |
4、问题根因定位
原代码的判断条件:
if (getuid() < first_application_uid) // 即 getuid() < 10000
- 主用户 system(uid=1000):1000 < 10000 ✅ → 发送 rtm_getlink → 获取到 mac
- 子用户 system(uid=1001000):1001000 < 10000 ❌ → 不发送 rtm_getlink → mac 为 null
问题根因:ifaddrs.cpp 使用完整的 uid 做判断,没有考虑多用户场景。子用户的系统进程 uid 远大于 10000,被错误地当作普通应用处理,导致 rtm_getlink 请求不会发送,mac 地址无法获取。
四、解决方案
1、修改networkinterface.gethardwareaddress()的返回信息 ?
这个是肯定不行的。
因为networkinterface 的代码位置在 libcore/ojluni/src/main/java/java/net/networkinterface.java。
这个是java的类包,无法导入android的类,获取不到android的信息。
并且这个类库不是随系统编译的,试过代码中加入java打印,编译验证是没有的显示的,
估计要用特殊指令单独编译这块代码,才会更新系统相关依赖包。
2、修改 bionic 层 ifaddrs.cpp
文件路径:bionic/libc/bionic/ifaddrs.cpp
将 getuid() 的判断改为提取 appid 后再比较:
修改前:
int getifaddrs(ifaddrs** out) {
*out = nullptr;
netlinkconnection nc;
bool getlink_success = false;
if (getuid() < first_application_uid) {
getlink_success =
nc.sendrequest(rtm_getlink) && nc.readresponses(__getifaddrs_callback, out);
}
// ...
}修改后:
int getifaddrs(ifaddrs** out) {
*out = nullptr;
netlinkconnection nc;
// 修改:使用 appid 判断,支持多用户场景
// android 多用户下 uid = userid * 100000 + appid
// 子用户的系统应用 appid 仍然 < first_application_uid
bool getlink_success = false;
uid_t appid = getuid() % 100000;
if (appid < first_application_uid) {
getlink_success =
nc.sendrequest(rtm_getlink) && nc.readresponses(__getifaddrs_callback, out);
}
// ...
}
核心改动就一行:把 getuid() 换成 getuid() % 100000。
100000 是 android 中用户偏移量(aid_user_offset)的固定值,从未改变过。通过取模运算提取出 appid,就能正确识别所有用户下的系统进程。
修改前后效果对比
| 场景 | uid | 原逻辑 getuid() < 10000 | 修改后 getuid() % 100000 < 10000 |
|---|---|---|---|
| 主用户 system | 1000 | ✅ 发送 rtm_getlink | ✅ 发送 |
| 主用户普通应用 | 10068 | ❌ 不发送 | ❌ 不发送 |
| 子用户 system | 1001000 | ❌ 不发送(问题) | ✅ 发送(appid=1000) |
| 子用户普通应用 | 1010068 | ❌ 不发送 | ❌ 不发送(appid=10068) |
修改后,子用户的系统应用可以正常获取 mac 地址。
普通应用可以吗?测试了一下还是不行!
就算强制进入获取 getlink_success 的逻辑,普通应用还是会返回0;
估计还要适配系统其他权限问题,比较麻烦所以这个解决方案对普通应用不行。
并且这种修改对 edla 认证可能会有影响,不建议使用。
3、应用层替代方案
如果不方便修改 bionic 层,也可以在应用层(系统应用)通过读取 sysfs 文件来获取 mac 地址:
/**
* 通过 sysfs 获取有线网 mac 地址
* 不依赖 networkinterface.gethardwareaddress(),不受 bionic 层 uid 限制
*/
public static string getethernetmac() {
try {
return new string(files.readallbytes(
paths.get("/sys/class/net/eth0/address"))).trim();
} catch (ioexception e) {
log.e(tag, "failed to read mac address from sysfs", e);
return null;
}
}但这种方式需要 selinux 策略允许应用读取 sysfs_net:
# device/<vendor>/<device>/sepolicy/private/your_app.te
allow your_app_domain sysfs_net:file { read open getattr };
allow your_app_domain sysfs_net:dir { search };userdebug版本确实可以通过cat sys/class/net/eth0/address 获取有线网mac地址
但是配置策略比较麻烦,有需要的可以自行测试。
wifi的mac地址同理:sys/class/net/wlan0/address
这个方案也是只能适配系统应用,并且要适配权限,比较麻烦,不建议修改。
4、主线程的服务/应用获取并记录mac地址
可以在主用户进程中获取 mac 地址写入个系统属性,子用户直接读属性:
string mac= getmacfromnetworkinterface();//主用户可以拿到
systemproperties.set("persist.debug.eth.mac",mac);
// 子用户应用中,非系统应用需要反射获取
string mac=systemproperties.get("persist.debug.etho.mac","");可以在系统wifi服务或者自定义的系统应用服务启动时获取mac,切换用户过程,这些服务是一直在的。
这方案修改代码最少,又不影响系统其他功能。
如果不行用prop属性,是否可以用settings属性?
一般的settings.system、secure都时候会重置的,global属性不会重置,这个获取和设置更加简单一点。
//系统服务 settings.global.putstring(getcontentresolver(), "mac_address", macstringxxx); //普通应用,直接调用,不用反射 string devicemac = settings.global.getstring(getcontentresolver(),"mac_address");
这个是目前最简单的实现方式,settings.global 保存和获取mac地址信息;
普通应用是没有settings设置权限的,只有读取权限。
5、让普通用户也可以设置和获取settings.global 属性
这个需要修改framework的代码,也是不太建议的。
系统修改下面两个地方其中一个:
defaultpermissiongrantpolicy.java → 给指定包名自动授权 grantpermissionstopackage permissionmanagerservice.java → 全局放行权限(所有应用都能用) private boolean checkpermission(string perm, int pid, int uid, boolean debug)
普通应用定义权限:
android.permission.write_secure_settings android.permission.write_global_settings
之前普通应用就可以通过settings.global.putstring 和 settings.global.getstring 设置获取属性;
但是这个是破坏android安全性的,edla认证是会有报错的。
五、其他
1、小结
android 切换用户后无法获取 mac 地址的根本原因是 bionic/libc/bionic/ifaddrs.cpp 中的 getifaddrs() 函数使用完整的 uid 做权限判断,没有考虑多用户场景。
子用户的系统进程 uid(如 1001000)远大于 first_application_uid(10000),被错误地当作普通应用,导致 rtm_getlink 请求不会发送,networkinterface.gethardwareaddress() 返回 null。
解决方案是将 getuid() 改为 getuid() % 100000,提取 appid 后再做判断,这样所有用户下的系统进程都能正确获取 mac 地址,同时不影响普通应用的安全限制。
修改方式和修改涉及的文件:
- bionic 层:
bionic/libc/bionic/ifaddrs.cpp(核心修改,改一行代码) - selinux 策略:如有需要,确保子用户的系统应用有
netlink_route_socket权限 - 应用层替代:可通过读取
/sys/class/net/eth0/address绕过,需配置 selinux 策略 - 系统服务:系统服务启动时获取mac地址保存到prop属性或者settings.global ,子用户可以读取。
- 目前验证测试,通过系统服务设置settings.global 的mac属性,普通用户获取 settings.global 是最简单的。
2、获取ip和mac地址几种方式
1、ifconfig 2、获取wifi 的ip 可以通过 wifimanager 获取当前连接的wifi信息,获取到ip地址; 3、获取有线网、wifi的ip、mac 通过获取 connectivitymanager获取连接的网络 network-->linkproperties获取到ip地址。 4、获取有线网、wifi、热点、p2p的ip、mac 通过获取所有节点信息:networkinterface.getnetworkinterfaces() 获取对应的ip地址和mac地址。
3、普通应用反射获取prop的封装方法
封装类和方法,可以直接使用:
import android.text.textutils;
import android.util.log;
import java.lang.reflect.method;
public class systempropertiesutil {
private static final string tag = "systempropertiesutil";
private static method sgetmethod;
private static method ssetmethod;
static {
try {
class<?> clazz = class.forname("android.os.systemproperties");
sgetmethod = clazz.getmethod("get", string.class, string.class);
ssetmethod = clazz.getmethod("set", string.class, string.class);
} catch (exception e) {
log.e(tag, "failed to init systemproperties methods", e);
}
}
/**
* 获取系统属性值
*
* @param key 属性名,如 "ro.build.display.id"
* @param defaultvalue 默认值,属性不存在或无权限时返回
* @return 属性值
*/
public static string get(string key, string defaultvalue) {
try {
if (sgetmethod != null) {
string value = (string) sgetmethod.invoke(null, key, defaultvalue);
return value;
}
} catch (exception e) {
log.e(tag, "failed to get property: " + key, e);
}
return defaultvalue;
}
/**
* 获取系统属性值,默认返回空串
*/
public static string get(string key) {
return get(key, "");
}
/**
* 获取 boolean 类型属性
*/
public static boolean getboolean(string key, boolean defaultvalue) {
string value = get(key, "");
if (textutils.isempty(value)) return defaultvalue;
return "true".equalsignorecase(value) || "1".equals(value);
}
/**
* 获取 int 类型属性
*/
public static int getint(string key, int defaultvalue) {
string value = get(key, "");
try {
return textutils.isempty(value) ? defaultvalue : integer.parseint(value);
} catch (numberformatexception e) {
return defaultvalue;
}
}
/**
* 设置系统属性(普通应用通常没有权限,仅系统应用可用)
*/
public static void set(string key, string value) {
try {
if (ssetmethod != null) {
ssetmethod.invoke(null, key, value);
}
} catch (exception e) {
log.e(tag, "failed to set property: " + key, e);
}
}
}如果系统应用通过prop设置mac地址属性,普通用户就用上面这个封装方法获取prop的属性。
以上就是android切换用户后无法获取mac地址的解决方法的详细内容,更多关于android无法获取mac地址的资料请关注代码网其它相关文章!
发表评论