Android切換用戶后無(wú)法獲取MAC地址的解決方法
一、前言
最近在做 Android 多用戶功能適配時(shí),發(fā)現(xiàn)一個(gè)問(wèn)題:
在主用戶下可以正常獲取有線網(wǎng)(Ethernet)的 MAC 地址,但切換到子用戶后,通過(guò) `NetworkInterface.getHardwareAddress()` 獲取到的 MAC 地址為 null。 獲取mac地址,需要系統(tǒng)應(yīng)用或者系統(tǒng)權(quán)限應(yīng)用;普通應(yīng)用是獲取不到的; 目前問(wèn)題是系統(tǒng)權(quán)限應(yīng)用,在子用戶下也是無(wú)法獲取到有線網(wǎng)節(jié)點(diǎn)eth0的mac地址。
這個(gè)問(wèn)題影響了子用戶下的網(wǎng)絡(luò)信息展示、設(shè)備標(biāo)識(shí)、某些應(yīng)用激活等功能。
經(jīng)過(guò)分析,問(wèn)題出在 bionic 庫(kù)的 ifaddrs.cpp 中,對(duì)多用戶場(chǎng)景的 UID 判斷代碼。
本文記錄完整的分析過(guò)程和解決方案。
Android13 之后好像就有這個(gè)問(wèn)題,本文的代碼具體代碼展示是Android16的。
二、問(wèn)題現(xiàn)象
在 Android 設(shè)備上創(chuàng)建子用戶(userId=10)后,子用戶中的系統(tǒng)應(yīng)用調(diào)用以下代碼獲取 MAC 地址:
NetworkInterface ni = NetworkInterface.getByName("eth0");
byte[] mac = ni.getHardwareAddress(); // 子用戶下返回 null| 場(chǎng)景 | 結(jié)果 |
|---|---|
| 主用戶(userId=0)系統(tǒng)應(yīng)用 | ? 正常返回 MAC 地址 |
| 主用戶(userId=0)普通應(yīng)用 | ? 返回 null(正常,安全限制) |
| 子用戶(userId=10)系統(tǒng)應(yīng)用 | ? 返回 null(異常,本文要解決的問(wèn)題) |
| 子用戶(userId=10)普通應(yīng)用 | ? 返回 null(正常,安全限制) |
三、原因分析
1、NetworkInterface.getHardwareAddress() 調(diào)用鏈路
從應(yīng)用層到內(nèi)核層的完整調(diào)用鏈路如下:
應(yīng)用層: NetworkInterface.getHardwareAddress() ↓ Java 層: libcore/ojluni/src/main/java/java/net/NetworkInterface.java ↓ 返回 ni.hardwareAddr 字段 JNI 層: libcore/luni/src/main/native/libcore_io_Linux.cpp ↓ 調(diào)用 getifaddrs() Bionic 層: bionic/libc/bionic/ifaddrs.cpp ← 【問(wèn)題所在】 ↓ 通過(guò) netlink 發(fā)送 RTM_GETLINK 請(qǐng)求 內(nèi)核層: netlink socket → 返回 AF_PACKET 類型地址(包含 MAC)
getHardwareAddress() 的 Java 層代碼本身沒(méi)有做 UID 權(quán)限判斷,它只是返回 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; // 關(guān)鍵:這個(gè)值來(lái)自 native 層
}hardwareAddr 的值是在 native 層通過(guò) getifaddrs() 獲取并填充的。如果 getifaddrs() 沒(méi)有返回 AF_PACKET 類型的地址信息,hardwareAddr 就是 null。
2、getifaddrs() 中的 UID 判斷邏輯
問(wèn)題的根源在 bionic/libc/bionic/ifaddrs.cpp 中的 getifaddrs() 函數(shù):
// bionic/libc/bionic/ifaddrs.cpp
int getifaddrs(ifaddrs** out) {
*out = nullptr;
NetlinkConnection nc;
// 關(guān)鍵判斷:只有 uid < 10000 的進(jìn)程才發(fā)送 RTM_GETLINK 請(qǐng)求
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:獲取網(wǎng)絡(luò)接口的鏈路層信息(包含 MAC 地址),只對(duì) uid < 10000 的進(jìn)程發(fā)送RTM_GETADDR:獲取網(wǎng)絡(luò)接口的 IP 地址信息,所有進(jìn)程都可以發(fā)送
注釋也說(shuō)明了原因:SELinux policy only allows RTM_GETLINK messages to be sent by system apps。
3、Android 多用戶 UID 計(jì)算規(guī)則
Android 多用戶下,UID 的計(jì)算公式為:
uid = userId * 100000 + appId
其中:
userId:用戶 ID,主用戶為 0,子用戶從 10 開始appId:應(yīng)用 ID,系統(tǒng)進(jìn)程 < 10000,普通應(yīng)用 ≥ 10000100000:用戶偏移量(AID_USER_OFFSET)
各場(chǎng)景下的 UID 值:
| 場(chǎng)景 | userId | appId | getuid() 返回值 |
|---|---|---|---|
| 主用戶 system_server | 0 | 1000 | 1000 |
| 主用戶 shell | 0 | 2000 | 2000 |
| 主用戶普通應(yīng)用 | 0 | 10068 | 10068 |
| 子用戶 system | 10 | 1000 | 1001000 |
| 子用戶 shell | 10 | 2000 | 1002000 |
| 子用戶普通應(yīng)用 | 10 | 10068 | 1010068 |
4、問(wèn)題根因定位
原代碼的判斷條件:
if (getuid() < FIRST_APPLICATION_UID) // 即 getuid() < 10000
- 主用戶 system(uid=1000):1000 < 10000 ? → 發(fā)送 RTM_GETLINK → 獲取到 MAC
- 子用戶 system(uid=1001000):1001000 < 10000 ? → 不發(fā)送 RTM_GETLINK → MAC 為 null
問(wèn)題根因:ifaddrs.cpp 使用完整的 uid 做判斷,沒(méi)有考慮多用戶場(chǎng)景。子用戶的系統(tǒng)進(jìn)程 uid 遠(yuǎn)大于 10000,被錯(cuò)誤地當(dāng)作普通應(yīng)用處理,導(dǎo)致 RTM_GETLINK 請(qǐng)求不會(huì)發(fā)送,MAC 地址無(wú)法獲取。
四、解決方案
1、修改NetworkInterface.getHardwareAddress()的返回信息 ?
這個(gè)是肯定不行的。
因?yàn)镹etworkInterface 的代碼位置在 libcore/ojluni/src/main/java/java/net/NetworkInterface.java。
這個(gè)是Java的類包,無(wú)法導(dǎo)入Android的類,獲取不到Android的信息。
并且這個(gè)類庫(kù)不是隨系統(tǒng)編譯的,試過(guò)代碼中加入Java打印,編譯驗(yàn)證是沒(méi)有的顯示的,
估計(jì)要用特殊指令單獨(dú)編譯這塊代碼,才會(huì)更新系統(tǒng)相關(guān)依賴包。
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 判斷,支持多用戶場(chǎng)景
// Android 多用戶下 uid = userId * 100000 + appId
// 子用戶的系統(tǒng)應(yīng)用 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);
}
// ...
}
核心改動(dòng)就一行:把 getuid() 換成 getuid() % 100000。
100000 是 Android 中用戶偏移量(AID_USER_OFFSET)的固定值,從未改變過(guò)。通過(guò)取模運(yùn)算提取出 appId,就能正確識(shí)別所有用戶下的系統(tǒng)進(jìn)程。
修改前后效果對(duì)比
| 場(chǎng)景 | uid | 原邏輯 getuid() < 10000 | 修改后 getuid() % 100000 < 10000 |
|---|---|---|---|
| 主用戶 system | 1000 | ? 發(fā)送 RTM_GETLINK | ? 發(fā)送 |
| 主用戶普通應(yīng)用 | 10068 | ? 不發(fā)送 | ? 不發(fā)送 |
| 子用戶 system | 1001000 | ? 不發(fā)送(問(wèn)題) | ? 發(fā)送(appId=1000) |
| 子用戶普通應(yīng)用 | 1010068 | ? 不發(fā)送 | ? 不發(fā)送(appId=10068) |
修改后,子用戶的系統(tǒng)應(yīng)用可以正常獲取 MAC 地址。
普通應(yīng)用可以嗎?測(cè)試了一下還是不行!
就算強(qiáng)制進(jìn)入獲取 getlink_success 的邏輯,普通應(yīng)用還是會(huì)返回0;
估計(jì)還要適配系統(tǒng)其他權(quán)限問(wèn)題,比較麻煩所以這個(gè)解決方案對(duì)普通應(yīng)用不行。
并且這種修改對(duì) EDLA 認(rèn)證可能會(huì)有影響,不建議使用。
3、應(yīng)用層替代方案
如果不方便修改 bionic 層,也可以在應(yīng)用層(系統(tǒng)應(yīng)用)通過(guò)讀取 sysfs 文件來(lái)獲取 MAC 地址:
/**
* 通過(guò) sysfs 獲取有線網(wǎng) 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 策略允許應(yīng)用讀取 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版本確實(shí)可以通過(guò)cat sys/class/net/eth0/address 獲取有線網(wǎng)mac地址
但是配置策略比較麻煩,有需要的可以自行測(cè)試。
wifi的mac地址同理:sys/class/net/wlan0/address
這個(gè)方案也是只能適配系統(tǒng)應(yīng)用,并且要適配權(quán)限,比較麻煩,不建議修改。
4、主線程的服務(wù)/應(yīng)用獲取并記錄mac地址
可以在主用戶進(jìn)程中獲取 MAC 地址寫入個(gè)系統(tǒng)屬性,子用戶直接讀屬性:
String mac= getMacFromNetworkInterface();//主用戶可以拿到
SystemProperties.set("persist.debug.eth.mac",mac);
// 子用戶應(yīng)用中,非系統(tǒng)應(yīng)用需要反射獲取
String mac=SystemProperties.get("persist.debug.etho.mac","");可以在系統(tǒng)wifi服務(wù)或者自定義的系統(tǒng)應(yīng)用服務(wù)啟動(dòng)時(shí)獲取mac,切換用戶過(guò)程,這些服務(wù)是一直在的。
這方案修改代碼最少,又不影響系統(tǒng)其他功能。
如果不行用prop屬性,是否可以用Settings屬性?
一般的Settings.System、Secure都時(shí)候會(huì)重置的,Global屬性不會(huì)重置,這個(gè)獲取和設(shè)置更加簡(jiǎn)單一點(diǎn)。
//系統(tǒng)服務(wù) Settings.Global.putString(getContentResolver(), "mac_address", macStringXXX); //普通應(yīng)用,直接調(diào)用,不用反射 String deviceMac = Settings.Global.getString(getContentResolver(),"mac_address");
這個(gè)是目前最簡(jiǎn)單的實(shí)現(xiàn)方式,Settings.Global 保存和獲取mac地址信息;
普通應(yīng)用是沒(méi)有Settings設(shè)置權(quán)限的,只有讀取權(quán)限。
5、讓普通用戶也可以設(shè)置和獲取Settings.Global 屬性
這個(gè)需要修改framework的代碼,也是不太建議的。
系統(tǒng)修改下面兩個(gè)地方其中一個(gè):
DefaultPermissionGrantPolicy.java → 給指定包名自動(dòng)授權(quán) grantPermissionsToPackage PermissionManagerService.java → 全局放行權(quán)限(所有應(yīng)用都能用) private boolean checkPermission(String perm, int pid, int uid, boolean debug)
普通應(yīng)用定義權(quán)限:
android.permission.WRITE_SECURE_SETTINGS android.permission.WRITE_GLOBAL_SETTINGS
之前普通應(yīng)用就可以通過(guò)Settings.Global.putString 和 Settings.Global.getString 設(shè)置獲取屬性;
但是這個(gè)是破壞Android安全性的,EDLA認(rèn)證是會(huì)有報(bào)錯(cuò)的。
五、其他
1、小結(jié)
Android 切換用戶后無(wú)法獲取 MAC 地址的根本原因是 bionic/libc/bionic/ifaddrs.cpp 中的 getifaddrs() 函數(shù)使用完整的 uid 做權(quán)限判斷,沒(méi)有考慮多用戶場(chǎng)景。
子用戶的系統(tǒng)進(jìn)程 uid(如 1001000)遠(yuǎn)大于 FIRST_APPLICATION_UID(10000),被錯(cuò)誤地當(dāng)作普通應(yīng)用,導(dǎo)致 RTM_GETLINK 請(qǐng)求不會(huì)發(fā)送,NetworkInterface.getHardwareAddress() 返回 null。
解決方案是將 getuid() 改為 getuid() % 100000,提取 appId 后再做判斷,這樣所有用戶下的系統(tǒng)進(jìn)程都能正確獲取 MAC 地址,同時(shí)不影響普通應(yīng)用的安全限制。
修改方式和修改涉及的文件:
- bionic 層:
bionic/libc/bionic/ifaddrs.cpp(核心修改,改一行代碼) - SELinux 策略:如有需要,確保子用戶的系統(tǒng)應(yīng)用有
netlink_route_socket權(quán)限 - 應(yīng)用層替代:可通過(guò)讀取
/sys/class/net/eth0/address繞過(guò),需配置 SELinux 策略 - 系統(tǒng)服務(wù):系統(tǒng)服務(wù)啟動(dòng)時(shí)獲取mac地址保存到prop屬性或者Settings.Global ,子用戶可以讀取。
- 目前驗(yàn)證測(cè)試,通過(guò)系統(tǒng)服務(wù)設(shè)置Settings.Global 的mac屬性,普通用戶獲取 Settings.Global 是最簡(jiǎn)單的。
2、獲取ip和mac地址幾種方式
1、ifconfig 2、獲取wifi 的ip 可以通過(guò) WifiManager 獲取當(dāng)前連接的wifi信息,獲取到ip地址; 3、獲取有線網(wǎng)、wifi的ip、mac 通過(guò)獲取 ConnectivityManager獲取連接的網(wǎng)絡(luò) Network-->LinkProperties獲取到ip地址。 4、獲取有線網(wǎng)、wifi、熱點(diǎn)、p2p的ip、mac 通過(guò)獲取所有節(jié)點(diǎn)信息:NetworkInterface.getNetworkInterfaces() 獲取對(duì)應(yīng)的ip地址和MAC地址。
3、普通應(yīng)用反射獲取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);
}
}
/**
* 獲取系統(tǒng)屬性值
*
* @param key 屬性名,如 "ro.build.display.id"
* @param defaultValue 默認(rèn)值,屬性不存在或無(wú)權(quán)限時(shí)返回
* @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;
}
/**
* 獲取系統(tǒng)屬性值,默認(rèn)返回空串
*/
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;
}
}
/**
* 設(shè)置系統(tǒng)屬性(普通應(yīng)用通常沒(méi)有權(quán)限,僅系統(tǒng)應(yīng)用可用)
*/
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);
}
}
}如果系統(tǒng)應(yīng)用通過(guò)prop設(shè)置mac地址屬性,普通用戶就用上面這個(gè)封裝方法獲取prop的屬性。
以上就是Android切換用戶后無(wú)法獲取MAC地址的解決方法的詳細(xì)內(nèi)容,更多關(guān)于Android無(wú)法獲取MAC地址的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android簡(jiǎn)易音樂(lè)播放器實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android簡(jiǎn)易音樂(lè)播放器的實(shí)現(xiàn)代碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02
Flutter開發(fā)技巧ListView去除水波紋方法示例
這篇文章主要為大家介紹了Flutter開發(fā)技巧ListView去除水波紋方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
Flutter StaggeredGridView實(shí)現(xiàn)瀑布流效果
這篇文章主要為大家詳細(xì)介紹了Flutter StaggeredGridView實(shí)現(xiàn)瀑布流效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Android CountDownTimer實(shí)現(xiàn)定時(shí)器和倒計(jì)時(shí)效果
這篇文章主要為大家詳細(xì)介紹了Android CountDownTimer實(shí)現(xiàn)定時(shí)器和倒計(jì)時(shí)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
Android采取BroadcastReceiver方式自動(dòng)獲取驗(yàn)證碼
這篇文章主要介紹了Android采取BroadcastReceiver方式自動(dòng)獲取驗(yàn)證碼,感興趣的小伙伴們可以參考一下2016-08-08
Android實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07

