Android4.3前后DNS解析简单研究

1. Change of Android4.3


在Android4.3以前,如果系统需要备份/恢复,防火墙以及DNS解析管理,Linux内核微调等,是需要ROOT权限才能进行的。在Android4.3中,Google修改了这一策略,Google向用户提供API和扩展来完成这些事情。其中DNS解析就是这一改变中的一环。







2. Android的DNS解析



Bionic是Android自己的C库版本。


在早期版本的Android中,DNS解析的方式类似于Ubuntu等发行版Linux。都是通过resovl.conf文件进行域名解析的。在老版本Android的bionic/libc/docs/overview.txt中可以看到,Android的DNS也是采用NetBSD-derived resolver library来实现,不同的是,bionic对其进行了一些修改。这些修改包括:


1.     resovle.conf文件的位置不再是/etc/resolv.conf,在Android中改为了/system/etc/resolv.conf。

2.     从系统属性(SystemProperties)中读取DNS服务器,比如“net.dns1”,“net.dns2”等。每一个属性必须包括了DNS服务器的IP地址。

3.     不实现Name ServiceSwitch。

4.     在查询时,使用一个随机的查询ID,而非每次自增1.

5.     在查询时,将本地客户端的socket绑定到一个随机端口以增强安全性。



3. Java与JNI层中DNS解析的公共流程



我们从下面小例子开始分析公共流程中DNS解析所经过的函数,对于Android中JNI和JAVA等层次概念请参考最开始的那一张结构图:



//获得www.taobao.com对应的IP地址,并通过Toast的方式打印出来
try {
	    InetAddress inetAddress = InetAddress.getByName("www.taobao.com");
	    Toast.makeText(MainActivity.this, "Address is " + inetAddress.getHostAddress(), Toast.LENGTH_LONG).show();		} catch (UnknownHostException e) {
	    			// TODO Auto-generated catch block
	    			e.printStackTrace();
	  }

以上Java代码给出了最简单的一次DNS解析的方法。主要实现是调用InetAddress类的静态方法getByName,该方法返回一个InetAddress实例,该实例中包括很多关于域名的信息。


    public static InetAddress getByName(String host) throws UnknownHostException {
        return getAllByNameImpl(host)[0];
    }

实际调用getAllByNameImpl函数。该函数内部主要进行三件事情,第一件,如果host是null,那么调用loopbackAddresses()。如果host是数字形式的地址,那么调用parseNumericAddressNoThrow解析并返回。如果是一个字符串,则使用lookupHostByName(host)返回一个InetAddress并clone一份返回。

 

lookupHostByName函数首先host的信息是否存在在缓存当中,如果有则返回。如果没有则:


InetAddress[] addresses = Libcore.os.getaddrinfo(host, hints);

getaddrinfo函数是一个native本地函数,声明如下:


public native InetAddress[] getaddrinfo(String node, StructAddrinfo hints) throws GaiException;

在getaddrinfo对应的JNI层函数中,实际调用了下面函数:


int rc = getaddrinfo(node.c_str(), NULL, &hints, &addressList);

getaddrinfo实现自bionic的netbsd库,具体文件位于/bionic/libc/netbsd/net中,后面我们会分析Android4.2和Android4.3的代码,来观察Google在Android4.3中对DNS解析做了什么样的修改。

除了getaddrinfo路径以外,在Java中InetAddress还有其他方式,比如

public String getHostName() {
        if (hostname == null) {
            try {
                hostname = getHostByAddrImpl(this).hostName;
            } catch (UnknownHostException ex) {
                hostname = getHostAddress();
            }
        }
        return hostname;
}


上述方法,调用了getHostByAddrImpl,在getHostByAddrImpl中:


String hostname = Libcore.os.getnameinfo(address, NI_NAMEREQD);

调用了getnameinfo方法,该方法同样是一个native函数,在JNI层对应的函数中直接调用了getnameinfo这个bionic库的函数:


int rc = getnameinfo(reinterpret_cast<sockaddr*>(&ss), size, buf, sizeof(buf), NULL, 0, flags);




4. Android4.2和Android4.3 bionic中DNS解析实现的变化



不管是getaddrinfo还是getnameinfo还是gethostbyname,都是实现在bionic库中,这里先以getaddrinfo为例分析Android4.3前后bionic在DNS解析处通用逻辑的变化。先从4.3以前版本开始。

