北邮计算机网络课设:C++写的DNS中继工具,支持域名拦截和上游转发
2026/6/12 7:50:54 网站建设 项目流程

本文还有配套的精品资源,点击获取

简介:这是一个面向高校课程设计的轻量级DNS中继实现,用C++编写,核心功能包括接收客户端DNS查询(UDP)、转发请求到指定上游DNS服务器、将响应原路返回,同时支持按域名或IP地址进行实时屏蔽。项目结构简洁,主逻辑集中在main.c和main.h中,配置统一通过dnsrelay.txt文件管理,可自定义上游DNS地址、端口及屏蔽规则列表,无需额外依赖,Linux环境下g++编译后即可运行。适合用于理解DNS协议交互流程、UDP套接字编程、请求/响应转发机制以及基础网络中间件开发思路。代码注释清晰,变量命名规范,调试信息友好,方便学生完成课程报告中的协议分析、功能验证与扩展实验,比如添加日志记录、缓存机制或黑白名单动态加载。

1. 项目概述:一个“看得懂、改得动、跑得稳”的课设级DNS中继

你有没有在计算机网络课设里,对着Wireshark抓到的一堆DNS查询包发过呆?明明课本上写着“客户端→本地DNS→根DNS→权威DNS→返回”,可真要自己写个能拦住ads.example.com、把www.baidu.com转发给223.5.5.5的中间件时,却卡在UDP socket怎么bind、DNS报文ID怎么透传、响应怎么原路送回去这些细节上?这个北邮课设项目,就是专为这种“理论懂、动手懵”的状态设计的——它不是工业级的dnsmasqcoredns,而是一份可逐行调试、每处修改都有明确反馈、编译失败也能一眼定位问题的C++实现。核心就两个文件:main.c(实际是.cpp,但命名沿用C习惯)和main.h,没有Makefile嵌套、没有第三方库依赖、不碰线程池和异步IO,所有逻辑都摊开在你眼皮底下。它解决的不是生产环境的高并发难题,而是课程设计最痛的三个点:协议解析是否准确?转发路径是否闭环?屏蔽逻辑是否可验证?比如,当你在dnsrelay.txt里写下block www.qq.com,启动后用dig @127.0.0.1 www.qq.com,Wireshark里立刻看不到发往上游的请求包,而dig返回NXDOMAIN——这种“所见即所得”的反馈,比任何PPT里的架构图都管用。它适合两类人:一是刚学完UDP socket但还没写过完整网络程序的学生,能从socket()bind()recvfrom()开始,一行行跟进去看DNS报文怎么解包;二是想快速验证某个屏蔽策略效果的实验者,改一行配置、重编译一次,三分钟就能看到结果。我带过几届学生做这个课设,最常被问的问题不是“怎么写”,而是“怎么确认我写的没出错”——这个项目的设计哲学,就是让每一个环节都有迹可循:上游转发失败时打印错误码,域名匹配成功时输出日志,甚至UDP收发缓冲区大小都硬编码成1024这种好记的数字,方便你在gdb里直接断点观察内存布局。

2. 整体设计与思路拆解:为什么用纯UDP+单线程+文本配置?

2.1 协议层选择:为什么死磕UDP,而不是TCP或HTTP?

DNS协议本身规定:绝大多数查询(A记录、AAAA记录等)必须走UDP,只有当响应报文超过512字节或需要区域传输时才降级到TCP。这个项目严格遵循RFC 1035的原始设计,原因很实在:课设场景下,UDP足够覆盖95%以上的教学需求,且能暴露最本质的网络编程问题。比如,UDP无连接特性意味着你必须手动维护“请求-响应”的映射关系——客户端发来一个ID为0x1234的查询,上游返回ID相同的响应,你得靠这个ID把响应塞回正确的sockaddr_in地址。如果换成TCP,连接管理、粘包处理、心跳保活这些额外复杂度会彻底淹没DNS协议本身的学习目标。更关键的是,UDP的“不可靠”反而成了教学利器:当你故意在代码里注释掉sendto()调用,dig命令会卡在“;; connection timed out”并自动重试,这种直观的失败反馈,比任何文字描述都深刻。实测下来,用g++ -std=c++11 main.cpp -o dnsrelay编译后,在Ubuntu 22.04上运行./dnsrelay,用netstat -ulnp | grep :53能清晰看到进程监听在UDP 53端口,而tcpdump -i lo port 53则能实时捕获到进出的UDP包——这种“底层可见性”,正是课程设计最需要的透明度。

