Android4.3前后DNS解析简单研究
阅读原文时间:2021年04月20日阅读:1

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..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&即可。

老外的github: https://github.com/cernekee/dnsproxy2

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器