在这个 lab 里,我们要把第二层链路层和第三层网络层连接起来。在出站方向,我们的代码将 IP 包封装为 ETH 帧,并视情况发起 ARP 请求以维护 IP 到 MAC 的映射表;在入站方向,我们把 ETH 帧解析为 IP 包并向上传递,或解析为 ARP 协议包并更新 ARP 表。
我们要维护 ARP 表和“想要发送但尚不知道目标 MAC 地址的包”列表,并在一定时间后让它们过期。所以我们在头文件里新增以下字段:
class NetworkInterface
{
public:
// ...
private:
// ...
static constexpr size_t ARP_TTL = 30000;
static constexpr size_t ARP_SAME_REQUEST_CD = 5000;
// Datagrams to send. We store datagrams whose next hop's MAC is still unknown here.
struct PendingArpRequest
{
std::vector<InternetDatagram> buffered_datagrams {};
size_t ms_since_last_request {
SIZE_MAX }; // Init to max so the first request is not limited by ARP_SAME_REQUEST_CD.
};
// Mapping ip to pending arp request
std::unordered_map<uint32_t, PendingArpRequest> pending_arp_requests_ {};
struct ArpEntry
{
EthernetAddress eth_addr {};
size_t living_ms { 0 };
};
// Table mapping IP to MAC and living time
std::unordered_map<uint32_t, ArpEntry> arp_table_ {};
};
在出站方向,如果已经知道下一跳对应的 MAC 地址,直接发送就行;如果不知道就要把想要出站的数据存储起来并广播 ARP 请求。注意对同一个 IP 的 ARP 请求不应太频繁,如实验文档所说我们可以设置一个五秒的 CD.
这里的 serialize 函数是一个辅助函数,用于把数据转换成二进制序列。在 InternetDatagram 这些类内部内存不一定连续(比如 std::vector<Refstd::string> payload {}),也可能有大小端问题,serialize 则处理这些问题把它们转化成连续的、网络序的字节流。
void NetworkInterface::send_datagram( InternetDatagram dgram, const Address& next_hop )
{
EthernetFrame eth_frame {};
eth_frame.header.src = ethernet_address_;
if ( arp_table_.contains( next_hop.ipv4_numeric() ) && arp_table_[next_hop.ipv4_numeric()].living_ms < ARP_TTL ) {
// Send datagram right away if the dst eth address is already known
eth_frame.header.dst = arp_table_[next_hop.ipv4_numeric()].eth_addr;
eth_frame.header.type = EthernetHeader::TYPE_IPv4;
eth_frame.payload = serialize( dgram );
} else {
pending_arp_requests_[next_hop.ipv4_numeric()].buffered_datagrams.push_back( std::move( dgram ) );
// Send ARP request if last ARP request with same IP was more than ARP_SAME_REQUEST_CD ago
if ( pending_arp_requests_[next_hop.ipv4_numeric()].ms_since_last_request < ARP_SAME_REQUEST_CD ) {
return;
}
eth_frame.header.dst = ETHERNET_BROADCAST;
eth_frame.header.type = EthernetHeader::TYPE_ARP;
ARPMessage arpmsg;
arpmsg.opcode = ARPMessage::OPCODE_REQUEST;
arpmsg.sender_ethernet_address = ethernet_address_;
arpmsg.sender_ip_address = ip_address_.ipv4_numeric();
arpmsg.target_ethernet_address = ETHERNET_ZERO;
arpmsg.target_ip_address = next_hop.ipv4_numeric();
eth_frame.payload = serialize( arpmsg );
pending_arp_requests_[next_hop.ipv4_numeric()].ms_since_last_request = 0;
}
transmit( eth_frame );
}
在入站方向,接收到 ETH 帧时做个 Multiplexing 分成 IPV4 和 ARP 两个分支,遇到 IPV4 就放进 datagramsreceived 给上层应用;遇到 ARP 先结合其 sender 的 IP 和 MAC 信息更新 ARP 表、发缓存的包(如果有),再根据类型判断是否回复。
void NetworkInterface::recv_frame( const EthernetFrame& frame )
{
if ( frame.header.dst != ethernet_address_ && frame.header.dst != ETHERNET_BROADCAST ) {
return;
}
if ( frame.header.type == EthernetHeader::TYPE_IPv4 ) {
InternetDatagram dgram;
if ( parse( dgram, frame.payload ) ) {
datagrams_received_.push( dgram );
} else {
debug( "recv_frame: Parse failed!" );
}
} else if ( frame.header.type == EthernetHeader::TYPE_ARP ) {
ARPMessage arpmsg;
if ( parse( arpmsg, frame.payload ) ) {
arp_table_[arpmsg.sender_ip_address] = { arpmsg.sender_ethernet_address, 0 };
// Send datagrams whose next hop's MAC was unknown but is known now.
if ( pending_arp_requests_.contains( arpmsg.sender_ip_address ) ) {
for ( const auto& dgram : pending_arp_requests_[arpmsg.sender_ip_address].buffered_datagrams ) {
EthernetFrame eth_frame;
eth_frame.header.src = ethernet_address_;
eth_frame.header.dst = arp_table_[arpmsg.sender_ip_address].eth_addr;
eth_frame.header.type = EthernetHeader::TYPE_IPv4;
eth_frame.payload = serialize( dgram );
transmit( eth_frame );
}
}
pending_arp_requests_.erase( arpmsg.sender_ip_address );
// Send ARP reply if it is an ARP request asking for our IP address
if ( arpmsg.opcode == ARPMessage::OPCODE_REQUEST && arpmsg.target_ip_address == ip_address_.ipv4_numeric() ) {
EthernetFrame eth_frame {};
eth_frame.header.src = ethernet_address_;
eth_frame.header.dst = arpmsg.sender_ethernet_address;
eth_frame.header.type = EthernetHeader::TYPE_ARP;
ARPMessage arpreply;
arpreply.opcode = ARPMessage::OPCODE_REPLY;
arpreply.sender_ethernet_address = ethernet_address_;
arpreply.sender_ip_address = ip_address_.ipv4_numeric();
arpreply.target_ethernet_address = arpmsg.sender_ethernet_address;
arpreply.target_ip_address = arpmsg.sender_ip_address;
eth_frame.payload = serialize( arpreply );
transmit( eth_frame );
}
} else {
debug( "recv_frame: Parse failed!" );
}
}
}
最后是 tick 函数,它更新大家的计时器然后在到期时 erase 就行。注意别把边遍历边 erase 的逻辑写错了。
void NetworkInterface::tick( const size_t ms_since_last_tick )
{
for ( auto it = pending_arp_requests_.begin(); it != pending_arp_requests_.end(); ) {
auto& [ip, pending_arp_request] = *it;
pending_arp_request.ms_since_last_request += ms_since_last_tick;
if ( pending_arp_request.ms_since_last_request > ARP_SAME_REQUEST_CD ) {
it = pending_arp_requests_.erase( it );
} else {
++it;
}
}
for ( auto it = arp_table_.begin(); it != arp_table_.end(); ) {
auto& [ip, arp_entry] = *it;
arp_entry.living_ms += ms_since_last_tick;
if ( arp_entry.living_ms > ARP_TTL ) {
it = arp_table_.erase( it );
} else {
++it;
}
}
}