Skip to content

南京大学本科生实验报告

课程名称: 计算机网络 任课教师:田臣

学院 工程管理学院 专业(方向) 计算机金融
学号 211275022 姓名 田昊东
Email 1040880153@qq.com 开始/完成日期 2023/5/13~2023/5/13

实验名称

Lab 5: Respond to ICMP

实验目的

  • 继实验三、实验四基础上继续完善设计的路由器,补充icmp功能,实现一个较为完善的路由器

实验内容

Task 2: Responding to ICMP echo requests

设计思路

  • 将所有的ICMP回复都封装在了一个函数中,即将task2和task3的icmp回复都写在同一函数中,使用type来控制发送ICMP包的类型

  • 发送判断:

  • python if ipv4.dst in self.interfaces: if packet.get_header(ICMP) is not None and packet.get_header(ICMP).icmptype == ICMPType.EchoRequest: # icmp答复 packet = icmp_reply(packet, 0)

  • 如果一个包的目的ip是路由器的一个端口,并且具有icmp包头且为EchoRequest类型,则路由器需要构造一个ICMP答复包,并进入发送程序

  • 具体的构造方式及发送逻辑将在task3中介绍

Task 3: Generating ICMP error messages

设计思路

  • python def icmp_reply(packet, type):#icmp回复包的构建 # 判断是否为表示错误的icmp if packet.has_header(ICMP): icmp = packet.get_header(ICMP) if icmp.icmptype == ICMPType.DestinationUnreachable or icmp.icmptype == ICMPType.TimeExceeded or icmp.icmptype == ICMPType.Redirect: return None # 初始化返回包 reply = Packet() reply_eth = Ethernet() reply_ipv4 = IPv4() reply_icmp = ICMP() reply_ipv4.src = packet[IPv4].src reply_ipv4.dst = packet[IPv4].src reply_ipv4.ttl = 64 reply_ipv4.protocol = IPProtocol.ICMP #去除原始包的以太网包头 del packet[Ethernet] # ping if type == 0: icmp = packet.get_header(ICMP) reply_icmp.icmptype = ICMPType.EchoReply reply_icmp.icmpcode = ICMPCodeEchoReply.EchoReply reply_icmp.icmpdata.identifier = icmp.icmpdata.identifier reply_icmp.icmpdata.sequence = icmp.icmpdata.sequence reply_icmp.icmpdata.data = icmp.icmpdata.data reply_ipv4.src = packet[IPv4].dst # 目的不可达 elif type == 1: reply_icmp.icmptype = ICMPType.DestinationUnreachable reply_icmp.icmpcode = ICMPCodeDestinationUnreachable.NetworkUnreachable reply_icmp.icmpdata.data = packet.to_bytes()[:28] # 超时 elif type == 2: reply_icmp.icmptype = ICMPType.TimeExceeded reply_icmp.icmpcode = ICMPCodeTimeExceeded.TTLExpired reply_icmp.icmpdata.data = packet.to_bytes()[:28] # arp失败 elif type == 3: reply_icmp.icmptype = ICMPType.DestinationUnreachable reply_icmp.icmpcode = ICMPCodeDestinationUnreachable.HostUnreachable reply_icmp.icmpdata.data = packet.to_bytes()[:28] # 路由器无法处理的包 else: reply_icmp.icmptype = ICMPType.DestinationUnreachable reply_icmp.icmpcode = ICMPCodeDestinationUnreachable.PortUnreachable reply_icmp.icmpdata.data = packet.to_bytes()[:28] # 装载 reply.add_header(reply_eth) reply.add_header(reply_ipv4) reply.add_header(reply_icmp) print("发送icmp包", reply, "\n") return reply

  • 首先判断传入的需要回复的包是不是一个ICMP错误消息,如果是则直接返回None,告知调用程序不需要进行回复

  • 然后进行一些通用初始化,设置ipv4包头的源和目的地址(一致是为了让转发时可以发现这一点,将ipv4发送地址修改为实际发出端口的ip)以及类型(icmp)
  • 之后根据type分别初始化,分别表示ping回复以及四种错误类型
    • 对于ping回复:首先设置返回包的icmp类型属性,然后将数据从源包的icmp包头复制过来
    • 对于错误类型:直接设置icmptypeicmpcode并从源包获取去除以太网包头后的前28字节内容
  • 将包头装载到回复包并返回

  • 为了方便使用,对原包发送流程进行了打包

  • python def send(self,packet): ipv4 = packet.get_header(IPv4) # 判断目的ip是否在转发表中 target = self.forwarding_table.lookup(ipv4.dst) if (target[0] == None): packet = icmp_reply(packet, 1) if packet is None: return self.send(packet) return # ttl判断 if ipv4.ttl <= 1: packet = icmp_reply(packet, 2) if packet is None: return self.send(packet) return # 计数减一 ipv4.ttl -= 1 # 添加到转发队列 print("等待转发: next",target[0], target[1], "目标 ", ipv4.dst, " ttl: ", ipv4.ttl, "\n") with self.lock: if target[0] not in self.arp_table.table: if target[0] not in self.wait_queue: self.wait_queue[target[0]] = Queue() # 发送arp请求包 self.send_queue.put( myPacket(None, target[0], target[1], time.time())) self.wait_queue[target[0]].put( myPacket(packet, target[0], target[1], time.time())) else: self.send_queue.put(myPacket( packet, target[0], target[1], time.time(), self.arp_table.table[target[0]][0]))

  • 首先回答这个问题路由器不应回复任何ICMP错误消息

    • 如果路由器回复ICMP错误信息,那考虑这个场景:首先路由器发现一个包TTL为0了,要发送一个ICMP错误信息包,路由器构造完成后开始准被发送包;而这时又发现包的目的ip在转发表中找不到,路由器就又需要发送一个ICMP包来告知这件事;这也就产生了一个发送给自己的非法包,又会继续产生ICMP错误包,陷入死循环。

    • 如果路由器回复ICMP错误信息,那么路由器很可能会陷入为ICMP错误信息发送ICMP错误信息的死循环,因为这很可能意味着一个ICMP错误包根本发不出去。

    • 因此路由器应该正常转发ICMP错误信息,而对于出现问题的ICMP错误包应该直接丢弃,不进行回复

    • 程序中实现了这一点

    • python if packet is None: return

  • send函数包含了两种可能需要回复的情况,一种是转发表中找不到目标ip;另一种是ttl=0

    • 打包这个函数是因为发送ICMP包也需要经过转发表映射、arp查询等步骤,使用send函数可以很方便的实现
  • if ipv4.dst in self.interfaces: if packet.get_header(ICMP) is not None and packet.get_header(ICMP).icmptype == ICMPType.EchoRequest: # icmp答复 packet = icmp_reply(packet, 0) else: #错误4 packet = icmp_reply(packet, 4) if packet is None: return self.send(packet)

  • 针对目的ip是路由器一个端口的情况,有两种可能,一种是需要回复ping包;另一种是出现了路由器无法处理的包

  • 直接调用icmp_reply生成icmp包进行处理

  • 最后一种可能的错误情况是arp请求失败

  • python while self.router.wait_queue[p.next].qsize(): q = self.router.wait_queue[p.next].get() packet = q.packet packet = icmp_reply(packet, 3) if packet is None: continue self.router.send(packet)

  • 当arp请求发送失败5次后,要取出所有对应的待发送的包,逐个移出队列,并针对每个丢弃的包发送一个ICMP包进行反馈