2.2 架构极简主义:单线程阻塞式为何是课设最优解?

项目采用单线程、阻塞式socket模型,看似“落后”,实则是精准的教学取舍。多线程或epoll虽然能提升性能,但会引入竞态条件、锁机制、事件循环等新概念,让学生陷入“先学并发再学DNS”的本末倒置。单线程的好处在于:所有逻辑按时间顺序线性展开,gdb调试时可以逐行step into,变量生命周期一目了然。比如,recvfrom()接收到一个DNS查询包后,程序立即执行parse_dns_query()解析报文头,接着调用match_block_rule()检查域名,再决定是直接构造NXDOMAIN响应还是调用forward_to_upstream()转发——这个链条里没有回调、没有状态机跳转,就像读一段C语言伪代码一样顺畅。我试过把forward_to_upstream()函数里加一句usleep(100000)模拟上游延迟,dig命令就会明显卡顿,而整个进程不会崩溃,因为没有其他线程在抢夺资源。这种“可控的慢”,恰恰是理解网络延迟、超时机制的最佳沙盒。至于性能瓶颈?课设场景下,单核CPU处理几百QPS完全够用,真要压测,你甚至可以把while(1)循环改成for(int i=0; i<1000; i++),让它只处理1000次请求就退出,方便你用time ./dnsrelay统计单次处理耗时。

2.3 配置驱动设计:dnsrelay.txt如何做到“零学习成本”?

配置文件dnsrelay.txt的设计,直击学生怕改配置的心理。它只有三类指令,全部用空格分隔,格式像极了Linux命令行:

upstream 223.5.5.5 53 block www.taobao.com block 192.168.1.100

没有JSON的括号嵌套,没有YAML的缩进陷阱,连注释都用#开头(# 这是注释),和/etc/hosts风格完全一致。解析逻辑也极其朴素:逐行读取,用strtok()按空格切分,第一个token是命令名,后续是参数。upstream命令存入全局结构体g_config.upstream_ipg_config.upstream_portblock命令则把参数存入std::vector<std::string> g_block_list。这种设计的好处是,学生想加一条屏蔽规则,只需要打开文本编辑器,敲一行block ads.google.com,保存后重启程序即可生效——不需要查文档、不需要编译、甚至不需要理解正则表达式。我在指导学生时发现,很多人卡在“怎么让程序读到新配置”,而这个方案的答案就是:“改完txt,Ctrl+S,然后./dnsrelay”。更妙的是,配置解析失败时,程序会在终端打印类似[ERROR] Invalid upstream format in line 1: 'upstream 223.5.5.5'的提示,精确到行号和错误原因,避免学生面对黑屏启动失败时的无助感。

3. 核心细节解析与实操要点:从DNS报文解包到屏蔽逻辑落地

3.1 DNS报文结构还原:为什么ID字段必须原样透传?

DNS查询和响应报文的前12个字节是固定头部,其中第1-2字节是ID(标识符),这是整个转发逻辑的命脉。很多初学者会误以为“反正要转发,ID随便生成一个就行”,但这是致命错误。RFC明确规定:响应报文的ID必须与查询报文完全一致,否则客户端会丢弃该响应。这个项目在forward_to_upstream()函数里,严格保留原始查询包的ID字段,仅修改QR(Query/Response)位为1、RA(Recursion Available)位为1,并清空QDCOUNT(问题数)以外的计数器。具体操作是:用memcpy()把原始包前12字节拷贝到响应缓冲区,然后用位运算修改标志位:

// 假设buf是原始查询包,resp_buf是响应缓冲区 memcpy(resp_buf, buf, 12); // 复制头部 resp_buf[2] |= 0x80; // 设置QR位(第2字节第7位) resp_buf[3] |= 0x80; // 设置RA位(第3字节第7位) // 清空ANCOUNT、NSCOUNT、ARCOUNT(第6-9字节) memset(resp_buf + 6, 0, 4);

这样做的好处是,dig或浏览器发出的查询,其ID(比如0xabcd)在上游响应里依然是0xabcd,客户端能100%匹配。我曾让学生故意把resp_buf[2] |= 0x80改成resp_buf[2] = 0xff,结果dig永远收不到响应,Wireshark里能看到上游返回了包,但本机UDP socket就是不触发recvfrom()——这就是协议细节咬死的典型例子。记住:DNS不是HTTP,没有URL路径,ID就是唯一的“会话凭证”。

