在Android4.3以前,如果系统需要备份/恢复,防火墙以及DNS解析管理,Linux内核微调等,是需要ROOT权限才能进行的。在Android4.3中,Google修改了这一策略,Google向用户提供API和扩展来完成这些事情。其中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绑定到一个随机端口以增强安全性。
我们从下面小例子开始分析公共流程中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
(&ss), size, buf, sizeof(buf), NULL, 0, flags);
不管是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服务器都是自动获取的。
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