ARTICLE DETAIL

资讯详情

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

Mininet实验全解析:从Hub、交换机到路由器与防火墙

Mininet实验全解析:从Hub、交换机到路由器与防火墙 简介南京大学2024年计算机网络课程实验资源将七个递进式实验打包成zip压缩包面向高校网络方向学习者与实践者可帮助强化TCP/IP协议栈、路由交换、网络安全等核心知识的掌握。包体共38个文件以29个Python脚本为主力配合txt参数与配置说明、shell启动脚本、pcap抓包样本及README实验指南整体仅39KB非常轻量。截至目前已有160人学习下载适合课程作业、实验报告撰写与考前冲刺。实验从lab_1的hub转发仿真延伸到lab_7的防火墙规则设计覆盖自研交换机、路由器、可靠数据传输、流量分析与middlebox策略等完整项目每项均提供mininet拓扑脚本、自动化测试用例与结果验证README逐项拆解实验目的、步骤和排错思路。学习者可获得网络协议行为观察、抓包验证、路由算法实现和防御策略配置的系统训练实现理论与动手能力的同步提升。1. NJU-2024 计算机网络课程实验七份实验串起一整条网络链路如果你在找一份能真正动手跑通的网络实验资源这套南京大学 2024 年计算机网络课程实验的压缩包值得花一晚上拆开看。它从最小的以太网 Hub 开始一路做到交换机、静态路由、NAT、BGP、TCP 性能干扰实验和防火墙规则引擎正好覆盖了“物理层往上到应用层”的主干脉络。相比于拿着谢希仁的教材空背概念这套实验把每个协议都变成了一个需要你亲手写代码的 Python 模块跑在 Mininet 里流量和抓包都是真实发生的。不管是正在补课程作业和实验报告的学生还是想系统过一遍网络基础的自学者这套资源都能直接拿来当复现蓝本踩坑的价值甚至比答案本身更大。2. 二层实验的起点lab_1 Hub 和 lab_2 交换机把转发从无脑做到有记忆2.1 先立起 Mininet 环境start_mininet.py 能做什么这套实验的核心运行环境是 Mininet所有 start_mininet.py 脚本都是用来定义网络拓扑的。以 lab_1 为例你大概率会看到这样一段典型内容#!/usr/bin/env python from mininet.topo import Topo from mininet.net import Mininet from mininet.cli import CLI class MyTopo(Topo): def build(self): h1 self.addHost(h1) h2 self.addHost(h2) h3 self.addHost(h3) s1 self.addSwitch(s1) self.addLink(h1, s1) self.addLink(h2, s1) self.addLink(h3, s1) if __name__ __main__: topo MyTopo() net Mininet(topotopo, controllerNone) net.addController(c0) net.start() CLI(net) net.stop()这里的controllerNone是关键实验里的 Hub 和 Switch 不是靠 OpenFlow 控制器去管而是每个 host 自己用 Python 脚本处理以太网帧。addSwitch在 Mininet 里默认会启动一个内核交换模块但你整套实验跑的是用户态写的myhub.py或myswitch.py所以拓扑里并不会真正依赖传统的交换机控制器逻辑。运行方式并不复杂cd lab_1 sudo python start_mininet.py启动后进入 Mininet CLI先pingall确认链路通然后另开一个终端窗口去指定 host 的 namespace 里跑myhub.py。这里有个细节Mininet 每个 host 都是独立网络命名空间脚本必须挂在对应的 host 上执行直接在你宿主机里python myhub.py是无效的。我一般是这样起 hub 的# 在 Mininet CLI 里 xterm h1 # 在 h1 的 xterm 里继续 python myhub.pyxterm 把 hub 进程挂到 h1 的命名空间里之后 h2、h3 之间做的任何 ping 或 UDP 广播都会经过这一层用户态转发。这也解释了为什么 lab_1、lab_2 里每个目录都带 start_mininet.py——拓扑结构对实验结论影响极大hub 是广播域全通交换机是学习后按表转发拓扑相同才能对比出二者的行为差异。2.2 lab_1myhub.py 收到直接全体广播Hub 的实验逻辑是所有实验里最朴素的。以太网 Hub 工作在物理层它不关心 MAC 地址也不做任何学习唯一动作就是把收到的每个帧往除接收端口以外的所有端口转发。代码骨架通常是这样的#!/usr/bin/env python import sys def handle_packet(port, pkt): # 以太网帧最小 64 字节pkt 是 bytes # 这里不需要解析任何头部直接原样复制 for out_port in [1, 2, 3]: if out_port ! port: send_packet(out_port, pkt) def main(): # 绑定到网络接口阻塞接收帧 while True: port, pkt recv_packet() handle_packet(port, pkt)send_packet和recv_packet是你需要自己补的底层原语一般通过 raw socket 绑定到对应 Mininet host 的 veth 接口上实现。关键点在于handle_packet里没有任何条件判断只要入口端口不等于出口端口一律转发。一个广播帧进来其余所有端口都会收到一份这天然构成了广播风暴的物理基础。配套的lab_1.pcap是这个实验的参考抓包我拿到它后的第一反应不是关掉浏览器去看解析而是先把它当成“标准答案”比对。用 Wireshark 打开 pcap过滤arp和icmp你会看到同一广播帧被复制成多份的痕迹。Hub 场景下任何一个 ARP 请求都会出现在所有链路上次数是 1 对多。判断自己写没写对的依据不是只看 pingall 通没通而是抓包后看有没有出现“同源 MAC 同一帧重复出现在多个接口”的现象。如果只有一份说明 hub 实际没转发出去只是 host 自己回了 ARP。2.3 lab_2myswitch.py 从泛洪进化为 MAC 学习lab_2 是整个课程第一个有“智能”的实验交换机要从 Hub 的无差别转发变成按 MAC 地址表转发。核心数据结构是转发映射逻辑分三步查表命中则单播转发查不到则泛洪同时记录源 MAC 和入端口。class MySwitch: def __init__(self, timeout30): self.mac_table {} # mac - (port, timestamp) self.timeout timeout def learn(self, src_mac, in_port): # 每收到一帧先把源 MAC 和入端口记下来 self.mac_table[src_mac] (in_port, time.time()) def lookup(self, dst_mac): entry self.mac_table.get(dst_mac) if entry and time.time() - entry[1] self.timeout: return entry[0] # 返回出端口 return None # 没查到泛洪 def handle(self, in_port, pkt): src_mac parse_src_mac(pkt) dst_mac parse_dst_mac(pkt) self.learn(src_mac, in_port) out_port self.lookup(dst_mac) if out_port is not None: send_packet(out_port, pkt) else: for p in range(1, 4): if p ! in_port: send_packet(p, pkt)注意timeout这个参数。MAC 地址表如果不老化终端一旦换网口迁移交换机就会一直把帧送往老旧端口直到表项被撑爆。这里的默认值我一般设为 30 到 60 秒之间课程测试脚本mytests.py会直接操纵这个类你构造测试时必须主动模拟以下时序h1 发帧 → 表学到 → 同目标再发 → 单播转发等待超时后再发 → 重新泛洪。如果不带老化测试这个实现的可用性就打折扣了。2.4 策略变体LRU、TO、Traffic实验的进阶玩法lab_2 目录里除了myswitch.py还有三组变体和对应测试myswitch_lru.py、myswitch_to.py、myswitch_traffic.py。它们解决的是同一个问题当表容量有限、无法装下所有 MAC 时该淘汰谁。变体文件策略描述适用场景说明myswitch.py普通超时老化到期删除默认教学版本适合看基本行为myswitch_lru.py最近最少使用淘汰容量满先踢旧的表空间受限时优先保留活跃终端myswitch_to.py基于超时机制的强制回收强调过期表项的及时清理myswitch_traffic.py结合流量统计优化转发顺序/丢弃看流量热度对表项存留的影响这个设计很有课程实验的味道同一功能骨架换了淘汰策略行为差异集中在极端场景。比如mytests_lru.py会故意制造“表满后再插入新地址”的情况LRU 版本淘汰最久未用的项而普通超时版本可能因为还没到期而继续保留旧项导致新地址被泛洪。跑这些测试时不要只看断言过没过把每次淘汰的表项打印出来你才能真正理解不同策略对转发路径的影响。实验报告里如果能画一张“不同策略下丢包率/泛洪次数对比”的小表这部分就完全拿捏了。3. 三层往上走myrouter.py 在 lab_3、lab_4、lab_5 里三次变形3.1 myrouter 的转发核心校验、TTL 与最长前缀匹配lab_3、lab_4、lab_5 三个实验共用同一个myrouter.py文件但每次需要改动的函数不同这种“一个骨架多次变身”的作业设计比单独写三个项目更能映射真实研发里迭代代码的状态。路由器的基本职责是收到 IP 包 → 校验头部 → TTL 减一 → 查 forwarding table → 改 MAC 头后从对应端口发出。class Router: def __init__(self): self.routes [] # [(prefix, mask_len, next_hop, iface)] self.arp_cache {} # ip - mac self.load_routes(forwarding_table.txt) def route_lookup(self, ip): best None for prefix, plen, next_hop, iface in self.routes: if ip_matches_prefix(ip, prefix, plen): if best is None or plen best[1]: best (next_hop, iface, plen) return best def handle_ip_packet(self, pkt): ip_header parse_ip(pkt) if invalid_checksum(ip_header): return ip_header.ttl - 1 if ip_header.ttl 0: send_icmp_time_exceeded(ip_header) return route self.route_lookup(ip_header.dst) if route is None: send_icmp_host_unreachable(ip_header) return next_hop, iface, _ route dst_mac self.resolve_arp(next_hop, iface) send_frame(iface, dst_mac, pkt)这里有个很容易被新手忽略的点TTL。很多人在静态路由实验里只实现了查表和改写 MAC把 TTL 判断丢掉了于是lab_3routertests.py里“路由回环超时”这一类的用例就过不了。IP 头里的 TTL 每次经过路由器必须减一减完小于等于 0 时回 ICMP Time Exceeded否则包在一个环路里永远转圈。校验和也是另一个高发坑点IPv4 头部 TTL 变过之后校验和必须重新计算否则对端直接丢包。3.2 forwarding_table.txt静态路由怎么写、怎么读路由器的转发行为完全由forwarding_table.txt驱动。这种文件格式虽然没有强标准但常见写法是每一行表示一条路由包含目标网络、掩码长度和下一条地址10.0.1.0 24 10.0.1.1 10.0.2.0 24 10.0.2.1 0.0.0.0 0 10.0.0.254解析时按空格分割prefix和mask_len一起构成路由前缀next_hop决定该把包交给哪个下一跳。第三行0.0.0.0/0是默认路由在查表引擎里对应最长前缀匹配的“兜底”。route_lookup必须选掩码最长的那条而不是第一个匹配的否则两条路由同时命中时会选错下一跳。做静态路由实验时我自己会额外在 forwarding_table 每条记录后面加注释风格的冗余字段比如出口接口名避免在 Mininet 里对着 veth 名猜数。测试脚本lab_4routertests.py里最常见的一类失败就是“路由表里配的 next_hop 跟 Mininet 拓扑里实际连到的主机 IP 对不上”导致 ARP 解析不到 MAC。配置文件不是拿来 readme 说说就完事的每个网段都得能 ping 通才算数。3.3 lab_4一改函数就变 NAT——端口映射和回程包lab_4 的重点不在转发核心而在于把同一个路由器骨架改造成 NAT。NAT 的实质是改写包的地址和端口同时维护一张映射表。出向包从内网 10.0.x.x 出来源地址换成公网 IP源端口换成随机高位端口回程包则靠这张映射表反向还原。class NATRouter(Router): def __init__(self): super().__init__() self.nat_table {} # (src_ip, src_port) - global_port self.port_map {} # global_port - (src_ip, src_port) def translate_outbound(self, pkt): src_ip, src_port, dst_ip, dst_port parse_tcp_udp(pkt) if (src_ip, src_port) not in self.nat_table: gport self.get_free_global_port() self.nat_table[(src_ip, src_port)] gport self.port_map[gport] (src_ip, src_port) else: gport self.nat_table[(src_ip, src_port)] rewrite_src(pkt, self.public_ip, gport) def translate_inbound(self, pkt): dst_ip, dst_port parse_dst(pkt) if dst_port in self.port_map: src_ip, src_port self.port_map[dst_port] rewrite_dst(pkt, src_ip, src_port)NAT 最大的学习点是状态它不是一个无状态转发器它必须记得自己改写过的每条流。lab_4 里你很容易遇到“内网访问外网通、外网主动连内网不通”的现象原因就是nat_table里只有出向映射没有对外开放的服务端口。如果你想让外网能主动连进来就得在公网侧手动预留一个全局端口并把它固定映射到某台内网主机这是很多课程作业里“加分项”级别的进阶需求。3.4 lab_5从静态表到 BGP网关注册新的 next hoplab_5 的文件结构跟 lab_3 非常像同一个myrouter.py、同一个forwarding_table.txt外加一个lab5_routertests.py。换汤不换药的背后是一个递进设计——路由器转发引擎本身不关心路由表是怎么来的静态配置、NAT 改写、BGP 学习最终吐出来的都是“目标网络 → 下一跳”的映射。lab_5 我理解为把转发引擎接入一个简单的 BGP 模拟环境你对端“AS”通过某种方式宣告网段你的 myrouter 收到 BGP Update 后把新路由写进 forwarding_table再由同一个route_lookup生效。也就是说上一节forwarding_table.txt的格式在 lab_5 里依然被复用但表的来源变成动态学习了。在这个实验里建议把lab5_routertests.py当成一个 black-box 回归测试来用不用管 BGP 协议细节只记住一点测试脚本检查的一定是“某网段被宣告后路由器能正确转发到新 next_hop”。你真正要保证的是route_lookup能正确处理新增路由、以及掩码更短的新路由会不会覆盖原来更精确的路由。BGP 本身就强调最长前缀匹配和路由优先级这两点在 lab_5 里都能直接体现。4. TCP 性能实验和防火墙规则lab_6 和 lab_7 的两个“非典型”实验4.1 lab_6 里三个角色blaster、blastee、middlebox 怎么配合lab_6 不是让你写 TCP 协议栈而是模拟真实路径上的网络损伤。三个 Python 文件对应三个角色blaster.py是发送端blastee.py是接收端middlebox.py插在两者之间负责按参数对流量做丢包、延迟、限速之类的干扰。# middlebox.py 的核心处理循环 def handle_packet(self, pkt, iface): if random.random() self.drop_rate: return # 直接丢弃模拟丢包 if self.delay_ms 0: time.sleep(self.delay_ms / 1000.0) # 模拟延迟 if self.bandwidth_kbps 0: self.tokens - len(pkt) * 8 / 1000.0 if self.tokens 0: time.sleep(-self.tokens / self.bandwidth_kbps) send_packet(iface, pkt)这个实验把 TCP 的很多“玄学”问题具象化了。你不写拥塞控制算法但能观察到 blaster 发出去的包在经过 middlebox 丢包后blastee 收到的序列号出现空洞再高丢包率下还能看到大量 Dup ACK 和重传这正是 TCP 拥塞窗口触发减半的典型信号。比如把丢包率从 0 调到 1%观察同样的文件传输时间变化这个数据是报告里最直观的结果。4.2 参数文件怎么控制网络blaster_params.txt、middlebox_params.txt这三个实验能跑出什么结果全看参数文件怎么填。blaster_params.txt控制发送端的流量特征middlebox_params.txt控制中间盒的损伤参数blastee_params.txt控制接收端的监听方式。常见字段包括参数典型值作用说明发包数量1000总发包数越大统计越稳定发包间隔0.001s影响发送速率过小会打满带宽丢包率 drop_rate0.010.01 即 1%越高越容易触发 TCP 重传延迟 delay_ms50模拟 RTT 增加直接拉大传输时长带宽限制 bandwidth1000以 kbps 为单位低于流量速率会造成排队接收端口8080blastee 监听端口必须和 blaster 目标一致运行命令没有太多花样但有个顺序要求先启动blastee.py再启动middlebox.py最后启动blaster.py。如果顺序反了blaster 发出去的初始 SYN 没人应答TCP 握手都完不成。我第一次跑的时候就把 middlebox 参数里的丢包率设成 0.5跑出来的 pcap 里全是重传一度以为是代码坏了后来把参数降回 0.01 才正常。TCP 对随机丢包远比想象中敏感这也是这个实验最想让你理解的事情。4.3 lab_7 防火墙规则文件、命中逻辑、回环测试lab_7 明显是前面所有实验的“加固版”。firewall.py需要按firewall_rules.txt里的规则对进出主机的网络包做允许或丢弃。规则文件的格式各课程可能不一样但核心一定包含方向、协议、地址和动作allow in tcp 10.0.0.2 any 10.0.0.1 80 deny in tcp 10.0.0.0/24 any any any allow out tcp 10.0.0.1 80 any any解析规则时按行处理每行拆成动作 方向 协议 源地址 源端口 目标地址 目标端口然后用一个匹配函数逐条比对。遇到首条匹配就返回对应动作注意规则顺序先出现者优先这一点和 iptables 的链式匹配一致。firewalltests.py和impairetest.py测试的就是这条链的判罚是否跟预期一致。实现防火墙时最容易踩的坑是协议判断写死成 TCP只要拿到非 TCP 包就丢掉结果 ICMP 的 ping 全都不通连带的 ARP 也可能受影响。正确做法是先判断协议类型再决定去解析哪个段TCP/UDP 看端口ICMP 看类型。4.4 与真实 HTTP 服务配合start_webserver.sh 和 wwwlab_7 里还有一个www目录和start_webserver.sh这是告诉你防火墙实验要跟真实的上层服务联动。www里一般放着静态网页文件脚本用 Python 内置 HTTP Server 把服务拉起来#!/bin/bash cd www python3 -m http.server 80 echo webserver started on :80把 Web Server 挂在 h1 上然后在另一台 host 上用curl请求 h1 的 80 端口。防火墙规则如果只允许 80 端口 TCP 通过那 curl 就能拿到页面如果再在规则文件里加一条deny in tcp any any any 80curl 会直接卡住直到超时而 ping 却仍然能通。这种“服务协议相比 ICMP 更依赖端口策略”的体验比单纯跑测试脚本更能让人理解防火墙规则设计的目标。5. 常见问题与避坑排查复现这套实验最容易翻车的五个点5.1 问题一Mininet 里 pingall 时好时坏第一次能通重跑就全挂现象同一套 start_mininet.py删掉重新sudo python start_mininet.py后host 之间 ping 不通报错里偶尔出现 address already in use。原因上一个 Mininet 实例没有完全退出残留的 veth 接口和进程占用着同一批端口号和 IP。Mininet 里的exit有时不会把所有 clutter 清干净。解决先执行sudo mn -c清空旧拓扑再确认没有残留的 myhub/myswitch/myrouter 进程sudo pkill -f myrouter.py。然后重新启动拓扑。我后来养成习惯每次换 lab 前都无脑sudo mn -c基本能避免九成环境问题。5.2 问题二myrouter 能 ping 通同网段但跨网段 IP 包就是出不去现象h1 和自己的网关路由器能通但 h1 ping h2 永远不通抓包只看到请求看不到响应。原因路由器转发流程里没有实现 ICMP 回包处理或者 TTL 减一后校验和没有重算包到达目标后被直接丢弃。解决在handle_ip_packet里单独加 ICMP Echo 分支收到目的地址为本机的 ICMP 请求时构造 Reply 包同时重新计算 IP 头校验和。跑lab_3routertests.py时重点看 TTL 和校验和两个测试用例能过这两个基本就能跨网段了。5.3 问题三NAT 实验里内网访问外网正常外网却连不回内网服务现象内网 host 用 curl 访问外网 server 能拿到页面但从外网主动 telnet 内网 IP 的某个端口连接直接超时。原因NAT 只翻译了出向连接没有生成外网到内网的固定入向映射。nat_table只存了连接发起方信息服务器从公网来的时候找不到对应转发项。解决给 NAT 增加入向端口映射。预先分配一个全局端口比如 60001固定映射到内网 host 192.168.1.2 的 8080 端口。translate_inbound命中这个静态映射后再改写目标地址防火墙相关的冲突另说但 NAT 这题的核心就是把这张映射表想明白。5.4 问题四防火墙测试时 Web 服务起不来curl 报 403 或 connection refused现象start_webserver.sh执行成功但另一台 host 用curl http://10.0.0.1返回 connection refusedping 却能通。原因防火墙规则把回程方向或 TCP SYN 直接 deny 掉了。很多实现只检查入方向忘了出方向响应包也可能被规则拦掉还有实现只匹配 TCP 协议但没区分 ESTABLISHED 和 NEW导致连接建立阶段就被拒。解决先不加任何规则用 curl 验证 Web 服务本身正常再加一条allow in tcp any any any 80和对应的allow out tcp any any any从最小允许集开始逐渐收紧。跑firewalltests.py时注意规则顺序把允许规则放在 deny 规则前头。5.5 问题五lab_6 跑起来只有 blastee 在收包数据吞吐几乎为零现象blastee 输出显示只收到了几百字节中间一大段序列号空洞重传风暴严重甚至连接直接 reset。原因middlebox 的丢包率和延迟参数设得太离谱。我见过有人把丢包率写成 0.5也就是 50% 随机丢包这在现实网络里已经是灾难级损伤再大的拥塞窗口也没用。解决从中间盒参数入手先把 drop_rate 改成 0.0、delay_ms 改成 0 跑通链路确定 blaster/blastee 本身没问题再逐步加大丢包率每步只改一个变量。正常情况下 1% 丢包已经开始触发重传5% 丢包足以让吞吐掉一个数量级。参数文件改完记得重启 middlebox 再测不然改动不生效。6. 验证习惯怎么确认自己的解法不是“碰巧能跑”这套实验最怕的不是写不出代码而是跑了一次python mytests.py全绿就交差结果换一台机器、改一个拓扑参数就废掉。我后来强制自己每次实验都走一遍固定验证链路分享出来给你参考。第一关是单元测试。每个 lab 都带了以tests.py结尾的脚本先原封不动跑一遍cd lab_2 python mytests.py python mytests_lru.py python mytests_to.py这些测试脚本不是“验收答案”它们往往只覆盖了若干固定场景尤其 LRU 和 Traffic 变种测试顺序改变就可能暴露问题。所以第二关一定要做拓扑内的端到端测试在 Mininet 里pingall通过后用 tcpdump 抓包核对关键行为而不是只看“通没通”。# 在 h2 上抓 eth0后台保存 pcap h2 tcpdump -i h2-eth0 -w /tmp/h2.pcap # 在 h1 上 ping h3 h1 ping -c 3 10.0.0.3然后打开 pcap把抓到的帧按时间排开确认交换机学习前后的转发行为确实从“泛洪”变成了“单播”。这一步比任何单元测试都真实因为 Mininet 里跑的是 Linux 内核协议栈加你的用户态代码抓到的包不会骗人。第三关是带参回归改一次 timeout、丢包率或路由表之后必须重跑前两关。参数改动是最容易引入回归的我吃过亏把 lab_4 的 timeout 调小之后忘了重跑路由测试交上去的版本直接把下一跳路由表项当作过期项清了导致跨网段丢包持续了半小时。从那以后我每次跑实验都把这三步当作强制流程单元测试清逻辑tcpdump 看真实包改参后重跑回归。这套习惯能帮你把“碰巧能跑”和“确实会跑”区分开也正好是实验报告里最拿得出手的过程证据。希望帮到你。本文还有配套的精品资源点击获取
返回列表