在getaddrinfo中,关键的一步如下:



/*         
* BEGIN ANDROID CHANGES; proxying to the cache
*/
if (android_getaddrinfo_proxy(hostname, servname, hints, res) == 0) {
return 0;
}

注意上面的注释,ANDROID_CHANGES,Google在Android4.2.2开始已经打算将所有DNS解析的方式向Netd代理的方式过渡了。后面我们还会看到ANDROID_CHANGES。

然后在android_getaddrinfo_proxy中,我们可以看到如下代码:


snprintf(propname, sizeof(propname), "net.dns1.%d", getpid());
if (__system_property_get(propname, propvalue) > 0) {
		return -1;
	}
// Bogus things we can't serialize.  Don't use the proxy.
if ((hostname != NULL &&
    strcspn(hostname, " \n\r\t^'\"") != strlen(hostname)) ||
   (servname != NULL &&
    strcspn(servname, " \n\r\t^'\"") != strlen(servname))) {
	return -1;
}
…
// Send the request.
proxy = fdopen(sock, "r+");
if (fprintf(proxy, "getaddrinfo %s %s %d %d %d %d",
	    hostname == NULL ? "^" : hostname,
	    servname == NULL ? "^" : servname,
	    hints == NULL ? -1 : hints->ai_flags,
		hints == NULL ? -1 : hints->ai_family,
	    hints == NULL ? -1 : hints->ai_socktype,
		hints == NULL ? -1 : hints->ai_protocol) < 0) {
	goto exit;
}
// literal NULL byte at end, required by FrameworkListener
if (fputc(0, proxy) == EOF ||
    fflush(proxy) != 0) {
	goto exit;
}

Android会首先尝试从系统属性(System Property)中读取DNS服务器的IP地址,然后使用这个DNS服务器来进行DNS解析。如果没有设置相关系统属性,则采用Netd的方式来进行DNS解析。由于在使用Netd方式进行解析的时候server name是不能为NULL的,所以可以看到上面将server name修改成了’^’。在分析Netd代理之前,我们最好停一停,看看Android4.3后,getaddrinfo是怎么做的。

 

首先是从JNI层的getaddrinfo的代码开始:


int rc = getaddrinfo(node.c_str(), NULL, &hints, &addressList);

和Android4.2.2没有变化,直接调用了getaddrinfo,其中第二个参数是NULL。


Int
getaddrinfo(const char *hostname, const char *servname,
const struct addrinfo *hints, struct addrinfo **res)
{
	return android_getaddrinfoforiface(hostname, servname, hints, NULL, 0, res);
}

直接调用了android_getaddrinfoforiface函数。


/* 4.3 */
static int android_getaddrinfo_proxy(
    const char *hostname, const char *servname,
    const struct addrinfo *hints, struct addrinfo **res, const char *iface)
{
	int sock;
	const int one = 1;
	struct sockaddr_un proxy_addr;
	FILE* proxy = NULL;
	int success = 0;
	*res = NULL;

	if ((hostname != NULL &&
	     strcspn(hostname, " \n\r\t^'\"") != strlen(hostname)) ||
	    (servname != NULL &&
	     strcspn(servname, " \n\r\t^'\"") != strlen(servname))) {
		return EAI_NODATA;
	}

	sock = socket(AF_UNIX, SOCK_STREAM, 0);
	if (sock < 0) {
		return EAI_NODATA;
	}

    …….


很明显,Android4.3以后删掉了读取系统属性的那一段代码,这时如果任然采用添加系统属性的方法来修改DNS服务器将不会产生任何作用。

 

Android除了使用getaddrinfo函数外,系统代码还会使用gethostbyname等其他路径。下面我们再看看gethostbyname路径在Android4.3前后发生的变化。

在给出代码之前,先说明下gethostbyname函数内部将调用gethostbyname_internal来真正进行DNS解析。


Android4.2.2:



static struct hostent *
gethostbyname_internal(const char *name, int af, res_state res)
{
	…

	rs->host.h_addrtype = af;
	rs->host.h_length = size;
	/*
	 * if there aren’t any dots, it could be a user-level alias.
	 * this is also done in res_nquery() since we are not the only
	 * function that looks up host names.
	 */
	if (!strchr(name, ‘.’) && (cp = __hostalias(name)))
		name = cp;
	
/*
	 * disallow names consisting only of digits/dots, unless
	 * they end in a dot.
	 */
	if (isdigit((u_char) name[0]))
		for (cp = name;; ++cp) {
                           …
		}
			if (!isdigit((u_char) *cp) && *cp != ‘.’)
				break;
		}
	if ((isxdigit((u_char) name[0]) && strchr(name, ‘:’) != NULL) ||
	    name[0] == ‘:’)
		for (cp = name;; ++cp) {
			if (!*cp) {
				…
			}
			if (!isxdigit((u_char) *cp) && *cp != ‘:’ && *cp != ‘.’)
				break;
		}
	hp = NULL;
	h_errno = NETDB_INTERNAL;
	if (nsdispatch(&hp, dtab, NSDB_HOSTS, “gethostbyname”,
	    default_dns_files, name, strlen(name), af) != NS_SUCCESS) {
		return NULL;
        }
	h_errno = NETDB_SUCCESS;
	return hp;
}

