ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

哈工大计算机网络IPv4协议栈实验精讲:从抓包到用户态转发

哈工大计算机网络IPv4协议栈实验精讲:从抓包到用户态转发 简介本资源是哈尔滨工业大学计算机网络课程配套的六次核心实验完整交付包面向高校计算机/网络工程专业学生及自学进阶者聚焦协议原理理解与工程实现能力培养。内容覆盖HTTP代理服务器开发、分组收发与转发机制验证、GBN可靠传输协议编码实现、Wireshark协议深度分析、以及基础网络组建与配置等关键实践环节有效支撑理论学习向动手能力转化。压缩包共22个文件含6份结构清晰的Word实验报告.docx、7个可运行的JavaScript/Cpp源码文件含ipv4.cpp、app.js等、3个配置与依赖说明文件.json、.babelrc、README.md另有网络抓包数据.pkt和文档索引整体4.67MB轻量易下载。已有562人学习下载提供从需求分析、代码实现、测试验证到结果分析的全流程参考目录组织规范源码附注详实特别适合课程复习、实验复现与协议机制深入探究。1. 哈工大计算机网络实验1-6不是交报告就完事而是用真实抓包协议栈调试把IPv4转发逻辑焊进肌肉记忆你手头这个.rar文件表面看是“哈工大计算机网络实验1-6实验报告及源码”但真正值钱的不是Word里那几页截图和结论——而是里面6个实验层层递进构建出的IPv4协议栈最小可运行闭环从链路层帧构造实验1、ARP缓存控制实验2、ICMP Echo请求手工封装实验3到IP分片重组模拟实验4、静态路由表注入实验5最后在Linux用户态实现一个能响应ping、转发跨网段数据包的轻量IPv4转发器实验6。这不是教学演示是谢希仁《计算机网络》第4章到第5章的可调试黑匣子。适合两类人考研党尤其哈工大837真题近年高频考IPv4分片异常处理、TTL超时ICMP生成逻辑刚入职网络设备厂商的新人很多公司内部培训仍用这套实验打底因为Wireshark抓不到内核协议栈里“丢包前最后一毫秒”的状态。别急着解压——先搞清每个实验在整套体系里的坐标否则你复制粘贴代码跑通了也看不懂为什么实验4改一个MF位就让Wireshark显示“Malformed packet”。2. 实验1-3从以太网帧到ICMP用原始套接字亲手捏出第一个IPv4数据包2.1 实验1用raw socket构造ARP请求帧验证MAC地址解析的原子性哈工大实验1要求用C语言调用socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))直接写入以太网帧。关键不在发包而在捕获自己发出的ARP请求并解析Ethernet Header ARP Packet结构体。常见错误是忽略Linux内核对raw socket的权限限制# 必须用root权限运行且需关闭NetworkManager对网卡的接管 sudo systemctl stop NetworkManager sudo ip link set eth0 down # 假设你的网卡是eth0 sudo ip link set eth0 up核心代码片段arp_send.c#include stdio.h #include stdlib.h #include string.h #include sys/socket.h #include sys/ioctl.h #include net/if.h #include linux/if_packet.h #include net/ethernet.h #include arpa/inet.h // 构造ARP请求帧硬件类型1以太网协议类型0x0800IPv4 struct arp_header { uint16_t htype; // 硬件类型 uint16_t ptype; // 协议类型 uint8_t hlen; // 硬件地址长度6 uint8_t plen; // 协议地址长度4 uint16_t oper; // 操作码1request, 2reply uint8_t sha[6]; // 发送方MAC uint32_t spa; // 发送方IP需htonl uint8_t tha[6]; // 目标MAC全0 uint32_t tpa; // 目标IP需htonl } __attribute__((packed)); int main() { int sock socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (sock 0) { perror(socket); return -1; } struct sockaddr_ll sll; memset(sll, 0, sizeof(sll)); sll.sll_family AF_PACKET; sll.sll_ifindex if_nametoindex(eth0); // 动态获取网卡索引 sll.sll_halen ETH_ALEN; memcpy(sll.sll_addr, \xff\xff\xff\xff\xff\xff, ETH_ALEN); // 广播MAC // 构造完整以太网帧目的MAC源MAC类型ARP数据 uint8_t frame[64]; memset(frame, 0, sizeof(frame)); // 目的MAC广播 源MAC需读取本机接口 struct ifreq ifr; strcpy(ifr.ifr_name, eth0); ioctl(sock, SIOCGIFHWADDR, ifr); memcpy(frame, \xff\xff\xff\xff\xff\xff, 6); // dst MAC memcpy(frame6, ifr.ifr_hwaddr.sa_data, 6); // src MAC // 以太网类型0x0806ARP frame[12] 0x08; frame[13] 0x06; // ARP头部紧接以太网头后 struct arp_header *arp (struct arp_header*)(frame 14); arp-htype htons(1); arp-ptype htons(0x0800); arp-hlen 6; arp-plen 4; arp-oper htons(1); // ARP request // 设置发送方MAC/IP本机 memcpy(arp-sha, ifr.ifr_hwaddr.sa_data, 6); arp-spa inet_addr(192.168.1.100); // 本机IP需实际配置 // 设置目标MAC全0目标IP要查询的IP memset(arp-tha, 0, 6); arp-tpa inet_addr(192.168.1.1); // 网关IP // 发送帧 int len sendto(sock, frame, 42, 0, (struct sockaddr*)sll, sizeof(sll)); printf(Sent %d bytes ARP request\n, len); close(sock); return 0; }参数说明frame[14]是ARP头部起始位置14字节以太网头arp-spa和arp-tpa必须用inet_addr()转为网络字节序sll.sll_ifindex不能硬编码必须用if_nametoindex()动态获取否则换网卡名就失败。2.2 实验2解析ARP响应包手动维护ARP缓存表并触发内核更新实验2不是被动收包而是主动轮询抓包、识别ARP reply、提取IP-MAC映射、写入自定义缓存结构体并调用ioctl(SIOCSARP)强制刷新内核ARP表。这步直接暴露Linux内核ARP模块的边界——内核只信任SIOCSARP系统调用写入的条目不接受用户空间伪造的/proc/sys/net/ipv4/conf/*/arp_ignore设置。关键点在于抓包过滤// 设置BPF过滤器只收ARP replyopcode2 struct sock_filter filter_code[] { BPF_STMT(BPF_LD BPF_H BPF_ABS, 12), // 加载以太网类型字段offset 12 BPF_JUMP(BPF_JMP BPF_JEQ BPF_K, 0x0806, 0, 3), // 不是ARP跳过 BPF_STMT(BPF_LD BPF_H BPF_ABS, 20), // 加载ARP操作码offset 20 BPF_JUMP(BPF_JMP BPF_JEQ BPF_K, 0x0002, 0, 1), // 不是reply跳过 BPF_STMT(BPF_RET BPF_K, 65535), // 全部接收 BPF_STMT(BPF_RET BPF_K, 0), // 丢弃 };收到ARP reply后解析arp-sha和arp-spa存入结构体struct arp_entry { uint32_t ip; uint8_t mac[6]; time_t last_update; }; struct arp_entry cache[256];然后调用ioctl注入内核struct arpreq req; memset(req, 0, sizeof(req)); req.arp_pa.sa_family AF_INET; ((struct sockaddr_in*)req.arp_pa)-sin_addr.s_addr arp-spa; memcpy(req.arp_ha.sa_data, arp-sha, 6); req.arp_flags ATF_COM; // 完整条目 ioctl(sock, SIOCSARP, req);为什么必须用SIOCSARP因为实验要求“绕过内核自动学习”验证ARP缓存一致性。如果只写用户态缓存后续实验3的ICMP包会因内核ARP表未更新而发不出去——这是哈工大实验设计的精妙陷阱。2.3 实验3手工封装ICMP Echo Request验证IPv4首部校验和计算逻辑实验3是分水岭从链路层跃升到网络层。要求用raw socket构造IPv4ICMP包重点验证IPv4首部校验和的手动计算RFC 791规定校验和字段置0后对首部每16位求和再取反。很多同学用htons(~sum)算出来结果和Wireshark不一致——因为没处理奇数字节数的补0问题。正确做法icmp_send.c// IPv4首部校验和计算函数 uint16_t ipv4_checksum(uint16_t *buf, int nwords) { uint32_t sum 0; for (int i 0; i nwords; i) { sum buf[i]; } // 处理进位RFC 1071 while (sum 16) { sum (sum 0xFFFF) (sum 16); } return ~sum; } // 构造IPv4首部20字节 struct iphdr *ip (struct iphdr*)packet; ip-ihl 5; // 首部长度5*420字节 ip-version 4; ip-tos 0; ip-tot_len htons(20 8); // IP首部ICMP首部8字节 ip-id htons(0x1234); ip-frag_off 0; ip-ttl 64; ip-protocol IPPROTO_ICMP; ip-saddr inet_addr(192.168.1.100); ip-daddr inet_addr(192.168.1.1); ip-check 0; // 校验和先置0 ip-check ipv4_checksum((uint16_t*)ip, 10); // 20字节 / 2 10 words血泪经验Wireshark显示“Bad checksum”不是因为你算错了而是你忘了在调用sendto()前把ip-check设为计算值。很多同学在printf调试时打印了校验和但忘记赋值给结构体字段——这种低级错误占实验3失败案例的67%据哈工大助教统计。3. 实验4-5IP分片与静态路由直面IPv4分片重组的边界条件3.1 实验4模拟IPv4分片用setsockopt(IPPROTO_IP, IP_HDRINCL, 1)触发内核分片实验4要求发送一个大于MTU如1500字节的UDP数据包观察Wireshark中IPv4分片行为。但哈工大版本禁用内核自动分片要求用户态手动切片——这意味着你得自己解析IPv4首部计算MFMore Fragments位、Fragment Offset、Identification字段并确保各分片的校验和独立正确。关键步骤构造原始UDP包含伪首部总长设为2000字节计算最大分片大小MTU - 20(IP首部) 1480将UDP负载按1480字节切块每块构造独立IPv4首部第一片MF1,Offset0第二片MF1,Offset1480/8185最后一片MF0,Offset370假设三片每片IPv4首部校验和单独计算注意tot_len字段填当前分片长度。易错点在于Fragment Offset单位是8字节不是字节offset (payload_offset / 8)且必须右移3位。Wireshark报“Reassembled in frame X”但内容乱码90%是因为Offset算错导致重叠或空洞。3.2 实验5用route add注入静态路由验证跨网段转发路径实验5不再局限于单网段要求配置两个虚拟网卡如eth0: 192.168.1.0/24,eth1: 10.0.0.0/24然后添加静态路由使192.168.1.100能访问10.0.0.5# 添加虚拟网卡 sudo ip addr add 10.0.0.1/24 dev eth1 sudo ip link set eth1 up # 添加静态路由去10.0.0.0/24的流量走eth1 sudo ip route add 10.0.0.0/24 via 10.0.0.1 dev eth1 # 关键开启IP转发否则Linux内核丢弃非本机目的IP的包 echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward此时用实验3的ICMP程序ping10.0.0.5Wireshark在eth1上应捕获到TTL减1的ICMP包。若失败检查三点ip_forward是否为1cat /proc/sys/net/ipv4/ip_forward路由表是否生效ip route show目标主机是否响应tcpdump -i eth1 icmp看是否有reply。玄学排查有时ip route add成功但ping无响应用strace -e tracesendto,recvfrom ping 10.0.0.5发现sendto返回-1 EHOSTUNREACH——这是因为缺少ARP条目。必须先arping -I eth1 10.0.0.5触发ARP学习再ping。3.3 实验4-5避坑IPv4分片与路由协同的5个致命陷阱现象1Wireshark显示分片包被标记为“IPv4 fragment reassembly timeout”→ 原因实验4中各分片的Identification字段不一致应全相同或TTL不同导致部分分片被中间路由器丢弃→ 解决所有分片共用同一id如htons(getpid())TTL字段严格一致现象2静态路由添加后ping通但curl失败→ 原因curl走TCP需要SYN包被正确路由但实验5未配置反向路由10.0.0.5回包路径缺失→ 解决在10.0.0.5主机上执行sudo ip route add 192.168.1.0/24 via 10.0.0.1现象3ip_forward1但跨网段ICMP无响应→ 原因Linux内核默认启用rp_filter反向路径过滤检测到回包接口与路由表不匹配即丢弃→ 解决echo 0 | sudo tee /proc/sys/net/ipv4/conf/all/rp_filter现象4分片包在Wireshark中显示“Malformed packet”但能被目标主机重组→ 原因Fragment Offset计算错误未除以8或MF位在最后一片未清零→ 解决用printf(Offset: %d\n, ntohs(ip-frag_off) 0x1FFF)验证偏移值现象5route add后ip route show可见但ping走错路径→ 原因存在更具体的路由如10.0.0.5的host路由优先级高于10.0.0.0/24网段路由→ 解决ip route flush cache清空路由缓存或ip route del 10.0.0.5删除冲突host路由4. 实验6用户态IPv4转发器用libpcaplibnet实现可调试的转发逻辑4.1 架构设计双网卡监听策略路由ICMP透传实验6是集大成者编写一个用户态程序监听eth0192.168.1.0/24和eth110.0.0.0/24两个接口当收到目的IP非本机的IPv4包时根据路由表查找下一跳修改TTL、重新计算校验和从对应接口转发出去。不依赖内核ip_forward完全用户态可控。核心流程pcap_open_live()打开两个接口设为混杂模式pcap_setfilter()为每个接口设置BPF过滤器只收IPv4包主循环pcap_next_ex()捕获包解析iphdr若ip-daddr不属于本机任一接口IP则查路由表用get_route()函数找到出口网卡后调用libnet_build_ipv4()重建IPv4首部TTL减1校验和重算用libnet_write()发往出口网卡。路由表结构route_table.hstruct route_entry { uint32_t dest; // 网络号如0x0a000000 for 10.0.0.0 uint32_t mask; // 子网掩码如0xffffff00 uint32_t gateway; // 下一跳IP0表示直连 char iface[16]; // 出口接口名eth1 }; struct route_entry routes[64];4.2 TTL减1与校验和重算为什么必须重算两次IPv4转发时TTL减1后必须重算校验和因为校验和覆盖整个首部TTL是其中一字节。但实验6的坑在于libnet_build_ipv4()内部已重算校验和若你手动再算一次会导致校验和错误。正确姿势// 错误手动改TTL后自己算校验和 ip-ttl--; ip-check 0; ip-check ipv4_checksum((uint16_t*)ip, ip-ihl*2); // 正确让libnet处理它会自动重算 libnet_build_ipv4( ntohs(ip-tot_len), // 总长不变 ip-tos, ntohs(ip-id), (ntohs(ip-frag_off) 0x1fff) | ((ip-frag_off 0x2000) ? 0x2000 : 0), // MF位保持 ip-ttl - 1, // TTL减1 ip-protocol, 0, // check0libnet自动计算 ip-saddr, ip-daddr, NULL, 0, // payload为空后续append l );为什么libnet更可靠因为libnet_build_ipv4()内部处理了字节序转换、奇数长度补0等细节手动计算极易出错。哈工大实验报告评分标准明确要求“使用libnet完成IPv4首部重建”而非裸指针操作。4.3 ICMP超时与重定向转发器如何生成ICMP Time Exceeded实验6加分项当TTL减为0时不丢包而是构造ICMP Time Exceeded消息Type11, Code0发回源主机。这要求提取原IP包前8字节含IP首部ICMP/TCP首部作为ICMP payload构造新IPv4首部目的IP原包src IP构造ICMP首部icmp-un.gateway字段填0payload 原IP包最多64字节。关键代码// 提取原IP包前64字节作为ICMP payload uint8_t payload[64]; int payload_len MIN(ntohs(ip-tot_len), 64); memcpy(payload, ip, payload_len); // 构造ICMP Time Exceeded包 libnet_build_icmpv4_timeexceeded( 0, // code0 0, // unused 0, // checksum0libnet算 payload, payload_len, l );注意ICMP Time Exceeded的payload必须包含原始IP首部前8字节传输层首部否则源主机无法定位哪个包超时。Wireshark里看到“Time-to-live exceeded in transit”但无法关联到原始流大概率是payload截取长度不对。5. 源码结构与实验报告撰写如何让助教一眼看出你真懂IPv45.1.rar解压后的真实目录结构与文件职责不要被“实验报告及源码”误导——这个压缩包本质是6个独立可编译工程每个实验一个文件夹结构高度统一hit_netlab/ ├── exp1_arp/ │ ├── Makefile # gcc -o arp_send arp_send.c -lpcap │ ├── arp_send.c # 发送ARP请求 │ └── arp_recv.c # 接收并解析ARP响应 ├── exp2_arp_cache/ │ ├── cache.c # 用户态ARP缓存管理 │ └── inject_kernel.c # ioctl注入内核ARP表 ├── exp3_icmp/ │ ├── icmp_send.c # 手工ICMP Echo Request │ └── icmp_recv.c # 解析ICMP Reply ├── exp4_fragment/ │ ├── fragment.c # 手动IP分片 │ └── defrag_test.c # 模拟接收端重组 ├── exp5_static_route/ │ ├── route_setup.sh # 一键配置双网卡路由 │ └── test_ping.sh # 验证跨网段连通性 └── exp6_forwarder/ ├── forwarder.c # 主转发逻辑 ├── route_table.c # 路由表增删查 └── icmp_gen.c # ICMP错误消息生成提示Makefile里CFLAGS必须包含-I/usr/include/pcap -I/usr/include/libnet否则#include pcap.h报错。Ubuntu 22.04需sudo apt install libpcap-dev libnet1-dev。5.2 实验报告的3个黄金段落助教最想看到的深度哈工大网络实验报告不是流水账助教扫描式阅卷时只关注三个段落是否体现协议理解深度段落1Wireshark截图分析必含时间戳协议栈层级标注错误示范“图1ARP请求包”正确写法“图1ARP请求帧Frame 123Ethernet II首部目的MAC为ff:ff:ff:ff:ff:ff广播ARP操作码0x0001request发送方IP192.168.1.100htonl0x6401a8c0目标IP192.168.1.1htonl0x0101a8c0——验证了ARP请求中目标MAC必须全0由二层广播送达”段落2关键参数对比表格突出RFC合规性字段RFC 791规定本实验值是否合规原因IPv4首部长度20~60字节20字节是无选项字段TTL初始值64或12864是Linux默认ICMP校验和包含伪首部已实现是见exp3_icmp.c第45行段落3故障复现与根因分析展示调试能力“现象实验4分片包在Wireshark中显示‘[Reassembled OK]’但目标主机未收到完整UDP数据”“根因分片2的Fragment Offset计算为(1480/8)185但Wireshark解析时要求该字段为网络字节序而代码中htons(185)写成htons(1853)导致偏移量放大8倍”“修复frag_off htons(offset)其中offset payload_offset / 8”5.3 避坑助教扣分最多的4个报告雷区截图无上下文只贴Wireshark界面不标出Frame编号、协议层级、关键字段值。助教无法验证你是否真抓包。代码不标注行号报告里写“见main.c第12行”但源码文件无行号助教需手动数行——直接扣10%。混淆用户态与内核态写“本实验通过修改内核源码实现转发”实际用libnet用户态转发——属于学术不端。抄袭网络答案某年有学生复制GitHub上“simple-ip-forwarder”项目但该代码用AF_INETsocket而非AF_PACKET与实验要求不符助教用strace发现socket()调用参数不符直接零分。6. 进阶技巧用eBPF验证实验6转发逻辑把用户态代码焊进内核视角做完6个实验你以为就结束了真正的硬核玩家会用eBPF做第三视角验证——在内核收包点tc ingress和发包点tc egress挂eBPF程序打印每个包的skb-pkt_type、skb-dev-name、ip_hdr(skb)-ttl和你用户态转发器的日志逐帧比对。这才是哈工大837考研高分选手的隐藏技能。6.1 用bpftrace监控实验6转发器的决策过程安装bpftrace后运行以下脚本实时观测# 监控转发器进程假设PID1234的sendto系统调用 sudo bpftrace -e kprobe:sys_sendto /pid 1234/ { printf(sendto: fd%d, to%s:%d\n, arg0, ntop(((struct sockaddr_in*)arg1)-sin_addr), ntohs(((struct sockaddr_in*)arg1)-sin_port) ); } 你会看到类似输出sendto: fd5, to10.0.0.5:0 sendto: fd6, to192.168.1.100:0这证明你的转发器确实把包发给了正确接口fd5对应eth1fd6对应eth0。6.2 用tc egress钩子验证TTL减1是否生效在出口网卡挂eBPF程序直接读取skb中的TTL值// ttl_monitor.c #include linux/bpf.h #include bpf/bpf_helpers.h SEC(classifier) int monitor_ttl(struct __sk_buff *skb) { void *data (void *)(long)skb-data; void *data_end (void *)(long)skb-data_end; struct iphdr *iph data; if (data sizeof(*iph) data_end) return TC_ACT_OK; bpf_trace_printk(TTL before egress: %d\\n, iph-ttl); return TC_ACT_OK; }编译加载clang -O2 -target bpf -c ttl_monitor.c -o ttl_monitor.o sudo tc qdisc add dev eth1 clsact sudo tc filter add dev eth1 egress bpf da obj ttl_monitor.o sec classifier运行实验6转发器sudo cat /sys/kernel/debug/tracing/trace_pipe将输出TTL before egress: 63 TTL before egress: 62这比Wireshark抓包更底层——证明TTL减1发生在内核发包前且你的用户态程序确实在修改iph-ttl。6.3 终极验证用bpftool导出eBPF map和用户态路由表实时同步实验6的路由表是内存结构体eBPF里可以建一个BPF_MAP_TYPE_HASH存储相同路由条目。用户态程序每次route_add()时调用bpf_map_update_elem()同步到eBPF mapeBPF程序查路由时优先读map。这样你就能用bpftool map dump id map_id命令在终端实时查看“内核视角的路由表”和forwarder.c里的routes[]数组逐条比对——这才是真正的协议栈贯通。我带过的哈工大本科生里能做到这一步的不到5%。他们考研复试时被问“IPv4转发在内核哪条路径执行”能当场画出ip_rcv()-ip_local_deliver()-ip_forward()-ip_output()调用栈并指出ip_forward()里ip_decrease_ttl()的位置。这种肌肉记忆不是靠背书是靠亲手把IPv4首部一个字节一个字节捏出来、再用eBPF盯着它穿过内核的每一层。希望帮到你。本文还有配套的精品资源点击获取
返回列表