Android系統網絡架構

這邊文章介紹了Android的系統網絡架構

一. 基本結構

1.1 類圖

在這裏插入圖片描述

1.2 WifiService

WifiManager中公開API的具體實現,提供了WiFi打開與關閉、配置、掃描、連接、斷開等方法;其中也包含了對調用者的權限檢查,如開關WiFi需要"Manifest.permission.CHANGE_WIFI_STATE"權限。
外部調用方式:

WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);

1.3 WifiStateMachine

狀態機: 是設計模式中“狀態模式”的一種運用。可以將繁雜的過程細化爲一個個分支狀態,每個狀態還有自己的父狀態,不同狀態下對消息的處理方式不同,並且消息可以在“子 → 父”狀態中流動。這樣易於管理,並且代碼的結構也更清晰,易於閱讀。

WifiStateMachine 是一個狀態機,用於管理WiFi驅動加載、掃描、連接、獲取IP、漫遊等各個狀態。

基本的狀態如圖:
在這裏插入圖片描述

State Description
DefaultState 初始狀態,WiFi 開關沒有打開,驅動沒有加載。當處於其他狀態時,消息由子狀態上行可用於日誌打印。
ConnectModeState ConnectModeStateWiFi開關已經打開,驅動已經加載,native中wpa_supplicant已經啓動。此時可以進行掃描和連接的操作。進入該狀態時發送 “android.net.wifi.WIFI_STATE_CHANGED” 廣播。
L2ConnectedState L2是"Level 2"的意思,代表OSI網絡模型中的2層即數據鏈路層。這個狀態代表數據鏈路已經建立完成。進入該狀態時發送"android.net.wifi.STATE_CHANGE"廣播,連接狀態是CONNECTING。
ObtainingIpState DHCP獲取IP過程時的狀態。
ConnectedState 已連接狀態,當鏈路建立完成且DHCP配置IP完成後會進入該狀態。進入該狀態時發送"android.net.wifi.STATE_CHANGE"廣播,連接狀態是CONNECTED。
RoamingState 漫遊狀態。如果附近的兩個熱點名字(ssid)相同,且網絡質量達到一定差異化時,系統就會進入漫遊狀態,連接到另一個熱點。
DisconnectingState 斷開中狀態。從斷開發起到斷開成功,處於該狀態。進入該狀態會發送 “android.net.wifi.STATE_CHANGE” 廣播,連接狀態是DISCONNECTING。
DisconnectedState 已斷開狀態。進入該狀態時會發送"android.net.wifi.STATE_CHANGE" 廣播,連接狀態是DISCONNECTED。

adb連接狀態下可以使用 “adb shell dumpsys wifi” 來查看連接WiFi的信息和連接的詳細過程。

上一次掃描結果:
在這裏插入圖片描述
一次L2連接成功的過程:
在這裏插入圖片描述

1.4 ConnectivityService

ConnectivityService(簡稱CS)是Android系統中的網絡連接大管家,所有類型(如WiFi、Telephony、Ethernet等)的網絡都需要註冊關聯到CS並提供鏈路請求接口。CS主要提供了以下幾個方面的功能:

  • 網絡有效性檢測(NetworkMonitor)
  • 網絡評分與選擇(NetworkFactory、NetworkAgent、NetworkAgentInfo)
  • 網口、路由、DNS等參數配置(Netd)
  • 向系統及三方提供網絡申請接口(ConnectivityManager)

啓動方式:

// SystemServer.java
try {
    connectivity = new ConnectivityService(
        context, networkManagement, networkStats, networkPolicy);
    ServiceManager.addService(Context.CONNECTIVITY_SERVICE, connectivity,
                /* allowIsolated= */ false,
        DUMP_FLAG_PRIORITY_HIGH | DUMP_FLAG_PRIORITY_NORMAL);
    networkStats.bindConnectivityManager(connectivity);
    networkPolicy.bindConnectivityManager(connectivity);
} catch (Throwable e) {
    reportWtf("starting Connectivity Service", e);
}

外部調用方式:

ConnectivityManager connectivityManager = 
            (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);

adb連接狀態下可以通過"adb shell dumpsys connectivity"來查看系統當前所有的網絡信息以及網絡檢測等關鍵日誌:
在這裏插入圖片描述

1.5 NetworkFactory

系統中的網絡工廠,也是CS向鏈路網絡請求的統一接口。Android系統啓動之初,數據和WiFi就通過WifiNetworkFactory和TelephonyNetworkFactory將自己註冊到CS中,方便CS迅速響應網絡請求。

NetworkFactory繼承自Handler,並通過AsyncChannel(對Messenger的一種包裝,維護了連接的狀態,本質上使用Messenger)建立了CS和WifiStateMachine之間的單向通信:

// NetworkFactory.java
public void register() {
    if (DBG) log("Registering NetworkFactory");
    if (mMessenger == null) {
        // 創建以自己爲Handler的Messenger並傳遞給CS
        // 之後CS就能夠使用Messenger通過Binder的形式與WifiStateMachine線程通信
        mMessenger = new Messenger(this);
        ConnectivityManager.from(mContext).registerNetworkFactory(mMessenger, LOG_TAG);
    }
}

CS通過NetworkFactory和WifiStateMachine單向通信:
在這裏插入圖片描述
對於AsyncChannel可以參考之前整理的一篇博客:
https://blog.csdn.net/qq_14978113/article/details/80701588

1.6 NetworkAgent

鏈路網絡的代理,是CS和鏈路網絡管理者(如WifiStateMachine)之間的信使,在L2連接成功後創建。通過NetworkAgent,WifiStateMachine可以向CS:

  • 更新網絡狀態 NetworkInfo(斷開、連接中、已連接等)
  • 更新鏈路配置 LinkProperties(本機IP、DNS、路由信息等)
  • 更新網絡能力 NetworkCapabilities(信號強度、是否收費等)

CS可以向WifiStateMachine:

  • 更新網絡有效性(即NetworkMonitor的網絡檢測結果)
  • 禁止自動連接
  • 由於網絡不可上網等原因主動斷開網絡

因此,NetworkAgent提供了CS和WifiStateMachine之間雙向通信的能力。原理類似NetworkFactory,也是使用了AsyncChannel和Messenger。

CS和WifiStateMachine通過NetworkAgent進行雙向通信:
在這裏插入圖片描述

1.7 NetworkMonitor

每種類型的鏈路網絡在L2連接上並註冊到CS中時,CS都會爲其匹配一個NetworkMonitor,用於進行網絡有效性檢測。NetworkMonitor將檢測結果反饋給CS,CS會根據結果進行以下過程:

  • 標記網絡有效性狀態
  • 提示用戶進入網絡二次登錄操作(針對Portal類型網絡,如機場WiFi)
  • 爲網絡進行評分,並進行網絡切換,提供最優網絡

觸發網絡檢測的時機:

  • 鏈路連接上並且Interface/IP/Router/DNS等配置成功
  • 網絡檢測不通過,延時反覆觸發檢測
  • Portal類型網絡登錄後觸發重新檢測
  • 三方APP通過CS的 reportNetworkConnectivity 反饋網絡有效性

檢測方式實際就是通過HTTP請求進行的:

// NetworkMonitor.java
private static final String DEFAULT_HTTPS_URL     = "https://www.google.com/generate_204";
private static final String DEFAULT_HTTP_URL      =
        "http://connectivitycheck.gstatic.com/generate_204";
private static final String DEFAULT_FALLBACK_URL  = "http://www.google.com/gen_204";
private static final String DEFAULT_OTHER_FALLBACK_URLS =
        "http://play.googleapis.com/generate_204";

1.8 NetworkPolicyManagerService

NetworkPolicyManagerService(簡稱NPMS)是Android系統的網絡策略管理者。NPMS會監聽網絡屬性變化(是否收費,metered)、應用前後臺、系統電量狀態(省電模式)、設備休眠狀態(Doze),在這些狀態發生改變時,爲不同名單內的網絡消費者配置不同的網絡策略。

啓動方式:

// SystemServer.java
try {
    networkPolicy = new NetworkPolicyManagerService(context, mActivityManagerService,
            networkManagement);
    ServiceManager.addService(Context.NETWORK_POLICY_SERVICE, networkPolicy);
} catch (Throwable e) {
    reportWtf("starting NetworkPolicy Service", e);
}

外部調用方式:

NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(this);

網絡策略的基本目的:

  • 在收費網絡的情況下省流量
  • 最大可能性的省電
  • 防止危險流量進入

網絡策略中幾個重要的名單:

NameList Description
mUidFirewallStandbyRules 黑名單,針對前後臺應用。此名單中的APP默認REJECT,可配置ALLOW。
mUidFirewallDozableRules 白名單,針對Doze。此名單中的APP在Doze情況下默認ALLOW。
mUidFirewallPowerSaveRules 白名單,針對省電模式(由Battery服務提供)。此名單中的APP在省電模式下默認ALLOW,但在Doze情況下仍然REJECT。

