Android: 限定app可访问的网络 – setProcessDefaultNetwork

在Android系统中, 我们可以在系统设置->流量使用情况里面,查看单个应用程序在各个不同网络(Mobile Network,WLAN)下的数据流量,同时还可以看到它们在前后台分别使用了多少的数据。并且还可以限制单个应用程序后台数据。但是可惜的是虽然Android提供了标准的API,可以限制应用程序可访问的网络,却没有相应的设置项。我想很多人都希望在手机使用移动网络的时候,能够限制某些应用程序访问移动网络(不仅仅是限制后台数据的使用量),或者在移动网络与其他网络共存的情况下,进行限定。

  • 原理及实现方法

Android系统定义的网络类型包括:Bluetooth, ethernet, mobile, mobile_dun, mobile_hipri, mobile_mms, mobile_supl, vpn, wifi以及wimax。可惜的是通过USB网络共享的网络却没有包含在内。好在通过蓝牙共享的网络包含在内:

$ adb shell dumpsys connectivity
...
Current Networks:
  NetworkAgentInfo{ ni{[type: Bluetooth Tethering[], state: CONNECTED/CONNECTED, reason: (unspecified), extra: (none), roaming: false, failover: false, isAvailable: true, isConnectedToProvisioningNetwork: false]}  network{101}  lp{{InterfaceName: bt-pan LinkAddresses: [192.168.44.192/24,]  Routes: [192.168.44.0/24 -> 0.0.0.0 bt-pan,0.0.0.0/0 -> 192.168.44.1 bt-pan,] DnsAddresses: [192.168.44.1,] Domains:  MTU: 0}}  nc{[ Transports: BLUETOOTH Capabilities: INTERNET&NOT_RESTRICTED&TRUSTED&NOT_VPN LinkUpBandwidth>=24000Kbps LinkDnBandwidth>=24000Kbps]}  Score{29}  everValidated{false}  lastValidated{false}  created{true}  explicitlySelected{false} }
...

Android SDK中提供了标准API,可以让某个进程只能访问特定的网络类型:

http://developer.android.com/reference/android/net/ConnectivityManager.html#setProcessDefaultNetwork(android.net.Network)

This method was deprecated in API level 23.

This function can throw IllegalStateException. Use bindProcessToNetwork(Network) instead. bindProcessToNetwork is a direct replacement.

Binds the current process to network. All Sockets created in the future (and not explicitly bound via a bound SocketFactory from Network.getSocketFactory()) will be bound to network. All host name resolutions will be limited to network as well. Note that if network ever disconnects, all Sockets created in this way will cease to work and all host name resolutions will fail. This is by design so an application doesn‘t accidentally use Sockets it thinks are still bound to a particular Network. To clear binding pass null for network. Using individually bound Sockets created by Network.getSocketFactory().createSocket() and performing network-specific host name resolutions via Network.getAllByName is preferred to calling setProcessDefaultNetwork.

当执行ConnectivityManager.setProcessDefaultNetwork()之后,当前进程只能使用指定的网络类型。

这个功能是如何实现的呢,我们先看一下frameworks/base/core/java/android/net/ConnectivityManager.java相关代码实现:

public class ConnectivityManager {
    // ...
    public static boolean setProcessDefaultNetwork(Network network) {
        int netId = (network == null) ? NETID_UNSET : network.netId;
        if (netId == NetworkUtils.getNetworkBoundToProcess()) {
            return true;
        }
        if (NetworkUtils.bindProcessToNetwork(netId)) {
            // Set HTTP proxy system properties to match network.
            // TODO: Deprecate this static method and replace it with a non-static version.
            Proxy.setHttpProxySystemProperty(getInstance().getDefaultProxy());
            // Must flush DNS cache as new network may have different DNS resolutions.
            InetAddress.clearDnsCache();
            // Must flush socket pool as idle sockets will be bound to previous network and may
            // cause subsequent fetches to be performed on old network.
            NetworkEventDispatcher.getInstance().onNetworkConfigurationChanged();
            return true;
        } else {
            return false;
        }
    }
    // ...
}

可见在setProcessDefaultNetwork()的时候,http proxy,dns都会使用当前网络的配置。我们再看一下bindProcessToNetwork做了些什么:

public class NetworkUtils {
    // ...
    /**
     * Binds the current process to the network designated by {@code netId}.  All sockets created
     * in the future (and not explicitly bound via a bound {@link SocketFactory} (see
     * {@link Network#getSocketFactory}) will be bound to this network.  Note that if this
     * {@code Network} ever disconnects all sockets created in this way will cease to work.  This
     * is by design so an application doesn't accidentally use sockets it thinks are still bound to
     * a particular {@code Network}.  Passing NETID_UNSET clears the binding.
     */
    public native static boolean bindProcessToNetwork(int netId);
    // ...
}

bindProcessToNetwork()是JNI方法,再往下看(frameworks/base/core/jni/android_net_NetUtils.cpp):

static jboolean android_net_utils_bindProcessToNetwork(JNIEnv *env, jobject thiz, jint netId)
{
    return (jboolean) !setNetworkForProcess(netId);
}
// ...
/*
 * JNI registration.
 */
