CS144-Lab5 计算机网络:Network Interface的功能

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
//! 构造Arp条目
struct ArpEntry {
uint32_t raw_ip_addr;
EthernetAddress eth_addr;
bool operator<(const ArpEntry &rhs) const { return raw_ip_addr < rhs.raw_ip_addr; }
};

//! 记录最大的ttl时间
const size_t arp_max_ttl = 30000;

//! 用于记录arp表
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
//! 构造Arp条目
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
// convert IP address of next hop to raw 32-bit representation (used in ARP header)
const uint32_t next_hop_ip = next_hop.ipv4_numeric();
optional<EthernetAddress> next_eth;

// 检查next_hop的IP地址是否在ARP里面有
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
// 如果在ARP里面有则直接发送并短路
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
// ARP内没有,先判断之前是否已经发送过探针,如果发送过就不发送了
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
// 丢弃目标MAC地址不是我自己的数据帧
if (frame.header().dst != _ethernet_address && frame.header().dst != ETHERNET_BROADCAST) {
return {};
}

如果这是一个确定源MAC和目标MAC的IP数据包,那么我们只需要接受这个数据包然后返回对应的数据即可

1
2
3
4
5
6
// 接受到IP数据段的时候(代表对方和自己都有了互相的ARP信息,不需要对ARP表进行操作),对这个数据段进行处理
if (frame.header().type == EthernetHeader::TYPE_IPv4) {
InternetDatagram datagram;
datagram.parse(frame.payload());
return datagram;
}

但是如果这是一个ARP探针,我们首先对其进行分析

1
2
3
4
5
6
7
// 接受到的是一个ARP包,先将这个包的内容序列化,并将其中包含的ARP信息尝试更新到自己的ARP表中
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
//! 更新ARP条目
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
// 过滤掉不是发给自己的IP地址的包
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
// 接受到ARP请求的时候,返回一个包含自己信息的ARP响应报文,同时利用这个frame更新自己的ARP表
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
// 收到别人传送回来的ARP的时候,如果缓存中有等待的对应条目,则删除,并发送对应的数据
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++;
}
}

超时处理

在这里我们只需要做两件事

  • 删除超时的ARP条目
  • 重新发送超时的探针

对于正常的条目和探针,我们只需要让其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
//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
void NetworkInterface::tick(const size_t ms_since_last_tick) {
// 将检测是否有超时的ARP条目
for (auto entry = _arp_table.begin(); entry != _arp_table.end();) {
if (entry->second < ms_since_last_tick) {
// 删除多余的ARP条目
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测试的时候运行的TUNTAP很有意思。这两个词之前好奇还是在使用Clash的时候既可以是TUN也可以是TAP模式,而且通常来说TUN模式的性能要比TAP要好。当时还不知道为什么,在写完这个实验以后搜了一些资料,目前浅显的理解大致认为是TAP的网络代理模拟是有处理到数据链路层的,也就是MAC地址也进行了模拟,而TUN则只是模拟到了IP层,并没有自己的MAC地址,因此损耗也要少一些。


CS144-Lab5 计算机网络:Network Interface的功能
https://halc.top/p/db490294
作者
HalcyonAzure
发布于
2023年4月24日
许可协议