NPMS對網絡策略進行統一管理和記錄,並配合Netd和iptables/ip6tables工具,達到網絡限制的目的。

adb連接狀態下可以使用"adb shell dumpsys netpolicy"來查看當前的網絡策略:
在這裏插入圖片描述

1.9 NetworkManagementService

Android SystemServer不具備直接配置和操作網絡的能力,所有的網絡參數(網口、IP、DNS、Router等)配置,網絡策略執行都需要通過Netd這個native進程來實際執行或者傳遞給Kernel來執行。

而NetworkManagementService(簡稱NMS)就是SystemServer中其他服務連接Netd的橋樑。

NMS和Netd之間通信的方式有兩種:Binder 和 Socket。爲什麼不全使用Binder?原因在於Android老版本上像 Vold、Netd 這種native進程和SystemServer通信的方式都是使用的Socket,目前高版本上也在慢慢的Binder化,提升調用速度。

SystemServer和Netd之間的數據流向圖:
在這裏插入圖片描述

1.10 Netd

爲了保障各個功能的正常運行,Android系統中有非常多的守護進程(Daemon)。爲了保證系統起來後各項功能都已經ready,這些daemon進程跟隨系統的啓動而啓動,而且一般比system_server進程先啓動。如存儲相關的Vold、電話相關的Rild、以及網絡相關Netd等。
在這裏插入圖片描述
init.svc.netd進程由init進程啓動,netd.rc 如下:

service netd /system/bin/netd
    class main
    socket netd stream 0660 root system
    socket dnsproxyd stream 0660 root inet
    socket mdns stream 0660 root system
    socket fwmarkd stream 0660 root inet
    onrestart restart zygote
    onrestart restart zygote_secondary

Netd作爲Android系統的網絡守護者,主要有以下方面的職能:

  • 處理接收來自Kernel的UEvent消息(包含網絡接口、帶寬、路由等信息),並傳遞給Framework
  • 提供防火牆設置、網絡地址轉換(NAT)、帶寬控制、網絡設備綁定(Tether)等接口
  • 管理和緩存DNS信息,爲系統和應用提供域名解析服務

adb連接狀態下可以使用 “adb shell dumpsys network_management” 查看NMS和Netd之前通過socket傳遞的信息記錄:
在這裏插入圖片描述

1.11 wpa_supplicant

與netd一樣,也是Android系統的一個daemon進程,與netd不同的是,它只有在WiFi開啓的情況下才會啓動,在WiFi關閉的時候會隨之關閉。wpa_supplicant向Framework提供了WiFi配置、連接、斷開等接口。

wpa_supplicant比Android的歷史要早,在很多其他平臺上也被廣泛利用,他增加了對更多RFC協議的支持,這也是Google最初選擇它的原因。但從Android近幾個版本來看,Google還是希望弱化wpa_supplicant,並將其功能遷移至Framework或者其他daemon進程中。Android 8.0發生的幾個改變:

  • 與system_server的通信從原來的Socket通信改成了HIDL,提高了速度、便於system分區自升級
  • 掃描的功能遷移到了system/wificond中,弱化wpa_supplicant

啓動方式:

service wpa_supplicant /system/vendor/bin/hw/wpa_supplicant -g@android:wpa_wlan0
    interface [email protected]::ISupplicant default
    interface [email protected]::ISupplicant default
    socket wpa_wlan0 dgram 660 wifi wifi
    class main
    disabled
    oneshot

wpa_supplicant和Framework通信:
在這裏插入圖片描述

二. 注網過程

Android系統網絡註冊過程很複雜,涉及到的模塊也非常多。主要可以分爲以下幾個步驟:

  1. WiFi熱點掃描,獲取掃描結果
  2. 配置WiFi驗證信息,已配置完可忽略
  3. 數據鏈路層L2連接(包含Associate、FourWay-Handshake、Group-Handshake等過程)
  4. DHCP通過UDP的方式獲取IP、Gateway、DNS等網絡信息
  5. 配置Interafce、IP、DNS、Router到netd

2.1 WiFi鏈路連接

以自動連接爲例:

