Android DNS之查询数据包的hash值

本文分析了Android DNS查询时如何通过请求包的hash值来判断两个请求是否相同。关键在于DNS报文的会话标识、标志、数量字段以及查询问题区域的Name、Type和Class。entry_init_key()函数用于计算hash值,主要涉及entry_hash()、_dnsPacket_hashQuery()和_dnsPacket_hashQR()。当请求的RD标志位、Name、Type和Class一致时,认为是相同的请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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。

类型助记符说明
1A由域名获得IPv4地址
2NS查询域名服务器
5CNAME查询规范名称
6SOA开始授权
11WKS熟知服务
12PTR把IP地址转换成域名
13HINFO主机信息
15MX邮件交换
28AAAA由域名获得IPv6地址
252AXFR传送整个区的请求
255ANY对所有记录的请求

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()实际上干了两件事:

  1. 为查询数据包计算一个key值,即hash值;
  2. 检查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字段

那么也就是说,只要两个请求中的这些字段相同,那么这就是两个完全相同的请求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值