3.2 域名屏蔽的两种模式:精确匹配 vs 通配符匹配

项目支持两种屏蔽方式,对应dnsrelay.txt里的不同写法:
-精确匹配block www.baidu.com→ 只拦截www.baidu.com的A记录查询,image.baidu.com不受影响;
-通配符匹配block *.baidu.com→ 使用fnmatch()函数匹配,拦截所有子域名。

实现上,match_block_rule()函数先提取查询报文中的域名(从QNAME字段解析,注意DNS压缩指针的处理),然后遍历g_block_list。对每个规则,如果是*.xxx格式,调用fnmatch(rule.c_str(), domain.c_str(), FNM_CASEFOLD);否则用strcasecmp()做大小写不敏感的全等比较。这里有个易错点:DNS域名在报文中是以[3]www[5]baidu[3]com[0]这样的长度前缀格式存储的,不能直接当C字符串用。项目在parse_domain_name()函数里做了正确解析:从偏移量0开始,读取第一个字节得到长度len,若len==0则结束,若len>=192(0xC0)则说明是压缩指针,需跳转到指定偏移继续解析。我让学生用printf("Domain: %s\n", domain.c_str())打印解析后的域名,再和dnsrelay.txt里的规则对比,能立刻发现www.baidu.com.(结尾的点)和www.baidu.com是否匹配——这正是调试时最常踩的坑:DNS规范要求域名以0x00结尾,但人类输入时不加点,代码里必须统一处理。

3.3 上游转发的健壮性设计:超时与重试如何避免“假死”?

UDP转发最大的风险是上游无响应,导致客户端无限等待。项目设置了两级超时机制:
-单次转发超时setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))tv.tv_sec=3,即上游响应超过3秒未到达,recvfrom()返回-1;
-客户端查询总超时:主循环里用clock_gettime(CLOCK_MONOTONIC, &start)记录查询开始时间,每次recvfrom()前检查是否已过5秒,超时则直接返回SERVFAIL

更关键的是重试逻辑:当上游超时,程序不会立即放弃,而是最多重试2次,且每次重试前usleep(50000)(50毫秒)。这个50毫秒不是拍脑袋定的——它远小于客户端默认的1秒重试间隔(dig+timeout=1),确保重试包能在客户端超时前发出。实测中,把上游IP设成不存在的192.168.99.99dig @127.0.0.1 www.baidu.com会显示;; connection timed out; no servers could be reached,但程序日志里会清晰打印[WARN] Upstream timeout, retry 1/2,这种可追溯的失败过程,比静默失败更有教学价值。另外,上游socket是SOCK_DGRAM类型,每次sendto()都必须指定目标地址,项目用struct sockaddr_in upstream_addr缓存上游IP和端口,避免重复解析字符串,这也是性能优化的小细节。

4. 实操过程与核心环节实现:从编译到功能验证的完整链路

4.1 编译与环境准备:为什么g++ 7.5+是硬性要求?

项目使用C++11标准,核心依赖只有<sys/socket.h><netinet/in.h><arpa/inet.h>这些POSIX网络API,以及<fnmatch.h>用于通配符匹配。编译命令极简:

g++ -std=c++11 -O2 main.cpp -o dnsrelay

但要注意:fnmatch()在旧版glibc中可能不支持FNM_CASEFOLD标志,所以最低要求是Ubuntu 18.04(g++ 7.5)或CentOS 8。如果遇到undefined reference to fnmatch,只需加-lfnmatch链接选项。环境准备步骤如下:
1.安装基础工具sudo apt update && sudo apt install build-essential tcpdump wireshark-cli
2.关闭系统DNS服务sudo systemctl stop systemd-resolved && sudo systemctl disable systemd-resolved(避免53端口冲突)
3.赋予CAP_NET_BIND_SERVICE权限sudo setcap 'cap_net_bind_service=+ep' ./dnsrelay(让普通用户能绑定1-1023端口)

最关键的一步是端口占用检查:运行sudo lsof -i :53,确保没有其他进程监听UDP 53。我见过太多学生卡在这一步,./dnsrelay启动无声无息,其实是被systemd-resolved占了端口。解决方案除了停服务,还可以把配置改成upstream 8.8.8.8 53,然后用sudo ./dnsrelay临时运行(不推荐长期用root)。