掃描流程:
在這裏插入圖片描述
在Android系統中,WiFi掃描的方式主要有三種:

  1. 前臺掃描:亮屏狀態下且在WiFi Settings頁面,每10s發起一次掃描
  2. 後臺掃描:亮屏狀態下且不在WiFi Settings頁面,掃描間隔呈二進制指數退避,退避:interval * (2^n),最短間隔爲20s,最長間隔爲160s
  3. PNO掃描:滅屏狀態下只掃描已保存的網絡。最小間隔min=20s,最大間隔max=20s*3

在Android 8.0 以後,爲了解決多種掃描類型帶來的冗雜,Google推出了 WifiScanningService,在其中維護了3個狀態機分別應用上述3種掃描:WifiSingleScanStateMachine 、WifiBackgroundScanStateMachine、WifiPnoScanStateMachine。

AP選擇流程:

假如設備中保存了多個可以上網的網絡,並且當前都可以被掃描到,系統如何保證連接上最佳(網絡質量最高、用戶最想要連接)的網絡呢?

Ans:WifiNetworkSelector提供了AP優選的能力,影響優選的因素有:

  1. 是否被用戶由於無法上網而UnWanted,進入了禁止自動連接的黑名單
  2. 信號是否過弱,2.4GHz下不低於-80dBm,5GHz下不低於-77dBm
  3. 其他因素一致情況下,5GHz比2.4GHz享有 40 分加成
  4. 上次用戶主動選擇的AP享有最高 480 分加成,根據時長遞減
  5. 根據信號衰減值(rssi)計算信號分加成(rssi + 85)* 4
  6. 與當前連接的AP一致享有 24 分加成
  7. 非開放網絡享有 80 分加成

連接流程:
在這裏插入圖片描述
Android WiFi的連接過程主要分爲 鏈路連接 和 DHCP獲取IP 兩個過程。(如果使用的是靜態IP則不需要進行DHCP)

WiFi鏈路連接:
在這裏插入圖片描述
(圖片來自:https://blog.csdn.net/QQ474111624/article/details/86620579)

  1. 認證:對於WPA-PSK、WPA2-PSK類型網絡使用密碼(Pre-shared key)進行認證;對於EAP類型(PEAP、TTLS、PWD、TLS)則根據具體的加密方法需要身份、密碼、證書等進行認證。

  2. 關聯:由STA向AP發出關聯請求,AP迴應關聯請求。STA和AP建立關聯後,後續數據報文的收發則只能與關聯的AP進行。

    注:對於開放類型的網絡,這時候鏈路就已經連理成功了。

  3. 四路握手:PTK(Pairwise Transient Key,成對臨時密鑰,用於加密單播數據流的加密密鑰)的生成、交換、安裝。

  4. 組握手:GTK(Group Temporal Key, 組臨時密鑰,用於加密廣播和組播數據流的加密密鑰)的生成、交換、安裝。

該協議原理可以參考:https://blog.csdn.net/QQ474111624/article/details/86620579

DHCP獲取IP:

動態主機設置協議(Dynamic Host Configuration Protocol,DHCP)是一個局域網內的網絡協議,使用UDP協議工作,主要用於內部網或網絡服務供應商自動分配IP地址。

DHCP流程:

在這裏插入圖片描述

  1. DHCP DISCOVER:DHCP客戶機發送有限廣播請求IP。(0.0.0.0:68 → 255.255.255.255:67)

  2. DHCP OFFER:DHCP服務器響應。在收到客戶機的DHCP請求後,DHCP服務器從IP地址池中找出合法可用的IP地址填入DHCP OFFER報文中併發送有限廣播給客戶機。(192.168.1.1:67 → 255.255.255.255:68)

  3. DHCP REQUEST:DHCP客戶機選擇IP。DHCP客戶機從接收到的DHCP OFFER消息中選擇IP地址,併發送DHCP REQUEST有限廣播到所有的DHCP服務器,表明它接受提供的內容。(0.0.0.0:68 → 255.255.255.255:67)

  4. DHCP ACK:DHCP服務器確認租約。(192.168.1.1:67 → 255.255.255.255:68)

Android系統中爲DHCP創建的協議族爲IPPROTO_UDP的Socket:

// DhcpClient.java
private boolean initUdpSocket() {
    final int oldTag = TrafficStats.getAndSetThreadStatsTag(TrafficStats.TAG_SYSTEM_DHCP);
    try {
        // UDP數據報
        mUdpSock = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_REUSEADDR, 1);
        Os.setsockoptIfreq(mUdpSock, SOL_SOCKET, SO_BINDTODEVICE, mIfaceName);
        // 廣播
        Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_BROADCAST, 1);
        Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_RCVBUF, 0);
        // Inet4Address.ANY: 0.0.0.0
        // DhcpPacket.DHCP_CLIENT: 68
        Os.bind(mUdpSock, Inet4Address.ANY, DhcpPacket.DHCP_CLIENT);
        NetworkUtils.protectFromVpn(mUdpSock);
    } catch(SocketException|ErrnoException e) {
        Log.e(TAG, "Error creating UDP socket", e);
        return false;
    } finally {
        TrafficStats.setThreadStatsTag(oldTag);
    }
    return true;
}

