Network Interface在通过TCP协议将数据包进行封装准备好以后,就需要“快递公司”来对这些数据包进行分发了。这个过程可以划分为两个部分,一个是数据包在中转转发的过程中需要经过的“中转”设备有哪些,其次就是如何选择“中转”的线路。
在网络接口的部分,主要实现的逻辑是作为发送的某一个节点,在知道了下一个中转站的IP地址以后,如何将数据包进行交付。
需要实现的逻辑首先对目前的知识进行一个梳理。首先在前面四个Lab里面,主要完成的是TCP数据包从一串简单的字符串,到最后封装成一个完整的,可以用于建立连接沟通的TCP数据包。TCP数据包本身并不关心数据包是如何从源IP到目标IP的,这一部分的主要实现是由网络层的IP路由和数据链路层进行沟通。
在数据链路层中,我们假设已经通过网络层的路由知道了下一条的IP地址,但是要知道一个网口今天可以是192.168.1.1
,明天就可以是10.0.0.1
,因此我们只知道IP地址是不足以让我们从硬件层面将数据包进行中转发送的,我们还需要针对每一个特定网口本身的硬件地址,也就是MAC地址,来硬件和硬件之间可以正确的发送数据。
由于硬件地址和IP地址的映射关系有可能是动态的,而每次发送数据都向所有设备广播询问一次MAC和IP的映射关系的话在交流频繁的网络情况下资源利用率十分低下,因此我们也需要在中转设备中动态维护一个缓存用的映射表,同时为这个映射表中每一个条目设定对应的TTL
来保证数据的实时性,在超过一定时间以后就删除该缓存。
1 2 3 4 5 6 7 8 9 10 11 12 struct ArpEntry { uint32_t raw_ip_addr; EthernetAddress eth_addr; bool operator <(const ArpEntry &rhs) const { return raw_ip_addr < rhs.raw_ip_addr; } };const size_t arp_max_ttl = 30000 ; std::map<ArpEntry, size_t > _arp_table{};
而获取IP和MAC地址对应关系的这个步骤则是由ARP协议实现,在硬件自己不知道要发送的下一个网口的MAC地址的时候,他就会给所有的网口广播ARP,正确的设备识别到了这个ARP是发送给自己的以后就返回自己的MAC地址,如果不是发送给自己的则丢弃不处理。同时和TCP中的超时重传一样,ARP探针自己也有可能会因为硬件链路的问题而导致对方没有收到自己的报文,所以也需要有一个超时重传的逻辑,来让自己尽可能的收到对方的回复。
1 2 3 4 5 6 7 8 9 10 11 12 struct ArpEntry { uint32_t raw_ip_addr; EthernetAddress eth_addr; bool operator <(const ArpEntry &rhs) const { return raw_ip_addr < rhs.raw_ip_addr; } };const size_t arp_probe_ttl = 5000 ; std::map<ArpProbe, size_t > _probe_table{};
在有了以上两个大体部分以后,我们就只需要实现
发送(IPV4/ARP)报文 接受(IPV4/ARP)报文 超时重传探针,以及管理ARP映射的TTL 两个部分即可。
实现细节 发送报文在发送报文之前,我们首先需要将IP地址转换为uint_32,以用于报文的封装,然后检查这个IP地址我们是否已经缓存了它对应的MAC地址
1 2 3 4 5 6 7 8 9 10 11 const uint32_t next_hop_ip = next_hop.ipv4_numeric (); optional<EthernetAddress> next_eth;for (const auto &entry : _arp_table) { if (entry.first.raw_ip_addr == next_hop_ip) { next_eth = entry.first.eth_addr; break ; } }
如果这个IP地址对应的MAC地址我们已经缓存了,那么就只需要将这个IP报文封装成网络帧进行发送
1 2 3 4 5 6 7 8 if (next_eth.has_value ()) { EthernetFrame eth_frame; eth_frame.header () = {next_eth.value (), _ethernet_address, EthernetHeader::TYPE_IPv4}; eth_frame.payload () = dgram.serialize (); _frames_out.push (eth_frame); return ; }
如果这个IP地址在我们维护的ARP映射表中并不存在对应的映射关系,那么我们首先要判断我们是否就这个IP发送过ARP探针,如果发送过探针了那么我们也没必要再发送一次,只要等待之前的探针让对方返回正确的MAC地址给我们即可。
1 2 3 4 5 6 for (auto &probe : _probe_table) { if (probe.first.raw_ip_addr == next_hop_ip) { return ; } }
如果没有发送的话,那么我们就需要封装一个ARP探针,用于检测目标IP对应的MAC地址,探针目标的IP地址就是IP数据包下一跳的IP,MAC地址则是广播地址(在该实验中直接将目标MAC设置为空即可)。
1 2 3 4 5 6 7 8 9 10 11 ARPMessage arp_probe; arp_probe.opcode = ARPMessage::OPCODE_REQUEST; arp_probe.sender_ethernet_address = _ethernet_address; arp_probe.sender_ip_address = _ip_address.ipv4_numeric (); arp_probe.target_ethernet_address = {}; arp_probe.target_ip_address = next_hop_ip; EthernetFrame probe_frame; probe_frame.header () = {ETHERNET_BROADCAST, _ethernet_address, EthernetHeader::TYPE_ARP}; probe_frame.payload () = arp_probe.serialize (); _frames_out.push (probe_frame);
同时由于探针有超时重传的机制,因此对于这个新发送的报文,我们也需要将其加入缓存表中并设定TTL
1 2 3 ArpProbe _arp = {next_hop_ip, dgram}; _probe_table[_arp] = arp_probe_ttl;
所有的代码
接受报文在接受报文的部分,我们无非会收到两种报文,一种是包含IP数据的报文,一种是对方给我们发过来的ARP报文
对于这两种报文,首先我们判断它是不是要发送给我们的或是否是一个广播的网络帧
1 2 3 4 if (frame.header ().dst != _ethernet_address && frame.header ().dst != ETHERNET_BROADCAST) { return {}; }
如果这是一个确定源MAC和目标MAC的IP数据包,那么我们只需要接受这个数据包然后返回对应的数据即可
1 2 3 4 5 6 if (frame.header ().type == EthernetHeader::TYPE_IPv4) { InternetDatagram datagram; datagram.parse (frame.payload ()); return datagram; }
但是如果这是一个ARP探针,我们首先对其进行分析
1 2 3 4 5 6 7 ARPMessage arp_msg; arp_msg.parse (frame.payload ()); ArpEntry src = {arp_msg.sender_ip_address, arp_msg.sender_ethernet_address}, dst = {arp_msg.target_ip_address, arp_msg.target_ethernet_address}; _update_arp_table({src, dst});
其中_update_arp_table用于更新arp表,具体代码如下
1 2 3 4 5 6 void NetworkInterface::_update_arp_table(initializer_list<ArpEntry> arp_entry) { for (auto &entry : arp_entry) { _arp_table[entry] = arp_max_ttl; } }
在解析了网络帧之后,我们大致可以得到以下三种分类
广播报文,请求某个IP对应的MAC地址,但这个IP不是我们的丢弃过滤
1 2 3 4 if (dst.raw_ip_addr != _ip_address.ipv4_numeric ()) { return {}; }
广播报文,请求某个IP对应的MAC地址,但这个IP是我们的返回我们的MAC地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (arp_msg.opcode == ARPMessage::OPCODE_REQUEST) { ARPMessage reply; reply.opcode = ARPMessage::OPCODE_REPLY; reply.sender_ip_address = _ip_address.ipv4_numeric (); reply.sender_ethernet_address = _ethernet_address; reply.target_ip_address = src.raw_ip_addr; reply.target_ethernet_address = src.eth_addr; EthernetFrame reply_frame; reply_frame.header () = {src.eth_addr, _ethernet_address, EthernetHeader::TYPE_ARP}; reply_frame.payload () = reply.serialize (); _frames_out.push (reply_frame); return {}; }
我们发出的ARP探针得到了别人的回复,知道了别人的MAC地址更新自己的ARP映射表,同时检查是否有对应目标MAC地址的报文等待发送
1 2 3 4 5 6 7 8 9 for (auto entry = _probe_table.begin (); entry != _probe_table.end ();) { if (entry->first.raw_ip_addr == src.raw_ip_addr) { send_datagram (entry->first.datagram, Address::from_ipv4_numeric (entry->first.raw_ip_addr)); entry = _probe_table.erase (entry); } else { entry++; } }
超时处理在这里我们只需要做两件事
对于正常的条目和探针,我们只需要让其TTL减少即可,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void NetworkInterface::tick (const size_t ms_since_last_tick) { for (auto entry = _arp_table.begin (); entry != _arp_table.end ();) { if (entry->second < ms_since_last_tick) { entry = _arp_table.erase (entry); } else { entry->second -= ms_since_last_tick; entry++; } } for (auto entry = _probe_table.begin (); entry != _probe_table.end (); entry++) { if (entry->second < ms_since_last_tick) { ARPMessage re_probe_arp; re_probe_arp.opcode = ARPMessage::OPCODE_REQUEST; re_probe_arp.sender_ip_address = _ip_address.ipv4_numeric (); re_probe_arp.sender_ethernet_address = _ethernet_address; re_probe_arp.target_ip_address = entry->first.raw_ip_addr; re_probe_arp.target_ethernet_address = {}; EthernetFrame re_probe_frame; re_probe_frame.header () = {ETHERNET_BROADCAST, _ethernet_address, EthernetHeader::TYPE_ARP}; re_probe_frame.payload () = re_probe_arp.serialize (); _frames_out.push (re_probe_frame); entry->second = arp_probe_ttl; } else { entry->second -= ms_since_last_tick; } } }
总结这个实验主要实现的逻辑都是数据链路层的,和之前几个Lab没有直接的关系。不过值得一提的就是在Lab4测试的时候运行的TUN
和TAP
很有意思。这两个词之前好奇还是在使用Clash
的时候既可以是TUN
也可以是TAP
模式,而且通常来说TUN
模式的性能要比TAP
要好。当时还不知道为什么,在写完这个实验以后搜了一些资料,目前浅显的理解大致认为是TAP
的网络代理模拟是有处理到数据链路层的,也就是MAC地址也进行了模拟,而TUN
则只是模拟到了IP层,并没有自己的MAC地址,因此损耗也要少一些。