在Android之DNS查询结果缓存中有提到一个DNS查询时的行为:==如果当前已经有一个相同的查询发出,那么后来的查询请求实际上会block,等待前一次的查询返回,如果成功那么一起返回,失败则后面的请求还会及其发起。==这篇笔记就来看看到底是如何判断两个DNS查询请求是不是完全相同的。
之所以要分析这个过程,是因为在实际问题定位过程中,如果看到两个对相同域名的DNS查询请求相隔不远,总会有疑问,难道是这种机制没生效?
1. DNS请求包格式
关于报文格式,可以参考这篇博客,里面有详细的介绍。为了下文方便对照,这里列出和查询相关的几点。
1.1 报文整体格式
1.1.1 会话标识
该字段2个字节,是DNS报文的标识,对于请求报文和其对应的应答报文,这个字段是相同的,通过它可以区分DNS应答报文是哪个请求的响应
1.1.2 标志
该字段2个字节,有如下标记:
flag | 说明 |
---|---|
QR(1bit) | 查询/响应标志,0为查询,1为响应 |
opcode(4bit) | 0表示标准查询,1表示反向查询,2表示服务器状态请求 |
AA(1bit) | 表示授权回答 |
TC(1bit) | 表示响应报文是可截断的 |
RD(1bit) | 表示期望递归 |
RA(1bit) | 表示可用递归 |
rcode(4bit) | 表示返回码,0表示没有差错,3表示名字差错,2表示服务器错误(Server Failure) |
1.1.3 数量字段
该字段共8个字节。Questions、Answer RRs、Authority RRs、Additional RRs 分别表示后面的四个区域的数目。Questions表示查询问题区域节的数量,Answers表示回答区域的数量,Authoritative namesversers表示授权区域的数量,Additional recoreds表示附加区域的数量。
1.2 查询问题区域格式
对于这篇笔记,我们只关注查询问题区域的格式,因为判断两个请求是否相同只和查询报文相关。
1.2.1 Name
即域名,==长度不固定并且没有填充字节。==但是域名并不是直接表示的,而是通过一种压缩方式记录的,方式如下:
1.2.2 查询类型type
主要有如下几种类型,经常遇到的有A、AAAA和PTR。
类型 | 助记符 | 说明 |
---|---|---|
1 | A | 由域名获得IPv4地址 |
2 | NS | 查询域名服务器 |
5 | CNAME | 查询规范名称 |
6 | SOA | 开始授权 |
11 | WKS | 熟知服务 |
12 | PTR | 把IP地址转换成域名 |
13 | HINFO | 主机信息 |
15 | MX | 邮件交换 |
28 | AAAA | 由域名获得IPv6地址 |
252 | AXFR | 传送整个区的请求 |
255 | ANY | 对所有记录的请求 |
1.2.3 查询类class
对于Internet,就是1.
2. entry_init_key()
在笔记Android之DNS查询结果缓存中中,我们有看到,查询是否有相同的hash值,即Entry.hash是否相同,所以问题的关键是该hash值是如何计算出来的。
实际上,hash值就是用entry_init_key()计算出来的,每个查询数据包都会计算一个hash值。
//一个辅助数据结构,用法见下面代码
typedef struct {
const uint8_t* base;
const uint8_t* end;
const uint8_t* cursor;
} DnsPacket;
/* initialize an Entry as a search key, this also checks the input query packet
* returns 1 on success, or 0 in case of unsupported/malformed data */
static int entry_init_key( Entry* e, const void* query, int querylen )
{
DnsPacket pack[1];
//先将Entry的所有内容清零
memset(e, 0, sizeof(*e));
e->query = query;
e->querylen = querylen;
//计算hash值,重点看这个
e->hash = entry_hash(e);
_dnsPacket_init(pack, query, querylen);
return _dnsPacket_checkQuery(pack);
}
如注释所述,entry_init_key()实际上干了两件事:
- 为查询数据包计算一个key值,即hash值;
- 检查DNS查询包是否ok
这里我们只关注其key值是如何计算的,即entry_hash()的具体实现。
2.1 entry_hash()
/* compute the hash of a given entry, this is a hash of most
* data in the query (key) */
static unsigned entry_hash( const Entry* e )
{
DnsPacket pack[1];
//pack的三个指针指向数据包的开始和结束位置
_dnsPacket_init(pack, e->query, e->querylen);
//计算查询数据包的hash值
return _dnsPacket_hashQuery(pack);
}
static void _dnsPacket_init( DnsPacket* packet, const uint8_t* buff, int bufflen )
{
packet->base = buff;
packet->end = buff + bufflen;
packet->cursor = buff;
}
2.2 _dnsPacket_hashQuery()
#define FNV_MULT 16777619U
static unsigned _dnsPacket_hashQuery( DnsPacket* packet )
{
unsigned hash = FNV_BASIS;
int count;
//rewind即回绕,让cusor指向查询数据包的开始位置
_dnsPacket_rewind(packet);
/* we ignore the TC bit for reasons explained in
* _dnsPacket_checkQuery().
*
* however we hash the RD bit to differentiate
* between answers for recursive and non-recursive
* queries.
*/
//数据包的前四个字节中只考虑RD标志位
hash = hash*FNV_MULT ^ (packet->base[2] & 1);
//跳过数据包的前四个字节
_dnsPacket_skip(packet, 4);
/* read QDCOUNT */
//读取问题查询数并且cusor指针前移两个字节
count = _dnsPacket_readInt16(packet);
//跳过其余三个区域的数目字段,即他们和hash值没有关系
_dnsPacket_skip(packet, 6);
/* hash QDCOUNT QRs */
for ( ; count > 0; count-- )
hash = _dnsPacket_hashQR(packet, hash);
return hash;
}
//从这里可以看出DNS数据包内容是大端表示
static int _dnsPacket_readInt16( DnsPacket* packet )
{
const uint8_t* p = packet->cursor;
if (p+2 > packet->end)
return -1;
packet->cursor = p+2;
return (p[0]<< 8) | p[1];
}
2.3 _dnsPacket_hashQR()
static unsigned _dnsPacket_hashQR( DnsPacket* packet, unsigned hash )
{
//对问题区域的name字段进行hash计算
hash = _dnsPacket_hashQName(packet, hash);
//对问题区域的type和class字段进行hash计算
hash = _dnsPacket_hashBytes(packet, 4, hash); /* TYPE and CLASS */
return hash;
}
static unsigned _dnsPacket_hashQName( DnsPacket* packet, unsigned hash )
{
const uint8_t* p = packet->cursor;
const uint8_t* end = packet->end;
for (;;) {
int c;
//到了数据包的末尾
if (p >= end) { /* should not happen */
XLOG("%s: INTERNAL_ERROR: read-overflow !!\n", __FUNCTION__);
break;
}
//取count字段,一个字节
c = *p++;
//单个域名字段超过64个字符非法
if (c == 0)
break;
if (c >= 64) {
XLOG("%s: INTERNAL_ERROR: malformed domain !!\n", __FUNCTION__);
break;
}
//长度非法
if (p + c >= end) {
XLOG("%s: INTERNAL_ERROR: simple label read-overflow !!\n",
__FUNCTION__);
break;
}
//域名中的每个非"."字符都参与到hash值的计算
while (c > 0) {
hash = hash*FNV_MULT ^ *p++;
c -= 1;
}
}
//指针会迁移到name字段的末尾,即type字段的开始
packet->cursor = p;
return hash;
}
//对numBytes字节个数据进行hash计算,并且迁移numBytes个指针
static unsigned _dnsPacket_hashBytes( DnsPacket* packet, int numBytes, unsigned hash )
{
const uint8_t* p = packet->cursor;
const uint8_t* end = packet->end;
while (numBytes > 0 && p < end) {
hash = hash*FNV_MULT ^ *p++;
}
packet->cursor = p;
return hash;
}
3. 总结
从上面的代码分析过程来看,我们抛开异或运算本身的含义外,我们可以看到DNS请求包中,最终会影响hash值的字段只有如下几个:
- flag字段中的RD标志位
- 问题查询区域的name字段、type字段和class字段
那么也就是说,只要两个请求中的这些字段相同,那么这就是两个完全相同的请求。