2.2 WiFi注網

WiFi、Data、Ethernet等類型鏈路網絡註冊到CS,並將Interface、IP、DNS、Router等網絡屬性設置到Netd中的過程稱爲Android系統的注網過程。

前面已經提到WifiStateMachine和CS之間是通過WifiNetworkAgent使用AsyncChannel來進行雙向通信的,這裏不再贅述。

注網流程圖:
在這裏插入圖片描述
網絡屬性設置到Netd的過程可以通過 “adb shell dumpsys network_management” 來查看,如下兩圖:(O上對於一些屬性的設定已經替換成了Binder的形式)

Android M 6.0
在這裏插入圖片描述
Android O 8.1
在這裏插入圖片描述

2.3 連接狀態及相關廣播

以WiFi舉例,對於應用開發者來說,有3個網絡相關的廣播比較重要:

Class Broadcast Description
WifiManager.java android.net.wifi.WIFI_STATE_CHANGED WiFi開關狀態的改變(打開、打開中、關閉、關閉中)
WifiManager.java android.net.wifi.STATE_CHANGE WiFi連接狀態的改變(已連接、連接中、已斷開、斷開中),“已連接” 不代表此時可以上網。
ConnectivityManager.java android.net.conn.CONNECTIVITY_CHANGE 網絡(不只是WiFi)連接狀態的改變(連接、斷開),“已連接” 代表此時可以上網。

這三個廣播都是粘性廣播,通過Context.sendStickyBroadcast來發送,因此應用在註冊該廣播時,如果之前有發送過該廣播,就一定會收到一次廣播通知。

Note:不要通過 “android.net.wifi.STATE_CHANGE” 來判斷是否可以上網,因爲知識鏈路成功後是不能代表此時可以支持上網的,需要等待CS配置完成併發送 “android.net.conn.CONNECTIVITY_CHANGE” 的廣播後纔可能上網。

三. 網絡優選/評分

3.1 網絡可行性驗證

在鏈路網絡註冊到CS,並且所有網絡配置信息都已經向Netd完成了配置,此時就會開始進行網絡診斷,具體診斷的任務交給NetworkMonitor。

NetworkMonitor也是一個狀態機,包含以下幾種基本狀態:
在這裏插入圖片描述

State Description
DefaultState 初始狀態。接收CS網絡診斷命令消息後觸發診斷;接收用戶登錄網絡消息
MaybeNotifyState 通知用戶登錄。接收診斷後發送的"CMD_LAUNCH_CAPTIVE_PORTAL_APP"消息,startActivity顯示登錄頁面
EvaluatingState 診斷狀態。進入時發送"CMD_REEVALUATE"消息,接收 “CMD_REEVALUATE” 消息並執行網絡診斷過程
CaptivePortalState 登錄狀態。進入時發送"CMD_LAUNCH_CAPTIVE_PORTAL_APP"消息顯示登錄頁面,發送10分鐘延遲的"CMD_CAPTIVE_PORTAL_RECHECK"消息進行再次診斷
ValidatedState 已驗證狀態。進入時發送"EVENT_NETWORK_TESTED"通知CS網絡診斷完成。
EvaluatingPrivateDnsState 私密DNS驗證狀態。Android Pie驗證私密DNS推出。

網絡驗證的原理:

1.DNS驗證:使用"Network.getAllByName(host)"進行DNS解析,成功則驗證通過,拋出"UnknownHostException"異常則說明驗證失敗。

2.HTTP驗證:使用HttpURLConnection訪問generate_204網站(訪問成功會返回204的response code),該網站一般使用Google提供的"http://connectivitycheck.gstatic.com/generate_204",各大手機廠商也會進行定製,防止被牆導致診斷失誤。HTTP驗證會有3種結果,根據response code確定:

  1. code=204:返回值由generate_204網站返回,網絡驗證通過
  2. 200<=code<=399:返回值由路由器網關返回,網絡需要登錄
  3. code不在上述範圍內:無法上網

