这个项目是北邮计算机网络课的课程设计,要求是实现一个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的打包,其中>表示大端序;H和L则分别表示两字节和四字节的无符号整型
压缩报文格式的解析
为了减小报文,域名系统使用一种压缩方法来消除报文中域名的重复。使用这种方法,后面重复出现的域名或者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等等模块,正是这些前人造好的“轮子”给我带来了很大的便利
附件: