北邮计算机网络课设:C++写的DNS中继工具,支持域名拦截和上游转发 本文还有配套的精品资源点击获取简介这是一个面向高校课程设计的轻量级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怎么透传、响应怎么原路送回去这些细节上这个北邮课设项目就是专为这种“理论懂、动手懵”的状态设计的——它不是工业级的dnsmasq或coredns而是一份可逐行调试、每处修改都有明确反馈、编译失败也能一眼定位问题的C实现。核心就两个文件main.c实际是.cpp但命名沿用C习惯和main.h没有Makefile嵌套、没有第三方库依赖、不碰线程池和异步IO所有逻辑都摊开在你眼皮底下。它解决的不是生产环境的高并发难题而是课程设计最痛的三个点协议解析是否准确转发路径是否闭环屏蔽逻辑是否可验证比如当你在dnsrelay.txt里写下block www.qq.com启动后用dig 127.0.0.1 www.qq.comWireshark里立刻看不到发往上游的请求包而dig返回NXDOMAIN——这种“所见即所得”的反馈比任何PPT里的架构图都管用。它适合两类人一是刚学完UDP socket但还没写过完整网络程序的学生能从socket()、bind()、recvfrom()开始一行行跟进去看DNS报文怎么解包二是想快速验证某个屏蔽策略效果的实验者改一行配置、重编译一次三分钟就能看到结果。我带过几届学生做这个课设最常被问的问题不是“怎么写”而是“怎么确认我写的没出错”——这个项目的设计哲学就是让每一个环节都有迹可循上游转发失败时打印错误码域名匹配成功时输出日志甚至UDP收发缓冲区大小都硬编码成1024这种好记的数字方便你在gdb里直接断点观察内存布局。2. 整体设计与思路拆解为什么用纯UDP单线程文本配置2.1 协议层选择为什么死磕UDP而不是TCP或HTTPDNS协议本身规定绝大多数查询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 -stdc11 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 i0; i1000; 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_ip和g_config.upstream_portblock命令则把参数存入std::vectorstd::string g_block_list。这种设计的好处是学生想加一条屏蔽规则只需要打开文本编辑器敲一行block ads.google.com保存后重启程序即可生效——不需要查文档、不需要编译、甚至不需要理解正则表达式。我在指导学生时发现很多人卡在“怎么让程序读到新配置”而这个方案的答案就是“改完txtCtrlS然后./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若len0则结束若len1920xC0则说明是压缩指针需跳转到指定偏移继续解析。我让学生用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_sec3即上游响应超过3秒未到达recvfrom()返回-1-客户端查询总超时主循环里用clock_gettime(CLOCK_MONOTONIC, start)记录查询开始时间每次recvfrom()前检查是否已过5秒超时则直接返回SERVFAIL。更关键的是重试逻辑当上游超时程序不会立即放弃而是最多重试2次且每次重试前usleep(50000)50毫秒。这个50毫秒不是拍脑袋定的——它远小于客户端默认的1秒重试间隔dig的timeout1确保重试包能在客户端超时前发出。实测中把上游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是硬性要求项目使用C11标准核心依赖只有sys/socket.h、netinet/in.h、arpa/inet.h这些POSIX网络API以及fnmatch.h用于通配符匹配。编译命令极简g -stdc11 -O2 main.cpp -o dnsrelay但要注意fnmatch()在旧版glibc中可能不支持FNM_CASEFOLD标志所以最低要求是Ubuntu 18.04g 7.5或CentOS 8。如果遇到undefined reference to fnmatch只需加-lfnmatch链接选项。环境准备步骤如下1.安装基础工具sudo apt update sudo apt install build-essential tcpdump wireshark-cli2.关闭系统DNS服务sudo systemctl stop systemd-resolved sudo systemctl disable systemd-resolved避免53端口冲突3.赋予CAP_NET_BIND_SERVICE权限sudo setcap cap_net_bind_serviceep ./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.cpp里match_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 // 0OFF, 1ERROR, 2WARN, 3INFO然后在关键位置插入日志// 在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 -stdc11 main.cpp -o dnsrelay -lfnmatch在编译命令末尾添加-lfnmatch5.2 独家避坑技巧Wireshark抓包的黄金三步法Wireshark是DNS调试的终极武器但新手常抓不到有效包。我的经验是严格按三步操作1.过滤器前置启动Wireshark后先在过滤栏输入udp.port 53 ip.addr 127.0.0.1排除所有无关流量2.双向追踪右键任意一个DNS包 →Follow→UDP 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) | | | |---[ID0x1234] A? www.baidu.com--| | | |---[ID0x1234] A? www.baidu.com--| | |--[ID0x1234] A180.101.49.12--| |--[ID0x1234] A180.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::mapstd::string, std::vectoruint8_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 domainads.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套接字编程、请求/响应转发机制以及基础网络中间件开发思路。代码注释清晰变量命名规范调试信息友好方便学生完成课程报告中的协议分析、功能验证与扩展实验比如添加日志记录、缓存机制或黑白名单动态加载。本文还有配套的精品资源点击获取