網絡驗證的主要流程:
在這裏插入圖片描述

3.2 評分機制

CS中註冊的網絡可能不只一種,同時,CS也能夠向Data和WiFi提供的NetworkFactory請求鏈路網絡。多種網絡共存時,就存在優先選擇的問題。CS通過分數統計的方式來進行網絡擇優。

評分的影響因素有:

  1. 鏈路網絡存在初始分數:WiFi默認爲60分,Data默認爲50分
  2. rssi:鏈路網絡根據信號衰減rssi更新初始分數
  3. 是否是用戶強行選擇的網絡(不可上網但用戶主動連接):如果是則100分
  4. 網絡是否可以上網:不可上網則減去40分
// NetworkAgentInfo.java
private int getCurrentScore(boolean pretendValidated) {
    if (networkMisc.explicitlySelected && (networkMisc.acceptUnvalidated || pretendValidated)) {
        // 用戶主動選擇,一般是攝像機、車載WiFi等設備
        // 直接返回100分
        return ConnectivityConstants.MAXIMUM_NETWORK_SCORE;
    }

    // currentScore爲鏈路網絡的初始分數,受rssi影響
    int score = currentScore;
    if (!lastValidated && !pretendValidated && !ignoreWifiUnvalidationPenalty()) 
    {
        // 不可上網,減去40分
        score -= ConnectivityConstants.UNVALIDATED_SCORE_PENALTY;
    }
    if (score < 0) score = 0;
    return score;
}

當前的分數影響到網絡選擇,以WiFi和Data舉例:

如果當前連接Data:

由於score小於WiFI的默認分數60,向WifiNetworkFactory請求網絡,並保持當前網絡直到WiF連接重新觸發網絡驗證、評分和網絡選擇。

如果當前連接WiFi:

  1. score > 50:保持使用WiFi,如果Data連接着且沒有"針對性"(NetworkRequest中存在"TRANSPORT_CELLULAR"選項)請求,則斷開Data網絡
  2. score < 50:保持使用WiFi,並向TelephonyNetworkFactory請求Data網絡,Data連上後重新觸發網絡驗證、評分和網絡選擇

註明:Android O上開始在CS中默認保留一個 “TRANSPORT_CELLULAR” 的NetworkRequest,除非用戶主動關閉數據網絡,否則保持數據鏈路的連接狀態,方便快速進行WiFi和Data之間的快速切換。

四. 網絡策略/防火牆

爲了達到省電/省流量/攔截等目的,Android系統會在多種場景下(Doze、Powersave、前後臺、Metered等)根據配置針對應用進行網絡流量限制。

4.1 Netfilter和iptables

Android基於Linux內核,而Linux則使用Netfilter這個"鉤子"在內核的IP協議棧中去hook各個階段的數據包,根據預先制定的包過濾規則,定義哪些數據包可以接收,哪些數據包需要丟棄或者拒絕。

iptables/ip6tables:iptables/ip6tables是用戶層的一個工具,用戶層使用iptables/ip6tables通過socket的系統調用方式(setsockopt、getsockopt)獲取和修改Netfilter需要的包過濾規則,是用戶層和內核層Netfilter之間交互的工具。(iptables用於IPv4,ip6tables用於IPv6)

Netfilter和iptables是Linux網絡防火牆中重要的組成部分。