先不关心使用的localdns是哪个,在Android4.2.2中,gethostbyname_internal直接调用了nsdispatch来进行域名解析。

 

下面再看看Android4.3中的变化:



static struct hostent *
gethostbyname_internal(const char *name, int af, res_state res, const char *iface, int mark)
{
…
	proxy = android_open_proxy();
	if (proxy == NULL) goto exit;

	/* This is writing to system/netd/DnsProxyListener.cpp and changes
	 * here need to be matched there */
	if (fprintf(proxy, “gethostbyname %s %s %d”,
			iface == NULL ? “^” : iface,
			name == NULL ? “^” : name,
			af) < 0) {
		goto exit;
	}

	if (fputc(0, proxy) == EOF || fflush(proxy) != 0) {
		goto exit;
	}

	result = android_read_hostent(proxy);

exit:
	if (proxy != NULL) {
		fclose(proxy);
	}
	return result;
}

从上面代码可以看到,Android4.3中彻底全面使用Netd的方式进行了DNS处理。

最后让我们再看看getnameinfo在bionic的实现。

首先是4.2.2的代码,路径上getnameinfo会调用getnameinfo_inet,然后出现下面的代码:


#ifdef ANDROID_CHANGES
	struct hostent android_proxy_hostent;
	char android_proxy_buf[MAXDNAME];
	int hostnamelen = android_gethostbyaddr_proxy(android_proxy_buf,
			MAXDNAME, addr, afd->a_addrlen, afd->a_af);
	if (hostnamelen > 0) {
		hp = &android_proxy_hostent;
		hp->h_name = android_proxy_buf;
	} else if (!hostnamelen) {
		hp = NULL;
	} else {
		hp = gethostbyaddr(addr, afd->a_addrlen, afd->a_af);
	}
#else
	hp = gethostbyaddr(addr, afd->a_addrlen, afd->a_af);
#endif

具体如何处理根据ANDROID_CHANGES宏决定,如果定义了该宏,则通过Netd的方式进行。如果没有则直接调用gethostbyaddr,该函数后面会进行实际的dns解析。

再看看Android4.3中的实现:


int hostnamelen = android_gethostbyaddr_proxy(android_proxy_buf,
				MAXDNAME, addr, afd->a_addrlen, afd->a_af, iface, mark);


强行使用Netd的方式完成DNS的解析。Google在Android4.3后让DNS解析全部采用Netd代理的方式进行。


Netd是Network Daemon的缩写,Netd在Android中负责物理端口的网络操作相关的实现,如Bandwidth,NAT,PPP,soft-ap等。Netd为Framework隔离了底层网络接口的差异,提供了统一的调用接口,简化了整个网络逻辑的使用。

简单来说就是Android将监听/dev/socket/dnsproxyd,如果系统需要DNS解析服务,那么就需要打开dnsproxyd,然后安装一定的格式写入命令,然后监听等待目标回答。


在分析Netd前,必须知道Netd的权限和所属。




图中可以看出,两者的owner都是root,现在就好理解为什么说Android4.3后很多原来功能不需要root的原因了,系统现在采用代理的方式,让属于同group的用户可以借助Netd来干一些原来只有root能干的事情。

Android的初始化大致上可以分为三个部分:第一部分为启动Linux阶段,该部分包括bootloader加载kernel与kernel启动。第二部分为android的系统启动,入口为init程序,这部分包括启动service manager,启动Zygote,初始化Java世界等。第三部分为应用程序启动,主要为运行package manager。