4.2 配置文件实战:三行配置构建最小可行系统

dnsrelay.txt是功能开关的中枢,我们用一个真实案例演示如何从零搭建:

# dnsrelay.txt upstream 114.114.114.114 53 block www.qq.com block 192.168.1.100

这三行实现了:所有查询转发到国内公共DNS114.114.114.114;屏蔽腾讯域名;屏蔽内网某台服务器IP。启动后,用以下命令验证:

# 1. 测试正常解析 dig @127.0.0.1 www.baidu.com | grep "ANSWER SECTION" # 2. 测试域名屏蔽(应返回NXDOMAIN) dig @127.0.0.1 www.qq.com | grep "status:" # 3. 测试IP屏蔽(应返回SERVFAIL或超时) dig @127.0.0.1 www.example.com | grep "status:"

注意:dig命令必须显式指定@127.0.0.1,否则会走系统默认DNS。如果第2步返回status: NOERROR,说明屏蔽规则没生效,此时检查main.cppmatch_block_rule()的调用位置——它必须在forward_to_upstream()之前,否则请求已经发出去了。我在调试时常用printf("[DEBUG] Matched block rule: %s\n", rule.c_str())打点,配合tail -f /dev/stdout实时查看日志。

4.3 功能扩展实录:如何在30分钟内添加日志记录?

课程报告常要求“添加日志功能”,这其实是绝佳的扩展练习。项目预留了LOG_LEVEL宏定义,只需修改main.h

#define LOG_LEVEL 2 // 0=OFF, 1=ERROR, 2=WARN, 3=INFO

然后在关键位置插入日志:

// 在recvfrom()后 if (LOG_LEVEL >= 3) { printf("[INFO] Received query from %s:%d, len=%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), n); } // 在match_block_rule()返回true后 if (LOG_LEVEL >= 2) { printf("[WARN] Blocked domain: %s (rule: %s)\n", domain.c_str(), rule.c_str()); }

编译后运行,日志会直接输出到终端。如果想写入文件,只需把printf改成fprintf(log_fp, ...),并在main()开头log_fp = fopen("dnsrelay.log", "a")。这个改动不到20行代码,却能让报告里的“功能验证”章节瞬间丰满——你可以截图dnsrelay.log里连续的Blocked domain记录,配上dig命令的返回结果,形成完整的证据链。更重要的是,这个过程教会学生:日志不是堆printf,而是分级控制、格式统一、便于grep筛选的工程实践。

5. 常见问题与排查技巧实录:那些课设现场踩过的坑

5.1 典型问题速查表

问题现象可能原因排查命令解决方案
./dnsrelay启动后无任何输出,netstat -ulnp \| grep :53看不到监听端口被占用或权限不足sudo lsof -i :53停用systemd-resolved,或用sudo setcap授予权限
dig @127.0.0.1 www.baidu.com返回connection timed out上游DNS不可达或防火墙拦截ping 114.114.114.114telnet 114.114.114.114 53检查上游IP是否可达,确认防火墙放行UDP 53
屏蔽规则block www.qq.com不生效,dig仍返回正确IP域名解析未走本机DNS或规则匹配失败dig @127.0.0.1 www.qq.com +shorttcpdump -i lo port 53 -w debug.pcap确认dig命令指定了@127.0.0.1,用Wireshark分析debug.pcap看是否有上游请求包
dig返回SERVFAIL而非NXDOMAIN上游转发失败,非屏蔽导致查看程序终端日志中的[WARN] Upstream timeout检查上游DNS地址是否拼写错误,或网络是否通畅
编译报错undefined reference to 'fnmatch'链接器未找到fnmatch库g++ -std=c++11 main.cpp -o dnsrelay -lfnmatch在编译命令末尾添加-lfnmatch

5.2 独家避坑技巧:Wireshark抓包的黄金三步法

Wireshark是DNS调试的终极武器,但新手常抓不到有效包。我的经验是严格按三步操作:
1.过滤器前置:启动Wireshark后,先在过滤栏输入udp.port == 53 && ip.addr == 127.0.0.1,排除所有无关流量;
2.双向追踪:右键任意一个DNS包 →FollowUDP Stream,这样能看到客户端查询和上游响应的完整对话(注意:UDP Stream里会显示[TCP Retransmission]字样,这是Wireshark的误标,实际是UDP重传);
3.报文对比:导出两个包(客户端查询和上游响应),用xxd命令转十六进制对比:
bash xxd query.pcap | head -20 xxd response.pcap | head -20
重点核对第1-2字节(ID)、第3字节(QR位)、第6-9字节(计数器)。如果ID不一致,说明透传逻辑有bug;如果QR位仍是0,说明响应头部未修改。这个方法比肉眼数包快十倍,我带学生时,半小时就能教会他们独立完成报文级调试。