Netfilter的工作流程:
在這裏插入圖片描述
(圖片來自 http://blog.chinaunix.net/uid-23069658-id-3160506.html)

收到的每個數據包都從(1)進來,經過路由判決,如果是發送給本機的就經過(2),然後往協議棧的上層繼續傳遞;否則,如果該數據包的目的地是不本機,那麼就經過(3)點,然後順着(5)點將該包轉發出去。Netfilter在 PRE_ROUTING、LOCAL_IN、LOCAL_OUT、FORWARD、POST_ROUTING 這5個階段分別設置回調函數(hook函數),對每一個進出的數據包進行檢測。

q:爲什麼不只在PRE_ROUTING和POST_ROUTING這兩個入口和出口設置數據包檢測?

ans:一方面,這兩個階段處於網絡層(IP層)協議棧中,這時候不會拆解TCP/UDP等傳輸層協議的頭部信息,如果需要對更上層協議內容(如端口等)進行過濾,在這兩個階段顯然不行;
另一方面,這兩個階段協議棧不知道這個數據包是需要轉發給誰,是轉發到下一跳還是傳遞給上層協議棧,如果是需要傳遞給上層應用,就更不知道需要傳遞給哪個應用了。但這些信息在LOCAL_IN和LOCAL_OUT這兩個階段是明確的(明確協議類型:TCP/UDP;明確端口;明確源IP、目的IP),這樣過濾應用的報文就成爲了可能。

Netfilter主要有3個模塊和3張表

  1. 包過濾子模塊:對應filter表,能夠對數據包進行過濾,DROP/REJECT/RETURN/ACCEPT
  2. NAT子模塊:對應nat表,能夠實現網絡地址轉換(這個在運營商服務主機中很常用,路由器中其實也運用了該功能,如你手機的外網IP是120.52.148.57,但內網IP是192.168.1.100,這個時候就需要進行網絡地址轉換)
  3. 數據報修改和跟蹤模塊:對應mangle表,能夠對數據包打上或者判斷mark標記,也可以修改數據報中的其它內容(如IP協議頭部的tos,修改tos的值可以加速數據報在路由中的轉發速度哦…)。

應用層通過iptables工具修改filter、nat和mangle這三張表來控制Netfilter的行爲。

iptables和Netfilter交互方式:

iptables的源碼在/external/iptables目錄下,編譯完成後,iptables在系統中是一個可執行的bin文件,位於/system/bin目錄下:
在這裏插入圖片描述
iptables和Netfilter通信使用的是sockopt的系統調用方式,通過setsockopt和getsockopt在參數中傳遞對應命令值來進行修改和查詢。
在這裏插入圖片描述
(圖片來自 http://blog.chinaunix.net/uid-23069658-id-3160506.html)

內核中定義了iptables sockopt的相關命令值:

// ./include/uapi/linux/netfilter_ipv4/ip_tables.h
/*
 * New IP firewall options for [gs]etsockopt at the RAW IP level.
 * Unlike BSD Linux inherits IP options so you don't have to use a raw
 * socket for this. Instead we check rights in the calls.
 *
 * ATTENTION: check linux/in.h before adding new number here.
 */
#define IPT_BASE_CTL        64

// 修改ip tables規則
#define IPT_SO_SET_REPLACE  (IPT_BASE_CTL)
// 加入流量計數器
#define IPT_SO_SET_ADD_COUNTERS (IPT_BASE_CTL + 1)
#define IPT_SO_SET_MAX      IPT_SO_SET_ADD_COUNTERS

// 獲取ip tables某種類型的表信息
#define IPT_SO_GET_INFO         (IPT_BASE_CTL)
// 獲取ip tables規則信息
#define IPT_SO_GET_ENTRIES      (IPT_BASE_CTL + 1)
#define IPT_SO_GET_REVISION_MATCH   (IPT_BASE_CTL + 2)
#define IPT_SO_GET_REVISION_TARGET  (IPT_BASE_CTL + 3)
#define IPT_SO_GET_MAX          IPT_SO_GET_REVISION_TARGET

以iptables獲取某個表的chain和規則信息爲例:

// ./external/iptables/libiptc/libiptc.c
struct xtc_handle *TC_INIT(const char *tablename)
{
        // 表所有信息數據結構,包含info和規則等
        struct xtc_handle *h;
        // 表基本信息數據結構
        STRUCT_GETINFO info;
        unsigned int tmp;
        socklen_t s;
        int sockfd;
retry:
        iptc_fn = TC_INIT;
        //...
        sockfd = socket(TC_AF, SOCK_RAW, IPPROTO_RAW);
        //...
        s = sizeof(info);

        // 把tablename複製到info中,用於告知Netfilter查詢的是哪張表
        strcpy(info.name, tablename);
        
        // 使用getsockopt的系統調用方式,其中IPT命令爲SO_GET_INFO,對應內核
        // 中定義的IPT_SO_GET_INFO,調用完成後,表信息通過info參數返回
        if (getsockopt(sockfd, TC_IPPROTO, SO_GET_INFO, &info, &s) < 0) {
                close(sockfd);
                return NULL;
        }

        if ((h = alloc_handle(info.name, info.size, info.num_entries))
            == NULL) {
                close(sockfd);
                return NULL;
        }

        /* Initialize current state */
        h->sockfd = sockfd;
        h->info = info;

        h->entries->size = h->info.size;

        tmp = sizeof(STRUCT_GET_ENTRIES) + h->info.size;

        // 使用getsockopt的系統調用方式,其中IPT命令爲SO_GET_ENTRIES,對應內核
        // 中定義的IPT_SO_GET_ENTRIES,調用完成後,規則信息通過h->entries參數返回
        if (getsockopt(h->sockfd, TC_IPPROTO, SO_GET_ENTRIES, h->entries,
                       &tmp) < 0)
                goto error;

        if (parse_table(h) < 0)
                goto error;

        CHECK(h);
        return h;error:
        TC_FREE(h);
        /* A different process changed the ruleset size, retry */
        if (errno == EAGAIN)
                goto retry;
        return NULL;
}

當然,native層並不需要這麼複雜的去操作ip tables,這些都已經被iptables工具封裝好了。系統中如netd這些native進程甚至我們獲得root權限的shell只需要使用iptables命令就可以,如查看filter表信息:
在這裏插入圖片描述
這個過程其實就是fork了iptables進程並執行了其main函數,並且攜帶了"-t filter -L"等args參數。

註明:Google、各大ODM及手機廠商都會配置很多包過濾規則來進行定製化,因此iptables的操作會很頻繁,每次fork都會佔用比較大的時間資源;並且爲了保證併發訪問修改內核的ip tables規則,iptables中其實是有文件鎖(#define XT_LOCK_NAME "/system/etc/xtables.lock")存在的,這樣就又存在排隊等待。這個過程比較耗時甚至可能還會引起上層的系統watchdog。

Google在Android O上做了優化:netd中fork出一個iptables-restore進程並且保持它的存活,每次需要時都通過socket的方式將命令發送給子進程,並且在成串的命令做了優化,儘可能保證一次查詢一次修改。大大優化了效率。
在這裏插入圖片描述

4.2 前後臺網絡策略

前面介紹NetworkPolicyManagerService是提到了一個mUidFirewallStandbyRules數組名單,這裏面緩存了後臺需要限制上網的uid名單。

NameList Description
mUidFirewallStandbyRules 黑名單,針對前後臺應用。此名單中的APP默認REJECT,可配置ALLOW。
mUidFirewallDozableRules 白名單,針對Doze。此名單中的APP在Doze情況下默認ALLOW。
mUidFirewallPowerSaveRules 白名單,針對省電模式(由Battery服務提供)。此名單中的APP在省電模式下默認ALLOW,但在Doze情況下仍然REJECT。

前後臺網絡策略最終通過filter表中的fw_standby來控制:
在這裏插入圖片描述
“2 references”:被另外兩條chain引用,fw_INPUT和fw_OUTPUT。只有鏈接到最終Netfilter直接操作的chain上才能夠生效。
含義:所屬進程uid是10121的數據包都會被丟棄。

mUidFirewallStandbyRules這個名單和fw_standby中的名單是對應的,這個名單也會被動態配置,根據應用的前後臺變化及系統的定製來確定。

註明:只有在非充電情況下fw_standy這條chain纔會生效,可以通過 “adb shell dumpsys battery unplug” 來取消USB充電,然後使用 “adb shell iptables -t filter -L” 來查看。

4.3 Doze下網絡策略

對應NetworkPolicyManagerService中的mUidFirewallDozableRules,這是個白名單,對應filter表中的fw_dozable鏈。這個白名單也是可配置的,如MIUI中的PowerKeeper就會對其進行配置,將一些關鍵應用配置在其中,防止Doze情況下這些應用無法上網,影響用戶使用。

正常情況下,fw_dozable這條鏈不會被使用:
在這裏插入圖片描述
當系統進入Doze模式下,fw_dozable就會被使用並且Add到fw_INPUT和fw_OUTPUT中:
在這裏插入圖片描述
這樣,與fw_dozable中配置的RETURN標誌規則的數據包都會被放行,如果都不匹配,則走到最後一條規則,被DROP:
在這裏插入圖片描述
注:可以使用 "adb shell dumpsys deviceidle force-idle deep"來進入Doze模式
在這裏插入圖片描述

五. 無法上網原因

最後,簡單介紹下幾種常見的無法上網的原因:

  1. WiFi網絡未驗證(portal網絡),訪問時路由器會進行redirect
  2. 運營商問題,無法連接外網
  3. DNS服務器問題,導致DNS解析失敗
  4. 系統時間不正常,導致SSL握手失敗,證書無效,HTTPS無法上網
  5. 系統對DNS訪問進行hook,某些情況下強行拒絕。如miuifirewall
  6. 系統由於Doze等原因進入省電模式,Netfilter根據iptables的設定進行DROP或者REJECT
  7. 被VPN代理,代理服務器問題
  8. 服務器自身問題
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章