与Netd相关联的是第二部分,也就是init进程。init进程在初始化中会处理/init.rc以及/init.<hardware>.rc两个初始化脚本,这些脚本决定了Android要启动哪些系统服务和执行哪些动作。

比如:


service servicemanager /system/bin/servicemanager  
    user system  
    critical  
    onrestart restart zygote  
    onrestart restart media  
  
service vold /system/bin/vold  
    socket vold stream 0660 root mount  
    ioprio be 2  
  
service netd /system/bin/netd  
    socket netd stream 0660 root system  
    socket dnsproxyd stream 0660 root inet  
  
service debuggerd /system/bin/debuggerd  
  
service ril-daemon /system/bin/rild  
    socket rild stream 660 root radio  
    socket rild-debug stream 660 radio system  
    user root  
    group radio cache inet misc audio sdcard_rw  

通过init.rc,我们可以看到netd和dnsproxy的权限和所属。直接从代码开始分析,netd源代码位于/system/netd/main.cpp,由C++编写。

从上面框架图中可以得知,netd由四个大部分组成,一部分是NetlinkManager,一个是CommandListener,然后是DnsProxyListener和MDnsSdListener。在main函数中netd依次初始化四个部件:


int main() {

    CommandListener *cl;
    NetlinkManager *nm;
    DnsProxyListener *dpl;
MDnsSdListener *mdnsl;

if (!(nm = NetlinkManager::Instance())) {
        ALOGE("Unable to create NetlinkManager");
        exit(1);
 };

…

cl = new CommandListener(rangeMap);
nm->setBroadcaster((SocketListener *) cl);

    if (nm->start()) {
        ALOGE("Unable to start NetlinkManager (%s)", strerror(errno));
        exit(1);
    }
setenv("ANDROID_DNS_MODE", "local", 1);
dpl = new DnsProxyListener(rangeMap);

if (dpl->startListener()) {
        ALOGE("Unable to start DnsProxyListener (%s)", strerror(errno));
        exit(1);
    }
    mdnsl = new MDnsSdListener();
    if (mdnsl->startListener()) {
        ALOGE("Unable to start MDnsSdListener (%s)", strerror(errno));
        exit(1);
    }
    if (cl->startListener()) {
        ALOGE("Unable to start CommandListener (%s)", strerror(errno));
        exit(1);
  }

代码都很简单,所以不需要赘述,只不过需要注意那句setenv(“ANDROID_DNS_MODE”,”local”,1),这句在后面有大作用。如果看过bionic代码的同学可能已经有所领悟了。

 

DnsProxyListener实际上就是pthread创造的一个线程,该线程仅仅监听dnsproxyd这个socket。

 

其他进程如何利用dnsproxyd来进行DNS解析呢?答案很简单,看到bionic中gethostbyname_internal中的这么一句:


if (fprintf(proxy, “gethostbyname %s %s %d”,
			iface == NULL ? “^” : iface,
			name == NULL ? “^” : name,
			af) < 0) {
		goto exit;
	}

其他进程打开dnsproxyd后(必须要同一个组),使用命令的方式来申请DNS解析。DnsProxyListener内部逻辑是很复杂的,这里没必要深究。现在看看gethostbyname这个命令如何解析。

Netd当中每一个命令对应一个类,该类继承自NetdCommand类。除此之外,还需要一个XXXXHandler的类来做实际命令的处理工作。XXXX是命令的名称,比如对于gethostbyname就有两个类:GetHostByNameCmd

GetHostByNameHandler。既然XXXXhandler中有两个公共方法,一个threadStart一个叫start。除此之外,还有个私有方法run。对命令的实际处理就是run方法实现的。


void DnsProxyListener::GetHostByNameHandler::run() {
    …
    struct hostent* hp;

    hp = android_gethostbynameforiface(mName, mAf, mIface ? mIface : iface, mMark);

    bool success = true;
    if (hp) {
        success = mClient->sendCode(ResponseCode::DnsProxyQueryResult) == 0;
        success &= sendhostent(mClient, hp);
    } else {
        success = mClient->sendBinaryMsg(ResponseCode::DnsProxyOperationFailed, NULL, 0) == 0;
    }
    if (!success) {
        ALOGW("GetHostByNameHandler: Error writing DNS result to client\n");
    }
    mClient->decRef();
}

关键的两行代码是android_gethostbynameforiface和sendBinaryMsg,后者是将前者得到的结果应答给请求DNS解析的进程。


struct hostent *
android_gethostbynameforiface(const char *name, int af, const char *iface, int mark)
{
	struct hostent *hp;
	res_state res = __res_get_state();

	if (res == NULL)
		return NULL;
	hp = gethostbyname_internal(name, af, res, iface, mark);
	__res_put_state(res);
	return hp;
}

关键仍然是调用了gethostbyname_internal。看到这里,看官们可能就会奇怪了,进程向Netd申请DNS请求的时候,调用的函数就是这个gethostbyname_internal,那么此时又调用一次岂不是递归了?这里就体现了创造Android工程师的智慧了。第一次调用gethostbyname_internal的时候是进程调用,并且这个时候ANDROID_DNS_MODE没有设置。第二次调用gethostbyname_internal的时候是Netd调用的,Netd的权限是root的,而且更关键的是前面Netd初始化的时候set了ANDROID_DNS_MODE,这两个不同的地方就影响了整个逻辑。

       除此之外,上方android_gethostbynameforiface函数中调用了__res_get_state函数。该函数获得了一个和线程相关的DNS服务器信息。去哪个local dns查询就看这个函数返回的res_thread结构了。这部分内容稍后进行分析。我们继续关注gethostbyname_internal的实现。


static struct hostent *
gethostbyname_internal(const char *name, int af, res_state res, const char *iface, int mark)
{
	const char *cache_mode = getenv("ANDROID_DNS_MODE");
	FILE* proxy = NULL;
	struct hostent *result = NULL;

	if (cache_mode != NULL && strcmp(cache_mode, "local") == 0) {
		res_setiface(res, iface);
		res_setmark(res, mark);
		return gethostbyname_internal_real(name, af, res);
	}

这一次判断cache_mode的语句将为true,此时进入gethostbyname_internal_real函数来处理DNS请求,后面就不用多分析了,有兴趣的童鞋可以继续跟随代码。后面就是构建DNS请求包和发送DNS请求了。






整个DNS解析的流程我们是清楚了,现在我们就要去想办法修改DNS服务器了。在android_gethostbynameforiface中,通过_res_thread_get函数获得__res_state。而在_res_thread_get函数中,用pthread_getspecific来获得与线程相关联的

_res_key。此时如果pthread_getspecific返回的是NULL说明该函数是第一次被调用,那么将会通过_res_thread_alloc分配内存然后进行初始化。初始化关键语句是res_ninit,该函数由会调用__res_vinit完成具体工作。

这里先给出__res_state结构的具体信息:


struct __res_state {
	char	iface[IF_NAMESIZE+1];
	int	retrans;	 	/* retransmission time interval */
	int	retry;			/* number of times to retransmit */
	u_int	options;		/* option flags - see below. */
	int	nscount;		/* number of name servers */
	struct sockaddr_in nsaddr_list[MAXNS];	/* address of name server */
#define	nsaddr	nsaddr_list[0]		/* for backward compatibility */
	u_short	id;			/* current message id */
	char	*dnsrch[MAXDNSRCH+1];	/* components of domain to search */
	char	defdname[256];		/* default domain (deprecated) */
	u_int	pfcode;			/* RES_PRF_ flags - see below. */
	unsigned ndots:4;		/* threshold for initial abs. query */
	unsigned nsort:4;		/* number of elements in sort_list[] */
	char	unused[3];
	struct {
		struct in_addr	addr;
		uint32_t	mask;
	} sort_list[MAXRESOLVSORT];
	res_send_qhook qhook;		/* query hook */
	res_send_rhook rhook;		/* response hook */
	int	res_h_errno;		/* last one set for this context */
	int _mark;          /* If non-0 SET_MARK to _mark on all request sockets */
	int	_vcsock;		/* PRIVATE: for res_send VC i/o */
	u_int	_flags;			/* PRIVATE: see below */
	u_int	_pad;			/* make _u 64 bit aligned */
	union {
		/* On an 32-bit arch this means 512b total. */
		char	pad[72 - 4*sizeof (int) - 2*sizeof (void *)];
		struct {
			uint16_t		nscount;
			uint16_t		nstimes[MAXNS];	/* ms. */
			int			nssocks[MAXNS];
			struct __res_state_ext *ext;	/* extention for IPv6 */
		} _ext;
	} _u;
        struct res_static   rstatic[1];
};

关键的成员是nsaddr_list,现在需要知道该成员何时何处被初始化了。答案是在前面的__res_vinit函数中,不过在深入之前必须要看看__res_ninit函数的注释部分。这一部分介绍了初始化的大概逻辑。


/*
 * Set up default settings.  If the configuration file exist, the values
 * there will have precedence.  Otherwise, the server address is set to
 * INADDR_ANY and the default domain name comes from the gethostname().
 *
 * An interrim version of this code (BIND 4.9, pre-4.4BSD) used 127.0.0.1
 * rather than INADDR_ANY ("0.0.0.0") as the default name server address
 * since it was noted that INADDR_ANY actually meant ``the first interface
 * you "ifconfig"'d at boot time'' and if this was a SLIP or PPP interface,
 * it had to be "up" in order for you to reach your own name server.  It
 * was later decided that since the recommended practice is to always
 * install local static routes through 127.0.0.1 for all your network
 * interfaces, that we could solve this problem without a code change.
 *
 * The configuration file should always be used, since it is the only way
 * to specify a default domain.  If you are running a server on your local
 * machine, you should say "nameserver 0.0.0.0" or "nameserver 127.0.0.1"
 * in the configuration file.
 *
 * Return 0 if completes successfully, -1 on error
 */


实际上这个所谓的配置文件正逐步被去掉,在__res_vinit后面有一段被#ifndefANDROID_CHANGES包围的代码,这段代码就是解析/etc/resolv.conf文件的。但是4.3后是#define了ANDROID_CHANGES的。所以ANDROID4.3以后再添加

resolv.conf是没有意义的了。

注释中说如果没有配置文件,则server address设为INADDR_ANY并且通过gethostname来获得默认domain name。也就是说,如果在wifi等环境下,DNS服务器都是自动获取的。



5. 对策与思路


Android4.3之前

在Android4.3以前,如果需要修改DNS服务器,有很多种方法,这些方法的实质就是向系统属性中添加“net.dns1”字段的信息。这些方法的前提条件都是获得root权限。具体方法有:

1.     在shell下,直接设置“net.dns1”等的系统属性。

2.     在init.rc脚本中,添加对“net.dns1”等系统属性的设置。

3.     在root权限下创建resovle.conf文件并添加相关name server信息。


Android4.3以后


在Android4.3以后,通过系统属性或者解析文件来手动修改DNS服务器已经是不可能了。主要有两种方法,一个是在NDK下面修改DNS解析逻辑,第二个是通过Android系统源代码修改相关逻辑,让Android4.3的新修改无效,然后重构Android。下面是一个老外基于NDK的修改方案,该方案需要以下权限:

1.     Root权限

2.     对/system文件夹有写权限

3.     能修改/etc/init.d

 

该方案重写了DnsProxyListener和bionic解析器逻辑,通过将/dev/socket/dnsproxyd改名然后自己替换它来达到目的。


/* 等待Netd启动 */
    while (do_wait && stat(SOCKPATH, &statbuf) < 0) {
        sleep(1);
    }
    /* 将其改名 */
    if (stat(SOCKPATH, &statbuf) == 0) {
        unlink(SOCKPATH ".bak");
        rename(SOCKPATH, SOCKPATH ".bak");
        restore_oldsock = 1;
    }

    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    …

    /* 移花接木 */
memset(&sock, 0, sizeof(sock));
    sock.sun_family = AF_UNIX;
    strcpy(sock.sun_path, SOCKPATH);

if (bind(sockfd, (struct sockaddr *)&sock, sizeof(sock)) < 0) 
…

if (chmod(SOCKPATH, 0660) < 0) 
…

/* 使用命令行或者缺省的IP做为DNS服务器,然后剩下的逻辑就是修改DnsProxyListener了 */
if (optind < argc)
        setup_resolver(argv[optind]);
    else
        setup_resolver("223.5.5.5");

代码逻辑比较容易理解,但是如何使用呢?很简单,使用adb将NDK生成的可执行文件拷贝到system目录下面,然后./dnstool –v 223.5.5.5&即可。








  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值