简介:计算机网络课程设计中的DNS服务器实验资料包,面向北邮大二下学生及计算机网络初学者,用于完成DNS服务器搭建与域名解析实验。资料围绕域名系统展开,详细介绍DNS分布式数据库、根域到子域的层次结构,以及A、AAAA、MX、CNAME、PTR等常见记录类型;同时梳理递归查询与迭代查询流程、权威服务器与缓存服务器的职责分工,并说明基于UDP 53端口的DNS报文交互和C语言套接字编程要点。资源共4个文件,含2个txt说明文档、1个C源文件和1个头文件,压缩包仅9KB,结构精简但覆盖实验核心。已有166人学习查看,适合作为课程设计参考或计网实验入门。读者可借助其中的配置示例、代码框架和DNS relay设置,快速理解DNS查询处理逻辑,也可结合dig或nslookup完成调试验证,为后续网络编程与服务器开发打下基础。
1. 计网课设里的 DNS 服务器实验:为什么搭起来不难,讲清楚却要花心思
学期末总会有一批类似“BUPT大二下,计网课程设计,DNS服务器实验.zip”的压缩包在同学之间传来传去,里面通常是报告模板、答辩 PPT 和一份转发再转发的代码。很多人以为把 zip 交上去就完事,结果答辩时被问一句“你搭的这个服务器,收到一个查询报文后到底做了什么”就卡住。这个课设的核心不是“装一个 DNS 软件”,而是要求你亲手搭建一条完整的域名解析链路,并且能讲清楚权威解析、反向解析、转发和缓存各自干了什么。这篇文章按课设的实际评分逻辑,把 DNS 服务器的部署、配置、代码实现和现场演示技巧拆开讲,适合正在赶课设、想拿高分而不是只求过检的同学。
2. DNS 报文与解析流程:实验开始前要讲清楚的三件事
答辩老师最爱问的第一个问题通常是“一次 DNS 查询是怎么走的”。这个问题答好了,后面所有配置命令都只是验证。答不好,哪怕你 dig 结果全对,分数也会打折。所以先从协议链路讲起,再落到报文头部和功能清单,最后给出一套适合课设的拓扑。
2.1 一次浏览器访问背后的递归与迭代
在浏览器地址栏里输入www.test.lab并回车时,第一步其实不是发 DNS 查询。浏览器先查本地 hosts 文件——Windows 在C:\Windows\System32\drivers\etc\hosts,Linux 在/etc/hosts——没命中才会往网卡配置里写的 DNS 服务器地址发包。这个 DNS 服务器地址通常由 DHCP 下发给客户端,它扮演的角色是“递归解析器”,替客户端把整个查询链路走完。
如果这个递归解析器收到的域名恰好属于自己负责的 zone,它就直接查自己的 zone 文件返回结果,这个行为叫“权威回答”。如果域名在自己的 zone 里找不到,它就根据配置里的forwarders把查询转给上一级 DNS,拿到结果后缓存一段时间再返回给客户端。很多课设失败的原因,就是把“递归服务器”和“权威服务器”这两个角色混在一起讨论。
我在课设里一般画这样一条链路作为答辩图:
浏览器 → 客户端 resolv.conf 里的 DNS → 你的 DNS 服务器(named) ├→ zone 文件命中:返回权威答案,AA=1 └→ 未命中:转发给公网 DNS,缓存后返回这条链路能回答“你的服务器什么时候是权威的,什么时候只是中转”这个高频问题。记住一个关键点:只有设置了 zone 的域名,你的服务器才叫权威;其他域名它只是缓存加转发。
2.2 报文头部的 12 个字节:QR、Opcode、AA、RD、RA
DNS 报文头部固定是 12 字节。前两个字节是 Transaction ID,客户端用它来匹配请求和响应;接下来的 Flags 字段里,课设必考的几个位是:
| 位名 | 含义 | 课设里的判断方法 |
|---|---|---|
| QR | 0 表示查询,1 表示响应 | dig 响应包中为 1 |
| Opcode | 0 表示标准查询 | 通常是 0,不用深究 |
| AA | Authoritative Answer,权威回答 | 用dig @127.0.0.1 www.test.lab看aa标志 |
| TC | Truncated,报文被截断 | 出现时表示响应超过 512 字节 |
| RD | Recursion Desired,期望递归 | 客户端发出的查询里为 1 |
| RA | Recursion Available,服务器支持递归 | 你的 named 开启 recursion 后为 1 |
QNAME 的编码也是一个考点。www.test.lab在报文里不是直接 ASCII 字符串,而是每个标签前面加一个长度字节:03 77 77 77 04 74 65 73 74 03 6c 61 62 00。末尾的00表示根。用手抓包看到这段十六进制时,能指出来说明你对协议字段是真理解,而不是只会跑命令。答辩时能说出“AA=1 说明这台服务器对 test.lab 拥有权威回答,RA=1 说明它同时支持递归”,这比贴一堆配置文件截图有用得多。
2.3 正向、反向、转发、缓存:功能清单与实验拓扑
按课设评分表拆开看,DNS 服务器实验至少要覆盖 5 个功能点:
- 安装并启动 DNS 服务软件,能响应
dig查询; - 配置正向 zone,让
www.test.lab解析到指定 IP; - 配置反向 zone,让指定 IP 能解析回域名;
- 配置转发或递归,让服务器也能解析外部域名;
- 用抓包工具或
dig输出验证上面每一项。
实验拓扑我建议用 VMware 开两台虚拟机,不要图省事在一台机器上又当客户端又当服务器。一台装 Ubuntu Server 或 UOS Server 20,IP 固定为192.168.50.10,专门跑 named 服务;另一台做客户端,IP 设为192.168.50.20,网卡 DNS 指向192.168.50.10。网络模式选 NAT 或仅主机都行,但别用桥接。桥接模式下宿舍路由器的 DNS 会干扰测试结果,你可能查到一个“来自上级路由器的缓存回答”,导致反复怀疑自己配置写错了。
3. 用 BIND9 在 Ubuntu 上搭建权威 DNS 服务器:named.conf 到 zone 文件的完整配置
选 BIND9 而不是其他方案的理由很简单:它是 Linux 上最主流的权威 DNS 实现,配置文件结构清晰,每一项都能对上课程设计评分表的检查点。Ubuntu 和 UOS Server 20 的安装路径几乎一样,后面我会把差异指出来。
3.1 安装 bind9 后的最小 named.conf.local
安装命令是sudo apt install bind9,装完后目录结构如下:/etc/bind/named.conf是主入口,它会 includenamed.conf.options、named.conf.local和named.conf.default-zones。要改的文件只有两个:options 管全局行为,local 管 zone 声明。
先编辑/etc/bind/named.conf.local,把自定义 zone 加进去:
// /etc/bind/named.conf.local zone "test.lab" { type master; file "/etc/bind/db.test.lab"; }; zone "50.168.192.in-addr.arpa" { type master; file "/etc/bind/db.192.168.50"; };这段配置声明了两个 zone:正向的test.lab和反向的50.168.192.in-addr.arpa。type master表示这台服务器是该 zone 的权威主服务器,直接读写本地文件。反向 zone 的名字是把 IP 段192.168.50.0/24反过来写,尾部加上in-addr.arpa。注意named.conf.local里 zone 名末尾不需要加点,BIND9 会自动补,但如果在named-checkzone命令里单独传 zone 名,就需要手动补上末尾的点。
这里的核心参数是file路径。BIND9 在 Ubuntu 上以bind用户运行,所以 zone 文件要放在/etc/bind/下,并且确保权限是644、属主是bind或root。我见过有人把 zone 文件放在/root/下,结果 named 启动时读不到文件,一直报permission denied。
3.2 db.test.lab 正向 zone 文件与反向 zone 文件
正向 zone 文件定义了域名到 IP 的映射。在/etc/bind/db.test.lab写入:
$TTL 600 @ IN SOA ns1.test.lab. admin.test.lab. ( 2025041501 ; serial 3600 ; refresh 1800 ; retry 604800 ; expire 600 ) ; negative TTL IN NS ns1.test.lab. ns1 IN A 192.168.50.10 www IN A 192.168.50.10 mail IN A 192.168.50.20第一行的$TTL 600是默认 TTL,表示这条记录默认被缓存 600 秒。SOA 记录是整个 zone 的“版本说明”:括号里第一行2025041501是 serial,格式通常是“日期 + 当天修改序号”,今天是 2025 年 4 月 15 日就写 2025041501,如果你一天里改了两次,第二次就要写成 2025041502。serial 不递增,从服务器和缓存就不会刷新,这是整个实验里最容易丢分的一个点。
NS 记录声明了该 zone 的权威服务器是ns1.test.lab;ns1、www、mail三条 A 记录分别映射了不同的主机名。注意所有以域名结尾的字段,比如ns1.test.lab,末尾都必须有圆点,否则 BIND9 会把test.lab再拼上去,变成ns1.test.lab.test.lab。
反向 zone 文件/etc/bind/db.192.168.50写成:
$TTL 600 @ IN SOA ns1.test.lab. admin.test.lab. ( 2025041501 ; serial 3600 ; refresh 1800 ; retry 604800 ; expire 600 ) ; negative TTL IN NS ns1.test.lab. 10 IN PTR ns1.test.lab. 10 IN PTR www.test.lab. 20 IN PTR mail.test.lab.PTR 记录是反向解析的核心。“10”在这里表示 IP 最后一个八位组,配合 zone 名50.168.192.in-addr.arpa,就完整组成了10.50.168.192.in-addr.arpa。注意192.168.50.10 -> 10.50.168.192.in-addr.arpa这个反写方向,几乎每个做课设的同学都会至少在这错一次。PTR 记录的 rdata 部分同样要记得末尾加点,www.test.lab.而不是www.test.lab。
3.3 用 named-checkzone 和 dig 验证配置:报错输出怎么看
写完配置文件不要急着重启服务。先跑两个检查命令,能省掉大部分排错时间:
sudo named-checkconf /etc/bind/named.conf sudo named-checkzone test.lab /etc/bind/db.test.labnamed-checkconf检查语法,如果有括号不匹配或分号缺失,它会直接报行号。named-checkzone检查 zone 文件内部逻辑,比如 SOA 记录缺了字段、PTR 记录反向段写错、末尾遗漏圆点。输出OK说明基础没问题,再重启服务:
sudo systemctl restart named重启后用 dig 验证正向解析:
dig @127.0.0.1 www.test.lab A +short正常情况下输出192.168.50.10。如果不加+short,在 Answer Section 里能看到一条完整的 A 记录。反向验证用-x参数:
dig @127.0.0.1 -x 192.168.50.10 +short输出www.test.lab.或ns1.test.lab.都算对。如果你在 UOS Server 20 上做,apt install bind9包名相同,唯一区别是部分 UOS 版本的 systemd 服务名可能显示为bind9.service的别名,用systemctl status named和systemctl status bind9两个命令都试一下,哪个有响应就用哪个看日志。
4. 从零写一个迷你 DNS 服务器:Python 方案与抓包验证
有些学校课设要求里明确写了“实现 DNS 协议”,这意味着你不能只装 BIND9 交差,得有一份自己能讲解代码逻辑的实现。自己手撸完整的 DNS 二进制解析工作量大且容易出错,通常做法是用dnslib库把报文解析和构造封装好,自己专注实现查询逻辑。这样既能讲代码,又不会在十六进制解码上耗掉整个周末。
4.1 dnslib 实现支持 A 记录和 PTR 记录的最小服务器
先安装依赖:pip install dnslib。然后写一个 UDP 服务,监听 53 端口,收到查询后从请求里提取域名和查询类型,在内存字典里查映射,构造响应返回。完整代码如下:
#!/usr/bin/env python3 import socket from dnslib import DNSRecord, QTYPE, RR mapping = { "www.test.lab": "192.168.50.10", "ns1.test.lab": "192.168.50.10", "mail.test.lab": "192.168.50.20", } sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("0.0.0.0", 53)) print("mini DNS server running on :53") while True: data, addr = sock.recvfrom(512) request = DNSRecord.parse(data) qname = str(request.q.qname)[:-1] # 去掉末尾的点 qtype = QTYPE[request.q.qtype] reply = DNSRecord(qr=1, aa=1) # 响应、权威回答标志 reply.q = request.q if qtype == "A" and qname in mapping: reply.add_answer(RR(qname, qtype="A", ttl=600, rdata=mapping[qname])) elif qtype == "PTR" and qname.endswith(".50.168.192.in-addr.arpa"): octet = qname.split(".")[0] ptr_map = {"10": "www.test.lab", "20": "mail.test.lab"} if octet in ptr_map: reply.add_answer( RR(qname, qtype="PTR", ttl=600, rdata=ptr_map[octet] + ".") ) sock.sendto(reply.pack(), addr)这段代码的核心在DNSRecord(qr=1, aa=1)。qr=1把响应标志位置 1,告诉客户端这是响应而不是另一个查询;aa=1设置权威回答标志,告诉客户端“这个答案来自权威服务器”。request.q回填到响应里,是为了让客户端能通过 Transaction ID 匹配到刚才发出的那个查询。
RR()的三个参数要细看:rdata=mapping[qname]生成的是 A 记录数据,ttl=600与前面 BIND9 配置里的 TTL 保持一致,PTR 记录构造时给域名加了末尾的圆点。这里有个细节:recvfrom(512)的 512 字节缓冲区是 DNS over UDP 的传统上限,如果查询响应超过 512 字节就要走 TCP 或者 EDNS0,课设场景下几乎不会触发。
运行需要 root 权限,因为监听 53 端口属于特权端口:
sudo python3 dns_server.py然后另开一个终端用 dig 测试:
dig @127.0.0.1 www.test.lab A +short看到192.168.50.10就说明迷你 DNS 服务器已经把报文完整走了一遍。如果卡住了,大概率是端口被 systemd-resolved 占住,先按下一章的方法处理。
4.2 dig 与 tcpdump 抓包:从十六进制里读出响应标志
代码能跑只是第一步,答辩时需要证明你理解线上报文。常用做法是用 tcpdump 把 53 端口的流量抓下来:
sudo tcpdump -i any port 53 -nn -vv再执行一条查询:
dig @127.0.0.1 www.test.lab Atcpdump 输出里会出现一收一发的两条 UDP 报文。发出那条有Flags [q],表示是一个标准查询;返回那条会有Flags [q. aa rd ra]或者类似组合。这里的aa就是 Authoritative Answer,对应你代码里DNSRecord(qr=1, aa=1)设的那个标志位;ra表示服务器支持递归。
如果想看最原始的十六进制报文,给 tcpdump 加-X参数:
sudo tcpdump -i any port 53 -nn -X在输出里找03 77 77 77,对照 2.2 节讲过的 QNAME 编码方式,现场指给老师看:03是长度为 3 的标签,77 77 77是ASCII码的www。能把这个指出来,比说十句“我实现了DNS”都有说服力。
4.3 时间不够时的后悔药:用 dnsmasq 兜底
如果 deadline 只剩半天,且课设没有硬性要求“手写协议解析”,可以直接换 dnsmasq 方案完成功能点。它的配置比 BIND9 简单得多,天然自带递归、缓存和转发能力。在/etc/dnsmasq.conf里加两行:
address=/test.lab/192.168.50.10 address=/www.test.lab/192.168.50.10重启服务后,整个test.lab域都解析到192.168.50.10,外部域名也能正常走系统配置的公网 DNS。这是最省事的兜底路径。但要注意:dnsmasq 和 BIND9 不能同时跑在同一台机器上,它们都抢 53 端口。用 dnsmasq 之前先把 named 停掉:
sudo systemctl stop named sudo systemctl disable named sudo systemctl restart dnsmasq这个方案作为“后悔药”是可以的,但答辩时如果老师问“dnsmasq 和你手写的服务器有什么区别”,你要能说出来:dnsmasq 是一个完整的转发加缓存服务器,你只有时间熟悉它的配置;如果想展示协议理解,还是要靠 4.1 那套 Python 代码。
5. DNS 实验里最常见的 5 个坑:从 53 端口占用到反向解析 NXDOMAIN
课设做 DNS 服务器实验,配置写错只占失败原因的很小一部分,大部分翻车发生在环境问题。这一章把高频问题按现象到解决的方式列出来,可以当作排错手册用。
5.1 现象:named 启动失败,日志显示 Address already in use
这是 Ubuntu 和 UOS 平台最典型的问题。执行systemctl start named后状态是 failed,用journalctl -u named看日志,里面有could not listen on UDP socket: Address already in use。
原因:Ubuntu 18.04 之后的 systemd-resolved 默认占用了 53 端口,BIND9 抢不到。解决方法是先停掉 systemd-resolved,再手动管理 resolv.conf:
sudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved sudo rm -f /etc/resolv.conf echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf这里把 resolv.conf 指向 127.0.0.1 是把本机 DNS 交给你的 named。注意如果改成 127.0.0.1 后apt update变慢或失败,通常是因为 named 的转发还没配置好,先把外网域名解析问题按 5.2 解决。
5.2 现象:test.lab 能解析,但 baidu.com 超时
自定义域名正常,外部域名全部 NXDOMAIN 或超时。原因通常是 named 的forwarders未配置,或recursion被关闭。BIND9 默认配置里只监听本地,且没写转发目标。
在/etc/bind/named.conf.options里加上:
options { directory "/var/cache/bind"; recursion yes; allow-query { any; }; forwarders { 223.5.5.5; 119.29.29.29; }; forward only; };这里两个公网 DNS 的选型要注意:223.5.5.5 是阿里 DNS,119.29.29.29 是腾讯 DNSPod,在国内网络环境下连通性比 8.8.8.8 稳定得多。forward only表示所有未命中 zone 的查询一律转给上游,不自己迭代。改完重启 named,再dig @127.0.0.1 baidu.com验证。如果还超时,检查虚拟机出网是否正常——ping 223.5.5.5能通,再排查别的。
5.3 现象:虚拟机里 dig 正常,真机浏览器打不开
服务器配置没有任何问题,在虚拟机里dig @192.168.50.10 www.test.lab返回正确,但回到宿主机用浏览器访问www.test.lab就是打不开。
原因有三层,按出现频率排:第一,宿主机网卡的 DNS 没有指向192.168.50.10,还在用路由器下发的地址;第二,VMware NAT 模式的 DHCP 会给虚拟机下发自己的 DNS,导致查询根本没到你的 named;第三,浏览器启用了 DoH(DNS over HTTPS),它直接走https://dns.google/dns-query这类加密通道,完全绕过系统 DNS。
解决:手动把宿主机网卡 DNS 改成192.168.50.10;浏览器地址栏输入edge://settings/privacy(Edge)或chrome://settings/security(Chrome)里关闭“使用安全 DNS”;最后用nslookup www.test.lab确认系统层解析已经指向你的服务器。
5.4 现象:反向解析总是 NXDOMAIN
dig -x 192.168.50.10返回 NXDOMAIN,但正向解析完全正常。这是反向 zone 配置写错的最典型表现。
原因基本是三类。zone 声明写错了段:IP 是 192.168.50.x,反向 zone 应该是50.168.192.in-addr.arpa,有人会写成192.168.50.in-addr.arpa,这在实际的 DNS 树里不存在。第二类是 PTR 记录里少了最左段:PTR 名称写成10.50.168.192.in-addr.arpa,但 zone 文件里只写了10,要确保和 zone 名拼接后完整。第三类是 PTR 记录 rdata 末尾漏了圆点。
排错方法是在服务器上直接查:
dig @127.0.0.1 10.50.168.192.in-addr.arpa PTR如果 authority 段显示50.168.192.in-addr.arpa,说明 zone 声明对了;如果查询名变成10.192.168.50.in-addr.arpa之类,说明反写顺序错了。还有一种情况是 dig 提示 SERVFAIL,说明 zone 文件语法有问题,用 3.3 节的named-checkzone 50.168.192.in-addr.arpa /etc/bind/db.192.168.50定位。
5.5 现象:改了 zone 文件,dig 结果还是旧 IP
在 zone 文件里把www.test.lab的 A 记录从192.168.50.10改成192.168.50.30,重启 named 后,dig 结果还是旧地址。这个坑几乎每个人都踩,直接原因是 SOA 里的 serial 没递增。
BIND9 不会根据文件的 mtime 自动重载 zone。每次修改 zone 文件后,serial 必须比上一次大,否则从服务器和缓存都会认为“zone 没有变化”,继续用内存里的旧记录。正确流程是:改文件 → serial 递增 →sudo named-checkzone→sudo rndc reload test.lab。rndc reload是重载指定 zone,比整个重启服务更优雅,答辩时说出来是加分项。
养成这个习惯:serial 用YYYYMMDDNN格式,比如 2025041502,每天改动不超过 99 次就够用。检查当前是否生效,可以用:
dig @127.0.0.1 www.test.lab A +noall +answer如果服务器和客户端在不同机器,客户端那边还有一层缓存,dig 时加+norecurse可以避免递归缓存干扰判断。
6. 用 Wireshark 验证权威响应:交报告前最后 10 分钟的检查习惯
接近交作业时,我习惯把整套实验按“协议级验证”跑一遍,而不只是看 dig 输出。这个习惯帮我在答辩前发现过很多隐蔽问题。第一步是抓包留存证物:
sudo tcpdump -i any port 53 -w /tmp/dns.pcap然后在另一终端执行完整的查询序列:正向 A 记录、反向 PTR、外部域名转发。Ctrl+C 结束抓包后,把 pcap 传到宿主机,用 Wireshark 打开。过滤表达式用dns.flags.response == 1,把所有响应报文过滤出来。点开其中一条www.test.lab的响应,看 Flags 区域里 Authoritative Answer 的位是否为 1,对应的是 2.2 节讲的 AA 标志。如果这条响应是从你的 named 或 Python 服务器发的,AA 必然为 1;如果是从上级缓存返回的,AA 为 0。这个区别在报告里截图标注出来,就是一条完整的“权威解析”证据链。
第二个有用的实操是验证 TTL 递减。前面配置里 TTL 设置为 600,第一次查询响应里 Answer 段的 TTL 是 600,过几十秒再查一次,数字会变小,说明缓存机制在工作。如果 TTL 一直是 600 不变,说明客户端每次绕过了缓存,或者你的解析器每次都触发了权威刷新。
第三个习惯是演示+norecurse和+recurse的区别。dig +norecurse @127.0.0.1 www.test.lab A时,named 只会查本地 zone,查不到就直接返回空 answer,不会帮你转发;+recurse时才会走 forwarders。答辩现场演示这个对比,能让老师一眼看出你理解“递归”和“权威”的区别。我在自己的课设里吃过亏,当时只会在报告上写“配置成功、解析正常”,结果老师追问“RA 标志位你在抓包里见过没”,我愣住了。后来每次改完 zone,都先 tcpdump 抓一把再继续下一步。这个习惯保留到现在,希望你也能用得上。
本文还有配套的精品资源,点击获取