当前位置: 首页 > 科技观察

从Chrome源码看DNS解析过程

时间:2023-03-13 01:25:59 科技观察

DNS解析就是把域名解析成对应的IP地址,因为广域网上的路由器需要知道IP地址才能知道把报文发给谁。DNS是DomainNameSystem的缩写,是一种协议,在RFC1035中有具体的描述。具体过程如下图所示:这个过程看似简单,但是有几个问题:(1)浏览器如何知道DNS解析服务器吗,比如上图中的8.8.8.8?(2)一个域名可以解析成多个IP地址吗?如果只有一个IP地址,并发量大的时候服务器会不会炸?(3)主机绑定域名后,是否需要不使用域名直接解析本地主机指定的IP地址?(4)域名解析的有效时间是多长时间,即同一个域名再次解析需要多长时间?(5)什么是域名解析的A记录、AAAA记录、CNAME记录?事实上,域名解析与Chrome没有直接关系。即便是最简单的curl命令也需要解析域名,不过我们可以通过Chrome源码来看一下这个过程,回答上面的问题。首先第一个问题,浏览器是怎么知道DNS解析服务器的呢?在本机的网络设置中可以看到当前的DNS服务器IP,比如我的电脑:这两个DNS服务器是我家连接的某条宽带Provided:一般宽带服务商都会提供DNS服务器。谷歌还为公众提供了两个免费的DNS服务,分别是8.8.8.8和8.8.4.4。这两个IP地址是为了方便记忆而取的。当你的DNS服务不灵时,你可以尝试改成这两个。接入网络的设备如何获取这些IP地址?它是通过动态主机配置协议(DHCP)实现的。当设备连接到路由器时,路由器通过DHCP为它分配一个IP地址,并告诉它DNS服务器。路由器的DHCP设置如下:这个过程可以用wireshark抓包观察:当我的电脑连接wifi时,会发送一个DHCPRequest广播。路由器收到这个广播后,就会给我的电脑分配一个IP地址,并通知DNS服务器。这时候系统就有了DNS服务器。Chrome调用系统函数res_ninit(Linux)获取系统的DNS服务器。此函数通过读取文件/etc/resolver.conf获取DNS:##MacOSXNotice##Thisfileisnotusedbythehostnameandaddressoresolution#ortheDNSqueryroutingmechanismsusedbymostprocesseson#thisMacOSXsystem.##Thisfileisautomaticallygenerated.#searchDHCPHOSTnameserver59.108.61.61nameserver219.232.48.61搜索选项的功能是当一个域名无法解析,会尝试加上相应的后缀,比如pinghello,如果解析不了,就分别pinghello.DHCP/hello.HOST,结果***无法解析。Chrome启动时会根据不同的操作系统获取DNS服务器配置,然后放入DNSConfig的nameservers中://Listofnameserveraddresses.std::vectornameservers;Chrome还会监控网络变化以同步更改配置。然后用这个nameservers列表初始化一个socketpool,socketpool,用来发送请求。当需要做域名解析时,会从socket池中取出一个socket,传入想要的server_index。初始化时为0,即取第一个DNS服务IP地址。一旦两次解析请求失败,则server_index+1使用下一个DNS服务。unsignedserver_index=(first_server_index_+attempt_number)%config.nameservers.size();//跳过knownfailedservers.//***attemptsnumber为2,在构造DnsConfig时设置server_index=session_->NextGoodServerIndex(server_index);如果所有的nameservers都失败了,那么它会取最早失败的nameserver。Chrome启动时,除了读取DNS服务器外,还会读取并解析hosts文件,并将其放入DNSConfig的hosts属性中。是一个Ha希图://ParsedresultsofaHostsfile.////虽然Hostsfiles将IP地址映射到一个listofdomainnames,forname//resolutionthedesiredmappingdirectionis:domainnametoIPaddress.//WhenparsingHosts,weapplythe"firsthit"ruleasWindowsandglibcdo.//WithaHostsfileof://300.300.302.7badip.300/local1host/local1host0.1localhost//10.0.0.1localhost//期望的localhost解析为127.0.0.1.usingDnsHosts=std::unordered_map;hosts文件在linux系统的/etc/hosts中:constbase::FilePath::CharTypekFilePathHosts[]=FILE_PATH_LITERAL("/etc/hosts");读取这个文件没什么技巧,需要逐行处理,判断一些不合法的情况,比如上面代码的注释。这样,DNSConfig中就有两个配置,一个是hosts,一个是nameservers,DNSConfig组合成DNSSession,它们的组合关系如下图所示:resolver是负责解析的驱动类,它组合了一个client,而client创建了一个session,session层有很大的作用管理server_index和socketpool,比如分配sockets等,session初始化config,config用于读取本地绑定的hosts和nameservers这两个配置。这些层中的每一层都有自己的职责。解析器有一个重要的功能,它结合一个作业来创建一个任务队列。resolver还结合了一个Hostcache,它是一个缓存,用于存储分析结果。如果缓存已满,则无需解析。这个过程是这样的。对外调用rosolver提供的HostResolverImpl::Resolve接口。这个接口会先判断是否可以本地处理:intnet_error=ERR_UNEXPECTED;if(ServeFromCache(*key,info,&net_error,addresses,allow_stale,stale_info)){source_net_log.AddEvent(NetLogEventType::HOST_RESOLVER_IMPL_CACHE_HIT,addresses->CreateNetLogCallback());//|ServeFromCache()|willset|*stale_info|asneeded.returnnet_error;}//TODO(szym):Donotdothisifnsswitch.confinstructsnotto.//http://crbug.com/117655if(ServeFromHosts(*key,info,addresses)){source_net_log。AddEvent(NetLogEventType::HOST_RESOLVER_IMPL_HOSTS_HIT,addresses->CreateNetLogCallback());MakeNotStale(stale_info);returnOK;}returnERR_DNS_CACHE_MISS;以上代码首先调用serveFromCache检查缓存中是否有。***,如果不是***,返回CACHE_MISS标志位。如果返回值不等于CACHE_MISS,则直接返回:}Otherwise创建一个job,看看能不能马上执行。如果作业队列太多,则将其添加到作业队列的后面,并传递一个成功的回调处理程序。所以这里和我们的认知基本一致,先检查是否有缓存,再检查是否有hosts,如果没有,再查询。查询缓存时,如果缓存过时,则返回null,判断是否过时的标准如下:boolis_stale()const{returnnetwork_changes>0||expired_by>=base::TimeDelta();}是网络发生了变化,还是expired_by大于0,就认为是过时的缓存。这个时间差就是当前时间减去当前缓存的过期时间:stale.expired_by=now-expires_;而过期时间是初始化时使用now+ttl的值,最后一个请求解析时返回这个ttlttl:uint32_tttl_sec=std::numeric_limits::max();ttl_sec=std::min(ttl_sec,record.ttl);*ttl=base::TimeDelta::FromSeconds(ttl_sec);上面的代码做了一个Anti-spill的处理。在wireshark的dns响应中,可以直观的看到这个ttl:当前域名的TTL值为600s,也就是10分钟。这个可以由购买域名的提供商来设置:另外可以看到记录类型是A,A是什么,如下图:添加解析的时候可以看到A是解析域名解析为IPv4地址,AAAA解析为IPv6地址,CNAME解析为其他域名。使用CNAME的好处是当很多其他域名指向一个CNAME时,当你需要更改IP地址时,只需要更改这个CNAME的地址,那么其他的东西也会生效,但是你需要做的第二次分析。如果域名无法在本地解析,Chrome将发送请求。操作系统提供了一个名为getaddrinfo的系统函数用于域名解析,但是Chrome并没有使用它,而是自己实现了一个DNS客户端,包括封装DNS请求报文和解析DNS响应报文。这可能是因为灵活性会更大,例如,Chrome可以决定如何使用名称服务器、顺序和失败尝试次数。在解析器的startJob中开始分析。获取下一个queryId,然后构建一个query,然后构建一个DnsUDPAttempt,然后执行它的start,因为DNS客户端查询使用的是UDP包(二级域名服务器使用TCP查询一级域名服务器):uint16_tid=session_->NextQueryId();std::unique_ptrquery;query.reset(newDnsQuery(id,qnames_.front(),qtype_,opt_rdata_));DnsUDPAttempt*attempt=newDnsUDPAttempt(server_index,std::move(lease),std::move(query));intrv=attempt->Start(base::Bind(&DnsTransactionImpl::OnUdpAttemptComplete,base::Unretained(this),attempt_number,base::TimeTicks::Now()));具体的解析过程分为几个步骤。代码组织是这样的。执行顺序由一个状态决定:intrv=result;do{//初始状态为STATE_SEND_QUERYStatestate=next_state_;next_state_=STATE_NONE;switch(state){caseSTATE_SEND_QUERY:rv=DoSendQuery();break;caseSTATE_SEND_QUERY_COMPLETE:rv=DoSendQueryComplete(rv);break;caseSTATE_READ_RESPONSE:rv=DoReadResponse();break;caseSTATE_READ_RESPONSE_COMPLETE:rv=DoReadResponseComplete(rv);break;default:NOTREACHED();break;}}while(rv!=ERR_IO_PENDING&&next_state_!=STATE_NONE);在第一个案例执行后,状态变为第二个案例的状态,在第二种情况的执行函数中,改为第三种,以此类推,直到在while循环中变成STATE_DONE,或者ERR状态结束。用这段代码组织当前交易很有趣。***解析成功后,会将结果放入缓存:if(did_complete){resolver_->CacheResult(key_,entry,ttl);RecordJobHistograms(entry.error());}然后生成一个addressList和传递给对应的回调,因为DNS解析可能会返回多个结果,如下图:这里我们不使用Chrome打印结果,都是直接从wireshark的输出看的,因为加个打印比较麻烦功能,直接查看wireshark的输出更直观,节省时间。本文简要介绍了DNS解析的过程以及DNS的一些相关概念。相信到这里,你应该能够回答上面提出的问题了。一般来说,客户端向域名解析服务器发起查询,然后服务器返回响应。当设备连接到网络时,DNS服务器名称服务器由路由器通过DHCP发送到设备。Chrome将按照名称服务器的顺序发起查询并缓存结果。有效时间以ttl为准。有效期内的两次查询将直接使用缓存。DNS解析结果有几种,最常见的有A记录和CNAME记录,A记录表示结果是一个IP地址,CNAME表示结果是另一个域名。本文不作深入和详细介绍,但应该涵盖了核心概念和逻辑流程。【本文为专栏作者“人人网FED”原创稿件,转载请联系原作者获得授权】点此查看更多本作者好文