5.3 课设报告加分项:协议分析的可视化呈现

课程报告常要求“分析DNS协议交互流程”,与其贴大段Wireshark截图,不如用ASCII艺术画一个精简流程图:

Client (192.168.1.100) DNS Relay (127.0.0.1) Upstream (114.114.114.114) | | | |---[ID=0x1234] A? www.baidu.com-->| | | |---[ID=0x1234] A? www.baidu.com-->| | |<--[ID=0x1234] A=180.101.49.12-->| |<--[ID=0x1234] A=180.101.49.12----| |

这个图的关键在于:所有箭头标注ID值,且上下游ID完全一致。在报告里配上文字说明:“如图所示,DNS Relay作为中间节点,严格保持查询ID不变,确保客户端能正确关联响应”。这种小而准的可视化,比泛泛而谈“DNS采用UDP协议”更能体现你的理解深度。另外,可以截取tcpdump的原始输出:

$ sudo tcpdump -i lo -nn -X port 53 15:22:34.123456 IP 127.0.0.1.54321 > 127.0.0.1.53: UDP, length 32 0x0000: 4500 003c 0000 4000 4011 0000 7f00 0001 E..<..@.@....... 0x0010: 7f00 0001 d431 0035 0028 0000 1234 0100 .....1.5.(...4.. 0x0020: 0001 0000 0000 0000 0377 7777 0562 6169 .........www.bai 0x0030: 6475 0363 6f6d 0000 0100 01 du.com.....

指出0x0010偏移处的1234就是ID字段,0x0020处的0377 7777对应域名www的长度前缀——这种落到字节层面的分析,才是课程设计该有的硬核姿态。

6. 扩展可能性与教学延伸:从课设到真实工程的桥梁

这个项目最迷人的地方,在于它的“可生长性”。它不是一个封闭的黑盒,而是一块精心设计的乐高底板,学生可以根据兴趣向上叠加模块。比如,添加缓存功能只需在main.h里声明一个std::map<std::string, std::vector<uint8_t>> g_cache,在forward_to_upstream()前检查g_cache.find(domain) != g_cache.end(),命中则直接sendto()缓存数据;未命中则转发并把响应存入缓存。计算缓存TTL时,要解析响应报文的TTL字段(位于RR记录的第7-10字节),这又自然引出了DNS资源记录格式的学习。另一个常见扩展是黑白名单动态加载:把dnsrelay.txt改成监听一个HTTP端口(如localhost:8080/api/block),用curl -X POST http://localhost:8080/api/block -d "domain=ads.com"实时更新规则。这会迫使学生接触HTTP协议解析、多线程处理(主线程监听UDP,另一线程监听HTTP)、以及线程安全的std::mutex保护——所有这些,都是从当前项目的main.cpp里顺延出来的自然生长点。我个人在指导时,会鼓励学生选一个扩展点深入,哪怕只实现50行代码,只要能讲清楚“为什么这么设计”“遇到了什么问题”“怎么验证成功的”,这份报告的价值就远超一份完美但无思考痕迹的成品。毕竟,真正的网络工程师,不是写出最复杂的代码,而是能在约束条件下,用最清晰的逻辑解决最实际的问题。

本文还有配套的精品资源,点击获取

简介:这是一个面向高校课程设计的轻量级DNS中继实现,用C++编写,核心功能包括接收客户端DNS查询(UDP)、转发请求到指定上游DNS服务器、将响应原路返回,同时支持按域名或IP地址进行实时屏蔽。项目结构简洁,主逻辑集中在main.c和main.h中,配置统一通过dnsrelay.txt文件管理,可自定义上游DNS地址、端口及屏蔽规则列表,无需额外依赖,Linux环境下g++编译后即可运行。适合用于理解DNS协议交互流程、UDP套接字编程、请求/响应转发机制以及基础网络中间件开发思路。代码注释清晰,变量命名规范,调试信息友好,方便学生完成课程报告中的协议分析、功能验证与扩展实验,比如添加日志记录、缓存机制或黑白名单动态加载。


本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询