其实我选题是排除法选择的(因为什么都不会),以下是心理过程:
- MiniNginx?好玩,打开看看;CPP?打扰了,下一位
- OrangeDB?有意思,打开看看;使用底层语言?打扰了,下一位
- 迷宫?因缺思厅,打开看看;疑似有点思路了(用docker控制容器io+python交互);写不出来迷宫生成,下一位
- DNS协议?
不选也没得选了;做着做着貌似还挺好玩,遂确定了选题
由于网上的文章要不过于复杂,充斥着难以理解的名词、要不过于简单,只是拿来糊弄小孩的,所以我选择先实践,后面遇到问题再相应搜索
经过一些抓包,我理解了部分dns请求的组成部分
TCP请求在头部增添了2字节的总长度标签
随机,用来区分多个结果
解码为二进制后用来标记请求,具体各个标记的意义如下表
具体需要关注的有:
1. Response:0表示查询数据包,1表示回复
2. Recursion Desired:是否需要服务器代为完成递归查询,若为1则服务器只返回最终结果;为0则只返回下一级ns服务器/结果
3. Reply code:回复数据包所包含的信息,0000表示成功,0011表示查无此域
1. Questions:问题数量(通常为1)
2. Answer RRs:标准应答数量
3. Authority RRs:权威应答数量
4. Additional RRs:额外应答数量
对于每个问题,包含以下三个参数
1. Name:域名,不定长
2. Type:查询记录的种类,2字节
3. Class:记录类别(在internet上均为1),2字节
其中,对于域名,有如下分割方法:
1. 以.为分界,将域名拆分为不同级别
2. 在每个级别前加上1字节的长度标记
3. 在结束后以长度标记00作为末尾
应答组成部分有:域名、查询类型、类别、TTL(有效时间)、数据长度(rdlength)、数据体。
其中前三个部分编码方式与查询类似
1. 转码为2进制后最高两位为11
2. 其余14为二进制转为10进制后是指向数据的起始位置(距离数据包开头的字节数)
3. 可能出现指针指向指针的情况(如记录1指向查询1,而记录2指向记录1)
此时需要特殊处理
TTL是4字节的数据,代表缓存有效期最长的秒数
- 对于A记录,有4*1字节的数据,分别代表记录IP的ABCD段,以
.
分割,无需特殊处理 - 对于TXT记录,可能出现一个记录中有多个tag的情况,此时需要依据总长度(rdlength)以及每个tag前1字节的长度切割数据
- 对于AAAA记录,有8*2字节的数据,分别代表ipv6一个段,以
:
分割,注意ipv6有化简形式(若整段为0则省略,否则省略一段开头的0) - 对于CNAME,编码方式与查询中域名相同
- 对于MX,存在一个2字节的优先级字段,代表该服务器在收信时的优先级,随后跟随服务器域名,编码方式相同
- 对于NS,编码方式与查询中域名相同
需要注意,查询A AAAA MX TXT时,有可能返回CNAME记录(代表此域名为CNAME类型)
在进行迭代查询时,服务器会将下一级名词服务器的地址包含在权威应答部分中。其数据格式与数据包中ns类型相同
一些dns服务器会在查询无果时在此部分插入SOA记录
包含edns等信息,不做处理
得益于Python优秀的封装库,我使用最常规的dict存储查询信息。key为一个包含(域名,查询类型)的tuple,value为记录的list
对于持久化保存,我选择使用pickle自动dump整个dict,并且能很方便的load
使用argparse库处理命令行参数,方便快捷。
值得一提的部分:在服务器收到请求后将数据以yield返回,保证可以连续处理 但间接导致的问题是无法扩展多线程,可能在后续需要将处理部分改写为新建进程