这个项目是北邮计算机网络课的课程设计,要求是实现一个DNS中继服务器,可以根据本地DNS表解析地址、拦截黑名单域名、中继查询本地没有记录的域名。设计的重点都放在DNS协议和UDP协议上了,所以本地的DNS记录是文件读入而没有使用数据库,中继查询到的结果也仅仅做了转发而没有缓存到本地,这些都是可以继续优化的点。附上详细的设计报告和源码供学弟学妹们参考。

系统功能设计

本次课程设计要求实现一个DNS中继服务器,读入本地的DNS资源记录文件,当客户端查询域名时,服务器进行检索,实现以下三种情况:

  • 普通解析:当解析地址为普通ip地址时,直接向客户端返回该地址
  • 拦截功能:当解析地址为0.0.0.0,则向客户端返回“域名不存在”的报错消息
  • 中继功能:当本地未检索到域名时,则向设定的域名服务器发出查询,正确接收到结果后,再将结果返回给客户端

程序允许用户自定义服务器地址与DNS资源记录文件路径,可以本地处理A记录解析与别名解析,对于其他类型的查询将通过中继的方式完成查询,并将查询内容与结果输出到debug信息中

程序通过以下指令运行:

python .\DNSRelay.py [-h] [-d {0,1,2}] [-s SERVER] [-f FILENAME]
-h 帮助说明
-d debug等级 可选值0,1,2 默认为`0`
-s 自定义域名服务器地址 默认为1.2.4.8
-f 资源文件地址 默认为同路径下dnsrelay.csv

模块划分

DNS中继服务程序分为服务处理模块与报文解析模块两个主体部分,其中服务处理模块分为本地处理模块与中继转发模块;报文解析模块分为解包模块和组包模块

graph TD
A[DNS中继服务器]-->B[服务处理]
A-->C[报文解析]
B-->D[本地处理]
B-->E[中继转发]
C-->F[组包模块]
C-->G[解包模块]

软件流程图

graph TD
s(开始)-->ini[初始化]
ini-->recv>接收消息]

recv-->unpack[解析报文]
unpack-->QR{报文类型}
QR--回答报文-->restoreID[还原ID]
restoreID-->returnC>发回客户端]
returnC-->recv

QR--查询报文-->search{本地检索}
search--找到记录-->packAns[生成回答包]
packAns-->returnC

search--未找到-->transID[修改ID]
transID-->askSvr[中继查询]
askSvr-->recv

测试结果

执行指令,开启中继服务程序

python DNSRelay.py -d 2

本地查询功能

执行指令

nslookup gateway.bupt 127.0.0.1

查询结果

程序输出

查询CNAME类型

nslookup -qt=CNAME baidu.com 127.0.0.1

查询结果

程序输出

本地拦截功能

本地添加一条资源记录

taobao.com A 0.0.0.0

执行指令

nslookup taobao.com 127.0.0.1 

查询结果

中继查询功能

执行指令

nalookup www.zachxia.cn 127.0.0.1

查询结果

程序输出

问题与解决方案

bytes对象的转换

在python3中,bytes对象与string对象是完全分开的,网络数据传输、文件等都是bytes数据结构。bytes和字符串数组的结构和操作方法类似,也是不可变的序列对象

作为DNS服务器,程序收发的数据自然都是bytes类型,为了方便地操作数据,我们引入了struct模块,通过其pack()unpack()方法完成组包与解包

比如通过以下代码

struct.pack('>HHHLH', self.RName, self.RType, self.RClass, self.ttl, self.RDLength)

就可以完成一条DNS Answer的打包,其中>表示大端序;HL则分别表示两字节和四字节的无符号整型

压缩报文格式的解析

为了减小报文,域名系统使用一种压缩方法来消除报文中域名的重复。使用这种方法,后面重复出现的域名或者labels被替换为指向之前出现位置的指针

指针占用2个字节,格式如下:

 0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1  1|                OFFSET                   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

于是通过d >= 0xC0确定指针类型,通过d = int.from_bytes(tmpData[i:i+2], byteorder = 'big') - 0xC000计算偏移量。

解析域名的完整代码如下:

while True:
    d = tmpData[i]
    if d == 0:
        self.RData = self.RData[1:]
        break
    elif d >= 0xC0:
        #指针类型
        d = int.from_bytes(tmpData[i:i+2], byteorder = 'big') - 0xC000
        tmpData = self.data[d:]
        i = 0
        continue
    elif d < 32:
        self.RData += '.'
    else:
        self.RData += chr(d)
    i += 1

多客户端请求的处理

由于同一服务器可能在同一时间内接收多个不同客户端的查询请求,中继查询的报文需要在接收到结果后返回给对应的客户端,同时还要对超时的报文做丢弃处理,于是需要记录的有:

  • 原始查询报文的ID
  • 查询客户端的地址
  • 接收报文的时间

于是在程序中维护一个ID转换字典IDdict,每当生成一个中继查询,都要通过IDdict[newID] = orgID, self.client_address, time.time()记录一条转换项;每收到一条回答报文,都要通过orgID, clientAddr, recvTime = IDdict.pop(newID)弹出一条记录项

转换ID通过全局统一分配方式生成,每发送一条中继查询报,nextID自增1

心得体会

本次计算机网络课程设计是我们第二次接触socket编程,上一次还是在上学期的C++论坛程序网络版,由于当时的通信协议完全由自己指定,现在回想起来确实有很多不合理的地方

而本次的实验完全在DNS协议下进行,因此我花了相当一部分时间进行文档的阅读和Wireshark抓包分析。基本弄清了DNS的报文结构后,后面的编码环节少走了很多弯路

涉及socket编程的部分,在python下有非常简单易用的socketserver,因此网络部份很顺利的完成。此外我还引入了负责处理参数的argparse、处理字节流的struct等等模块,正是这些前人造好的“轮子”给我带来了很大的便利

附件