测试结果

image-20230514001439024

抓包分析

  • image-20230514005433156
  • 使用server1 ping 一个前缀在转发表中但是实际不存在的IP192.168.200.3
    • 首先server1像路由器发送ICMP请求,之后的5s路由器发送了5次arp但都会失败,因此路由器需要把错误信息发还给server1
    • 路由器首先发送arp请求,找到server1对应的mac之后将ICMP错误信息发送给server1
  • image-20230514004930257
  • 使用clent ping一个不存在于转发表中的ip,路由器会发现无法转发后将ICMP错误信息返还给client
  • image-20230514004904749
  • 首先使用server1 ping路由器一个端口,路由器正常进行回复,返回一个ping回复包
  • 然后发送一个TTL为1的包,路由器将其TTL减一后为0,因此路由器会返回一个ICMP错误包
  • tracerouter分析
  • 从server1对client发动tracerouter
  • image-20230518192333929
  • image-20230518192405172
  • image-20230518192434287
  • 在试探过程中traceroute会逐渐提高ttl的限制(每个ttl会重复发送3个包)
  • 在这个过程中路由器先是会返回3次超时包(传输到端口192.168.100.2就终止了)
  • 之后又返回了3次端口不可达包,这是由于UDP包设置错误造成的
  • 由此说明路由器可以正确返回ICMP错误包

实验总结

  • 本次实验的难度相对不是很大,不过想要通过测试案例仍然需要十分细心,处理好协议的每一个细节。

  • 实验3~5逐步完成了一个具有基础功能的路由器,在这个过程中尽管遇到了很多挑战,但对层路由器协议有了更为深刻的理解,实际完成过程中更体会到了各种协议设计的精妙!

  • 由于实验3~5不断在原有的代码上加以完善,良好代码的结构就显得十分重要,对一些重要功能进行封装、复用可以极大的减少重复代码,并减少错误的可能。