static JNINativeMethod gNetworkUtilMethods[] = {
    /* name, signature, funcPtr */
    // ...
    { "bindProcessToNetwork", "(I)Z", (void*) android_net_utils_bindProcessToNetwork },
    // ...
};

setNetworkForProcess()的实现在system/netd/client/NetdClient.cpp中:

// ...
int setNetworkForTarget(unsigned netId, std::atomic_uint* target) {
    if (netId == NETID_UNSET) {
        *target = netId;
        return 0;
    }
    // Verify that we are allowed to use |netId|, by creating a socket and trying to have it marked
    // with the netId. Call libcSocket() directly; else the socket creation (via netdClientSocket())
    // might itself cause another check with the fwmark server, which would be wasteful.
    int socketFd;
    if (libcSocket) {
        socketFd = libcSocket(AF_INET6, SOCK_DGRAM, 0);
    } else {
        socketFd = socket(AF_INET6, SOCK_DGRAM, 0);
    }
    if (socketFd < 0) {
        return -errno;
    }
    int error = setNetworkForSocket(netId, socketFd);
    if (!error) {
        *target = netId;
    }
    close(socketFd);
    return error;
}

extern "C" int setNetworkForSocket(unsigned netId, int socketFd) {
    if (socketFd < 0) {
        return -EBADF;
    }
    FwmarkCommand command = {FwmarkCommand::SELECT_NETWORK, netId, 0};
    return FwmarkClient().send(&command, sizeof(command), socketFd);
}

extern "C" int setNetworkForProcess(unsigned netId) {
    return setNetworkForTarget(netId, &netIdForProcess);
}
// ...

客户端发送FwmarkCommand::SELECT_NETWORK通知服务端设置socketFd的属性,相关代码在

system/netd/server/FwmarkServer.cpp:

int FwmarkServer::processClient(SocketClient* client, int* socketFd) {
    // ...
    switch (command.cmdId) {
    // ...
        case FwmarkCommand::SELECT_NETWORK: {
            fwmark.netId = command.netId;
            if (command.netId == NETID_UNSET) {
                fwmark.explicitlySelected = false;
                fwmark.protectedFromVpn = false;
                permission = PERMISSION_NONE;
            } else {
                if (int ret = mNetworkController->checkUserNetworkAccess(client->getUid(),
                                                                         command.netId)) {
                    return ret;
                }
                fwmark.explicitlySelected = true;
                fwmark.protectedFromVpn = mNetworkController->canProtect(client->getUid());
            }
            break;
        }
        // ...
    }

    fwmark.permission = permission;

    if (setsockopt(*socketFd, SOL_SOCKET, SO_MARK, &fwmark.intValue,
                   sizeof(fwmark.intValue)) == -1) {
        return -errno;
    }

    return 0;
}

为什么这么做可以了?

先看一下bionic中的这个commit:

commit ceb5bd787c8ce281e5f4343c5d4f77b41c3e2919
Author: Sreeram Ramachandran <sreeram@google.com>
Date:   Mon May 12 11:19:16 2014 -0700

    Introduce netd_client, a dynamic library that talks to netd.
    
    The library exists outside bionic. It is dynamically loaded, to replace selected
    standard socket syscalls with versions that talk to netd.
    
    Change connect() to use the library if available.
    
    (cherry picked from commit 3a6b627a14df8111b03e452f2df4b5f4938e0e49)
    
    Change-Id: Ib6198e19dbc306521a26fcecfdf6e8424d163fc9
---
 libc/Android.mk                       |  2 ++
 libc/SYSCALLS.TXT                     |  4 ++--
 libc/arch-arm/syscalls/__connect.S    | 14 ++++++++++++++
 libc/arch-arm/syscalls/connect.S      | 14 --------------
 libc/arch-arm64/syscalls/__connect.S  | 22 ++++++++++++++++++++++
 libc/arch-arm64/syscalls/connect.S    | 21 ---------------------
 libc/arch-mips/syscalls/__connect.S   | 19 +++++++++++++++++++
 libc/arch-mips/syscalls/connect.S     | 19 -------------------
 libc/arch-mips64/syscalls/__connect.S | 26 ++++++++++++++++++++++++++
 libc/arch-mips64/syscalls/connect.S   | 25 -------------------------
 libc/arch-x86/syscalls/__connect.S    | 27 +++++++++++++++++++++++++++
 libc/arch-x86/syscalls/connect.S      | 27 ---------------------------
 libc/arch-x86_64/syscalls/__connect.S | 17 +++++++++++++++++
 libc/arch-x86_64/syscalls/connect.S   | 16 ----------------
 libc/bionic/NetdClient.cpp            | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 libc/bionic/connect.cpp               | 22 ++++++++++++++++++++++
 libc/bionic/libc_init_dynamic.cpp     |  4 ++++
 libc/private/NetdClient.h             | 28 ++++++++++++++++++++++++++++
 18 files changed, 251 insertions(+), 124 deletions(-)

bionic/libc/bionic/NetdClient.cpp:

