Android - Permission denied when binding a mobile network socket (OkHttp.socketFactory(network.socketFactory))

Viewed 273

I'm looking for your help / ideas related to the problem below:

What I want to achieve:

  • route requests through mobile network on Android, using Retrofit2, OktHttp

What I did:

  • obtained the needed Network instance using ConnectivityManager
  • set the socketFactory for OkHttpClient to use the one from the Network instance
  • specified these permissions in AndroidManifest.xml.:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>

Example code:

val network: Network = connectivityManager.allNetworks.first {
    connectivityManager.getNetworkCapabilities(it)?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
}

val okHttpClient = OkHttpClient.Builder()
            .socketFactory(network.socketFactory)
            .build()

Result:

  • exception is thrown: java.net.SocketException: Binding socket to network 505 failed: EACCES (Permission denied)

Notes / Disclaimer:

  • the above works for WiFi
  • I know this is a bad practice: it is not the default behaviour of the application. Users can opt-in and opt-out anytime. There's a specific use case in which this makes sense.

If you have any ideas on what I'm doing wrong, or why isn't it working for mobile networks, please let me know.

1 Answers

I managed to find out why I did wrong.

In a nutshell:

  • instead of ConnectivityManager.getAllNetworks() use ConnectivityManager.requestNetwork(), specifying the network details and use the received one.
val cellularRequest = NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    .build()

connectivityManager.requestNetwork(cellularRequest, object :
            ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                // use the network...
            }
        })

The long story:
When testing, I had WiFi and Mobile Data turned on.
From the docs about ConnectivityManager.getAllNetworks():

  • "Returns an array of all Network currently tracked by the framework."
  • I think the keyword here is the part I've highlighted (Mobile data network interface will not be tracked from the start, it won't appear in the list. When I've seen the one with CELLULAR capabilities, I thought that's the one I'm looking for. I was mistaken. That's a different interface. I didn't observe that it has no internet capability.)
  • by default, it returned 2 network instances, see below:
Network ID Capabilities
525 Transports: CELLULAR
Capabilities: IMS&NOT_METERED&TRUSTED&NOT_VPN&VALIDATED&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED
767 Transports: WIFI
Capabilities: NOT_METERED&INTERNET&NOT_RESTRICTED&TRUSTED&NOT_VPN&VALIDATED&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED

Notice, that the CELLULAR one (ID: 525) has no internet capabilities, and has IMS (which, if I understood correctly from the docs, refers to the GSM network). When I've tried to bind the socket to this network, it obviously failed.
I've then called the requestNetwork() method, and observed that I've received a new (3rd) network instance:

Network ID Capabilities
770 Transports: CELLULAR
Capabilities: SUPL&INTERNET&NOT_RESTRICTED&TRUSTED&NOT_VPN&VALIDATED&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED

Using this one, the binding works seamlessly.

Related