Fix DNS server lookup breaking with Android O due to Android removing access to net.dns# system properties. (#148)
As of Android 8 (Oreo) access to net.dns# has been removed (https://developer.android.com/about/versions/oreo/android-8.0-changes.html). The reasoning given is that it, "improves privacy on the platform". Currently c-ares uses this to get the list of DNS servers. Now the only way to access the DNS server list is by using the Connectivity Manager though Java. This adds the necessary JNI code to use the Connectivity Manager and pull the DNS server list. The old way using __system_property_get with net.dns# remains for compatibilty. Using the Connectivity Manager requires the ACCESS_NETWORK_STATE permission to be set on the app. Existing applications most likely are not setting this and keeping the previous method as a fallback will at the very least ensure those apps don't break on older versions of Android. They will need to add this permission for Android 8 compatibility. Included in the patch are two initalization functions which are required. The JVM must be registered as well as the Connectivity Manager itself. There is no way to get the Connectivity Manager except though Java. Either being passed down to C directly or by passing in an Android Context which can be used to get the Connectivity Manager. Examples are provided in the documentation.pull/151/head
parent
b4f1ff7b51
commit
1abf13ed30
8 changed files with 497 additions and 8 deletions
@ -0,0 +1,252 @@ |
||||
/* Copyright (C) 2017 by John Schember <john@nachtimwald.com>
|
||||
* |
||||
* Permission to use, copy, modify, and distribute this |
||||
* software and its documentation for any purpose and without |
||||
* fee is hereby granted, provided that the above copyright |
||||
* notice appear in all copies and that both that copyright |
||||
* notice and this permission notice appear in supporting |
||||
* documentation, and that the name of M.I.T. not be used in |
||||
* advertising or publicity pertaining to distribution of the |
||||
* software without specific, written prior permission. |
||||
* M.I.T. makes no representations about the suitability of |
||||
* this software for any purpose. It is provided "as is" |
||||
* without express or implied warranty. |
||||
*/ |
||||
#if defined(ANDROID) || defined(__ANDROID__) |
||||
|
||||
#include <jni.h> |
||||
|
||||
#include "ares_setup.h" |
||||
#include "ares.h" |
||||
#include "ares_android.h" |
||||
#include "ares_private.h" |
||||
|
||||
static JavaVM *android_jvm = NULL; |
||||
static jobject android_connectivity_manager = NULL; |
||||
|
||||
static jclass jni_get_class(JNIEnv *env, const char *path) |
||||
{ |
||||
jclass cls = NULL; |
||||
|
||||
if (env == NULL || path == NULL || *path == '\0') |
||||
return NULL; |
||||
|
||||
cls = (*env)->FindClass(env, path); |
||||
if ((*env)->ExceptionOccurred(env)) { |
||||
(*env)->ExceptionClear(env); |
||||
return NULL; |
||||
} |
||||
return cls; |
||||
} |
||||
|
||||
static jmethodID jni_get_method_id(JNIEnv *env, jclass cls, |
||||
const char *func_name, const char *signature) |
||||
{ |
||||
jmethodID mid = NULL; |
||||
|
||||
if (env == NULL || cls == NULL || func_name == NULL || *func_name == '\0' || |
||||
signature == NULL || *signature == '\0') |
||||
{ |
||||
return NULL; |
||||
} |
||||
|
||||
mid = (*env)->GetMethodID(env, cls, func_name, signature); |
||||
if ((*env)->ExceptionOccurred(env)) |
||||
{ |
||||
(*env)->ExceptionClear(env); |
||||
return NULL; |
||||
} |
||||
|
||||
return mid; |
||||
} |
||||
|
||||
void ares_library_init_jvm(JavaVM *jvm) |
||||
{ |
||||
android_jvm = jvm; |
||||
} |
||||
|
||||
int ares_library_init_android(jobject connectivity_manager) |
||||
{ |
||||
JNIEnv *env = NULL; |
||||
int need_detatch = 0; |
||||
int res; |
||||
|
||||
if (android_jvm == NULL) |
||||
return ARES_ENOTINITIALIZED; |
||||
|
||||
res = (*android_jvm)->GetEnv(android_jvm, (void **)&env, JNI_VERSION_1_6); |
||||
if (res == JNI_EDETACHED) |
||||
{ |
||||
env = NULL; |
||||
res = (*android_jvm)->AttachCurrentThread(android_jvm, &env, NULL); |
||||
need_detatch = 1; |
||||
} |
||||
if (res != JNI_OK || env == NULL) |
||||
return ARES_ENOTINITIALIZED; |
||||
|
||||
android_connectivity_manager = (*env)->NewGlobalRef(env, connectivity_manager); |
||||
|
||||
if (need_detatch) |
||||
(*android_jvm)->DetachCurrentThread(android_jvm); |
||||
|
||||
return ARES_SUCCESS; |
||||
} |
||||
|
||||
int ares_library_android_initialized(void) |
||||
{ |
||||
if (android_jvm == NULL || android_connectivity_manager == NULL) |
||||
return ARES_ENOTINITIALIZED; |
||||
return ARES_SUCCESS; |
||||
} |
||||
|
||||
void ares_library_cleanup_android(void) |
||||
{ |
||||
JNIEnv *env = NULL; |
||||
int need_detatch = 0; |
||||
int res; |
||||
|
||||
if (android_jvm == NULL || android_connectivity_manager == NULL) |
||||
return; |
||||
|
||||
res = (*android_jvm)->GetEnv(android_jvm, (void **)&env, JNI_VERSION_1_6); |
||||
if (res == JNI_EDETACHED) |
||||
{ |
||||
env = NULL; |
||||
res = (*android_jvm)->AttachCurrentThread(android_jvm, &env, NULL); |
||||
need_detatch = 1; |
||||
} |
||||
if (res != JNI_OK || env == NULL) |
||||
return; |
||||
|
||||
(*env)->DeleteGlobalRef(env, android_connectivity_manager); |
||||
android_connectivity_manager = NULL; |
||||
|
||||
if (need_detatch) |
||||
(*android_jvm)->DetachCurrentThread(android_jvm); |
||||
} |
||||
|
||||
char **ares_get_android_server_list(size_t max_servers, |
||||
size_t *num_servers) |
||||
{ |
||||
JNIEnv *env = NULL; |
||||
jobject active_network = NULL; |
||||
jobject link_properties = NULL; |
||||
jobject server_list = NULL; |
||||
jobject server = NULL; |
||||
jstring str; |
||||
jclass obj_cls; |
||||
jmethodID obj_mid; |
||||
jclass list_cls; |
||||
jmethodID list_mid; |
||||
jint nserv; |
||||
const char *ch_server_address; |
||||
int res; |
||||
size_t i; |
||||
char **dns_list = NULL; |
||||
int need_detatch = 0; |
||||
|
||||
if (android_jvm == NULL || android_connectivity_manager == NULL || |
||||
max_servers == 0 || num_servers == NULL) |
||||
{ |
||||
return NULL; |
||||
} |
||||
|
||||
res = (*android_jvm)->GetEnv(android_jvm, (void **)&env, JNI_VERSION_1_6); |
||||
if (res == JNI_EDETACHED) |
||||
{ |
||||
env = NULL; |
||||
res = (*android_jvm)->AttachCurrentThread(android_jvm, &env, NULL); |
||||
need_detatch = 1; |
||||
} |
||||
if (res != JNI_OK || env == NULL) |
||||
goto done; |
||||
|
||||
/* JNI below is equivalent to this Java code.
|
||||
import android.content.Context; |
||||
import android.net.ConnectivityManager; |
||||
import android.net.LinkProperties; |
||||
import android.net.Network; |
||||
import java.net.InetAddress; |
||||
import java.util.List; |
||||
|
||||
ConnectivityManager cm = (ConnectivityManager)this.getApplicationContext() |
||||
.getSystemService(Context.CONNECTIVITY_SERVICE); |
||||
Network an = cm.getActiveNetwork(); |
||||
LinkProperties lp = cm.getLinkProperties(an); |
||||
List<InetAddress> dns = lp.getDnsServers(); |
||||
for (InetAddress ia: dns) { |
||||
String ha = ia.getHostAddress(); |
||||
} |
||||
|
||||
Note: The JNI ConnectivityManager object was previously initialized in |
||||
ares_library_init_android. |
||||
*/ |
||||
|
||||
obj_cls = jni_get_class(env, "android/net/ConnectivityManager"); |
||||
obj_mid = jni_get_method_id(env, obj_cls, "getActiveNetwork", |
||||
"()Landroid/net/Network;"); |
||||
active_network = (*env)->CallObjectMethod(env, android_connectivity_manager, |
||||
obj_mid); |
||||
if (active_network == NULL) |
||||
goto done; |
||||
|
||||
obj_mid = jni_get_method_id(env, obj_cls, "getLinkProperties", |
||||
"(Landroid/net/Network;)Landroid/net/LinkProperties;"); |
||||
link_properties = (*env)->CallObjectMethod(env, android_connectivity_manager, |
||||
obj_mid, active_network); |
||||
if (link_properties == NULL) |
||||
goto done; |
||||
|
||||
obj_cls = jni_get_class(env, "android/net/LinkProperties"); |
||||
obj_mid = jni_get_method_id(env, obj_cls, "getDnsServers", |
||||
"()Ljava/util/List;"); |
||||
server_list = (*env)->CallObjectMethod(env, link_properties, obj_mid); |
||||
if (server_list == NULL) |
||||
goto done; |
||||
|
||||
list_cls = jni_get_class(env, "java/util/List"); |
||||
list_mid = jni_get_method_id(env, list_cls, "size", "()I"); |
||||
nserv = (*env)->CallIntMethod(env, server_list, list_mid); |
||||
if (nserv > (jint)max_servers) |
||||
nserv = (jint)max_servers; |
||||
if (nserv <= 0) |
||||
goto done; |
||||
*num_servers = (size_t)nserv; |
||||
list_mid = jni_get_method_id(env, list_cls, "get", "(I)Ljava/lang/Object;"); |
||||
|
||||
obj_cls = jni_get_class(env, "java/net/InetAddress"); |
||||
obj_mid = jni_get_method_id(env, obj_cls, "getHostAddress", |
||||
"()Ljava/lang/String;"); |
||||
dns_list = ares_malloc(sizeof(*dns_list)*(*num_servers)); |
||||
for (i=0; i<*num_servers; i++) |
||||
{ |
||||
server = (*env)->CallObjectMethod(env, server_list, list_mid, (jint)i); |
||||
dns_list[i] = ares_malloc(64); |
||||
dns_list[i][0] = 0; |
||||
if (server == NULL) |
||||
{ |
||||
continue; |
||||
} |
||||
str = (*env)->CallObjectMethod(env, server, obj_mid); |
||||
ch_server_address = (*env)->GetStringUTFChars(env, str, 0); |
||||
strncpy(dns_list[i], ch_server_address, 64); |
||||
(*env)->ReleaseStringUTFChars(env, str, ch_server_address); |
||||
} |
||||
|
||||
done: |
||||
if ((*env)->ExceptionOccurred(env)) |
||||
(*env)->ExceptionClear(env); |
||||
|
||||
if (server_list != NULL) |
||||
(*env)->DeleteLocalRef(env, server_list); |
||||
if (link_properties != NULL) |
||||
(*env)->DeleteLocalRef(env, link_properties); |
||||
if (active_network != NULL) |
||||
(*env)->DeleteLocalRef(env, active_network); |
||||
|
||||
if (need_detatch) |
||||
(*android_jvm)->DetachCurrentThread(android_jvm); |
||||
return dns_list; |
||||
} |
||||
|
||||
#endif |
@ -0,0 +1,26 @@ |
||||
/* Copyright (C) 2017 by John Schember <john@nachtimwald.com>
|
||||
* |
||||
* Permission to use, copy, modify, and distribute this |
||||
* software and its documentation for any purpose and without |
||||
* fee is hereby granted, provided that the above copyright |
||||
* notice appear in all copies and that both that copyright |
||||
* notice and this permission notice appear in supporting |
||||
* documentation, and that the name of M.I.T. not be used in |
||||
* advertising or publicity pertaining to distribution of the |
||||
* software without specific, written prior permission. |
||||
* M.I.T. makes no representations about the suitability of |
||||
* this software for any purpose. It is provided "as is" |
||||
* without express or implied warranty. |
||||
*/ |
||||
|
||||
#ifndef __ARES_ANDROID_H__ |
||||
#define __ARES_ANDROID_H__ |
||||
|
||||
#if defined(ANDROID) || defined(__ANDROID__) |
||||
|
||||
char **ares_get_android_server_list(size_t max_servers, size_t *num_servers); |
||||
void ares_library_cleanup_android(void); |
||||
|
||||
#endif |
||||
|
||||
#endif /* __ARES_ANDROID_H__ */ |
@ -0,0 +1,155 @@ |
||||
.\" |
||||
.\" Copyright (C) 2017 by John Schember |
||||
.\" |
||||
.\" Permission to use, copy, modify, and distribute this |
||||
.\" software and its documentation for any purpose and without |
||||
.\" fee is hereby granted, provided that the above copyright |
||||
.\" notice appear in all copies and that both that copyright |
||||
.\" notice and this permission notice appear in supporting |
||||
.\" documentation, and that the name of M.I.T. not be used in |
||||
.\" advertising or publicity pertaining to distribution of the |
||||
.\" software without specific, written prior permission. |
||||
.\" M.I.T. makes no representations about the suitability of |
||||
.\" this software for any purpose. It is provided "as is" |
||||
.\" without express or implied warranty. |
||||
.\" |
||||
.TH ARES_LIBRARY_ANDROID_INIT 3 "13 Sept 2017" |
||||
.SH NAME |
||||
ares_library_android_init \- c-ares library Android initialization |
||||
.SH SYNOPSIS |
||||
.nf |
||||
#include <ares.h> |
||||
|
||||
int ares_library_android_init(jobject \fIconnectivity_manager\fP) |
||||
|
||||
int ares_library_android_initialized(); |
||||
|
||||
void ares_library_init_jvm(JavaVM *\fIjvm\fP) |
||||
|
||||
.fi |
||||
.SH DESCRIPTION |
||||
.PP |
||||
The |
||||
.B ares_library_android_init |
||||
function performs initializations internally required by the c-ares |
||||
library when used on Android. This can take place anytime after |
||||
\fIares_library_init(3)\fP. It must take place after |
||||
\fIares_library_init_jvm\fP. ares_library_android_init must be called |
||||
before DNS resolution will work on Android 8 (Oreo) or newer when |
||||
targetSdkVersion is set to 26+. |
||||
.PP |
||||
As of Android 8 (API level 26) getting DNS server information has |
||||
becomei more restrictive and can only be accessed using the |
||||
Connectivity Manager. It is necessary to pass the connectivity |
||||
manager to c-ares via JNI. Also, the ACCESS_NETWORK_STATE permission |
||||
must be present in the Android application. |
||||
.PP |
||||
Android older than 8 do not need to to be initalized as they |
||||
are less restrictive. However, this is a run time not compile time |
||||
limitation. Proper Android initalization should take place regardless |
||||
of the targeted Android version. |
||||
.PP |
||||
|
||||
.PP |
||||
Deinitalization will take place though \fIares_library_cleanup(3)\fP. |
||||
.PP |
||||
The |
||||
.B ares_library_init_jvm |
||||
function allows the caller to register the JVM with c-ares. |
||||
It's meant to be called during JNI_OnLoad because you're guaranteed |
||||
to have the JVM in that function. The JVM is required in order |
||||
to use the Connectivty Manager registered using |
||||
\fIares_library_android_init\fP. This must be call before |
||||
\fIares_library_android_init\fP. |
||||
.PP |
||||
The |
||||
.B ares_library_android_initialized |
||||
function can be used to check whether c-ares has been initalized for use |
||||
with Android. |
||||
.SH RETURN VALUES |
||||
ARES_SUCCESS will be returned on success otherwise an error code will |
||||
be returned. |
||||
.SH THREAD SAFETY |
||||
.B These init functions are not thread safe. |
||||
You have to call it once the program has started, but this call must be done |
||||
before the program starts any other thread. This is required to avoid |
||||
potential race conditions in library initialization, and also due to the fact |
||||
these might call functions from other libraries that |
||||
are thread unsafe, and could conflict with any other thread that is already |
||||
using these other libraries. |
||||
.SH JNI |
||||
Accesing the Connectivity Manager though Java: |
||||
.PP |
||||
Register the \fIares_library_android_init\fP. |
||||
.PP |
||||
.Bd -literal |
||||
static JNINativeMethod funcs[] = { |
||||
{ "initialize_native", "(Landroid/net/ConnectivityManager;)I", |
||||
(void *)&ares_library_android_init} |
||||
}; |
||||
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) |
||||
{ |
||||
JNIEnv *env = NULL; |
||||
jclass cls = NULL; |
||||
jint res; |
||||
|
||||
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) |
||||
return -1; |
||||
|
||||
cls = (*env)->FindClass(env, JNIT_CLASS); |
||||
if (cls == NULL) |
||||
return -1; |
||||
|
||||
res = (*env)->RegisterNatives(env, cls, funcs, sizeof(funcs)/sizeof(funcs[0])); |
||||
if (res != 0) |
||||
return -1; |
||||
|
||||
ares_library_init_jvm(vm); |
||||
return JNI_VERSION_1_6; |
||||
} |
||||
.Ed |
||||
.PP |
||||
Calling the registered function from Java: |
||||
.PP |
||||
.Bd -literal |
||||
public class MyObject { |
||||
static { |
||||
System.loadLibrary("cares"); |
||||
} |
||||
|
||||
private static native boolean initialize_native(ConnectivityManager |
||||
connectivity_manager); |
||||
|
||||
public static boolean initialize(Context context) { |
||||
initialize_native((ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE)); |
||||
} |
||||
} |
||||
.Ed |
||||
.PP |
||||
Initalizing the Connectivity Manager in JNI directly using an Android Context. It is assumed |
||||
the JVM has aleady been registered through \fIJNI_OnLoad\fP. |
||||
.PP |
||||
.Bd -literal |
||||
void initialize(jobject android_context) |
||||
{ |
||||
jclass obj_cls = jni_get_class(env, "android/content/Context"); |
||||
jmethodID obj_mid = jni_get_method_id(env, obj_cls, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); |
||||
jfieldID fid = (*env)->GetStaticFieldID(env, obj_cls, "CONNECTIVITY_SERVICE", "Ljava/lang/String;"); |
||||
jstring str = (*env)->GetStaticObjectField(env, obj_cls, fid); |
||||
connectivity_manager = (*env)->CallObjectMethod(env, android_context, obj_mid, str); |
||||
if (connectivity_manager == NULL) |
||||
return; |
||||
ares_library_android_init(connectivity_manager); |
||||
} |
||||
.Ed |
||||
.SH AVAILABILITY |
||||
This function was first introduced in c-ares version 1.14.0. |
||||
.SH SEE ALSO |
||||
.BR ares_library_init(3), |
||||
.BR ares_library_cleanup(3), |
||||
.SH AUTHOR |
||||
John Schember |
||||
.PP |
||||
Copyright (C) 2017 by John Schember |
||||
|
Loading…
Reference in new issue