template <typename FunctionType>
static void netdClientInitFunction(void* handle, const char* symbol, FunctionType* function) {
    typedef void (*InitFunctionType)(FunctionType*);
    InitFunctionType initFunction = reinterpret_cast<InitFunctionType>(dlsym(handle, symbol));
    if (initFunction != NULL) {
        initFunction(function);
    }
}

static void netdClientInitImpl() {
    void* netdClientHandle = dlopen("libnetd_client.so", RTLD_LAZY);
    if (netdClientHandle == NULL) {
        // If the library is not available, it's not an error. We'll just use
        // default implementations of functions that it would've overridden.
        return;
    }
    netdClientInitFunction(netdClientHandle, "netdClientInitAccept4",
                           &__netdClientDispatch.accept4);
    netdClientInitFunction(netdClientHandle, "netdClientInitConnect",
                           &__netdClientDispatch.connect);
    netdClientInitFunction(netdClientHandle, "netdClientInitNetIdForResolv",
                           &__netdClientDispatch.netIdForResolv);
    netdClientInitFunction(netdClientHandle, "netdClientInitSocket", &__netdClientDispatch.socket);
}

static pthread_once_t netdClientInitOnce = PTHREAD_ONCE_INIT;

extern "C" __LIBC_HIDDEN__ void netdClientInit() {
    if (pthread_once(&netdClientInitOnce, netdClientInitImpl)) {
        __libc_format_log(ANDROID_LOG_ERROR, "netdClient", "Failed to initialize netd_client");
    }
}

bionic/libc/private/NetdClientDispatch.h:

struct NetdClientDispatch {
    int (*accept4)(int, struct sockaddr*, socklen_t*, int);
    int (*connect)(int, const struct sockaddr*, socklen_t);
    int (*socket)(int, int, int);
    unsigned (*netIdForResolv)(unsigned);
};

extern __LIBC_HIDDEN__ struct NetdClientDispatch __netdClientDispatch;

bionic/libc/bionic/NetdClientDispatch.cpp:

extern "C" __socketcall int __accept4(int, sockaddr*, socklen_t*, int);
extern "C" __socketcall int __connect(int, const sockaddr*, socklen_t);
extern "C" __socketcall int __socket(int, int, int);

static unsigned fallBackNetIdForResolv(unsigned netId) {
    return netId;
}

// This structure is modified only at startup (when libc.so is loaded) and never
// afterwards, so it's okay that it's read later at runtime without a lock.
__LIBC_HIDDEN__ NetdClientDispatch __netdClientDispatch __attribute__((aligned(32))) = {
    __accept4,
    __connect,
    __socket,
    fallBackNetIdForResolv,
};

bionic/libc/bionic/socket.cpp:

int socket(int domain, int type, int protocol) {
    return __netdClientDispatch.socket(domain, type, protocol);
}

bionic/libc/bionic/connect.cpp:

int connect(int sockfd, const sockaddr* addr, socklen_t addrlen) {
    return __netdClientDispatch.connect(sockfd, addr, addrlen);
}

system/netd/client/NetdClient.cpp:

int netdClientConnect(int sockfd, const sockaddr* addr, socklen_t addrlen) {
    if (sockfd >= 0 && addr && FwmarkClient::shouldSetFwmark(addr->sa_family)) {
        FwmarkCommand command = {FwmarkCommand::ON_CONNECT, 0, 0};
        if (int error = FwmarkClient().send(&command, sizeof(command), sockfd)) {
            errno = -error;
            return -1;
        }
    }
    return libcConnect(sockfd, addr, addrlen);
}
// ...
extern "C" void netdClientInitConnect(ConnectFunctionType* function) {
    if (function && *function) {
        libcConnect = *function;
        *function = netdClientConnect;
    }
}

所以当你调用libc中的connect()的时候, connect() -> netdClientConnect() -> __connect(),巧妙!

NOTE: fwmark可通过如下命令查看:

$ adb shell ip rule list
0:	from all lookup local 
10000:	from all fwmark 0xc0000/0xd0000 lookup 99 
13000:	from all fwmark 0x10063/0x1ffff lookup 97 
13000:	from all fwmark 0x10066/0x1ffff lookup 1032 
14000:	from all oif wlan0 lookup 1032 
15000:	from all fwmark 0x0/0x10000 lookup 99 
16000:	from all fwmark 0x0/0x10000 lookup 98 
17000:	from all fwmark 0x0/0x10000 lookup 97 
19000:	from all fwmark 0x66/0x1ffff lookup 1032 
22000:	from all fwmark 0x0/0xffff lookup 1032 
23000:	from all fwmark 0x0/0xffff uidrange 0-0 lookup main 
32000:	from all unreachable
  • 环境

Android系统版本:android-5.1.1_r15 @Nexus 4, 在项目中编译。

  • 代码

相关的设置代码可以参考:

frameworks/base/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java:

        // ...
        final ConnectivityManager cm = ConnectivityManager.from(this);
        final Network network = new Network(mNetId);
        // Also initializes proxy system properties.
        cm.setProcessDefaultNetwork(network);
        // ...

发表评论

电子邮件地址不会被公开。 必填项已用*标注