CS144 Lab Checkpoint 5: down the stack (the network interface)
至于为什么没有Lab 4,因为Lab 4没有要写的代码,分析RTT等的实验也就懒得写了。
Overview
TCP报文的发送格式有以下几种:
- TCP-in-UDP-in-IP:用户只需要提供TCP报文作为数据载荷,Linux内核提供一个接口(UDPSocket),内核提供UDP,IP,以太网的报头,由于这一工作是内核完成的,所以可以做到应用间隔离;
- TCP-in-IP:通常情况下,TCP直接放在IP报文中,而不需要UDP。内核提供TUN接口,允许应用程序提供IP报文,内核负责其他部分,如以太网报头,但是应用程序必须自己构建IP报头;
- TCP-in-IP-in-Ethernet:上述的两种方式都有赖于内核构建适当的以太网数据帧,这意味着内核必须根据下一跳的IP地址知道下一跳所对应的以太网地址,如果内核不知道,它就会进行一个广播操作,在广播中Linux会询问“谁声明了以下IP地址,您的以太网地址是什么?”
第三种方式由网络接口执行:网络接口是一个将出站的数据报文转换成链路层数据帧的组件,在本实验中要实现一个网络接口,放在TCP/IP的最底层,生成的数据帧将通过TAP设备的接口发送给Linux。
其实这个实验就是要实现ARP协议
Implementation
void NetworkInterface::send datagram(const InternetDatagram &dgram, const Address &next hop);
函数功能:当调用者(例如,您的 TCPConnection 或路由器)想要将出站 Internet (IP) 数据报发送到下一跳时,将调用此方法。
您的接口的工作是:将此数据报转换为以太网帧并(最终)发送它。
- 如果目标以太网地址已知,则立即发送。创建以太网帧(类型 = EthernetHeader::TYPE IPv4),将有效负载设置为序列化数据报,并设置源地址和目标地址。
- 如果目标以太网地址未知,则广播下一跳以太网地址的 ARP 请求,并将 IP 数据报排队,以便在收到ARP 回复后发送。
void NetworkInterface::recv frame(const EthernetFrame &frame);
函数功能: 当以太网帧从网络到达时,将调用此方法。代码应忽略任何未发往网络接口的帧(这意味着以太网目的地是广播地址或存储在以太网地址成员变量中的接口自己的以太网地址)。
- 如果入站帧是 IPv4,则将有效负载解析为 InternetDatagram,如果成功(即 parse() 方法返回 ParseResult::NoError),则将生成的数据报推送到数据报接收队列。
- 如果入站帧是 ARP,则将有效负载解析为 ARPMessage,如果成功,则记住发送方的 IP 地址和以太网地址之间的映射30 秒。(从请求和回复中学习映射。)此外,如果是ARP 请求询问我们的 IP 地址,则发送适当的 ARP 回复。
void NetworkInterface::tick(const size t ms since last tick);
这称为随着时间流逝。使所有已过期的 IP 到以太网映射失效。
具体实现如下:
首先,先增加类的成员变量和函数,以便主要函数的实现:
class NetworkInterface
{
public:
// An abstraction for the physical output port where the NetworkInterface sends Ethernet frames
class OutputPort
{
public:
virtual void transmit( const NetworkInterface& sender, const EthernetFrame& frame ) = 0;
virtual ~OutputPort() = default;
};
// Construct a network interface with given Ethernet (network-access-layer) and IP (internet-layer)
// addresses
NetworkInterface( std::string_view name,
std::shared_ptr<OutputPort> port,
const EthernetAddress& ethernet_address,
const Address& ip_address );
// Sends an Internet datagram, encapsulated in an Ethernet frame (if it knows the Ethernet destination
// address). Will need to use [ARP](\ref rfc::rfc826) to look up the Ethernet destination address for the next
// hop. Sending is accomplished by calling `transmit()` (a member variable) on the frame.
void send_datagram( const InternetDatagram& dgram, const Address& next_hop );
// Receives an Ethernet frame and responds appropriately.
// If type is IPv4, pushes the datagram to the datagrams_in queue.
// If type is ARP request, learn a mapping from the "sender" fields, and send an ARP reply.
// If type is ARP reply, learn a mapping from the "sender" fields.
void recv_frame( const EthernetFrame& frame );
// Called periodically when time elapses
void tick( size_t ms_since_last_tick );
// Accessors
const std::string& name() const { return name_; }
const OutputPort& output() const { return *port_; }
OutputPort& output() { return *port_; }
std::queue<InternetDatagram>& datagrams_received() { return datagrams_received_; }
// 下一个实验要用到
const Address& ip_address() const { return ip_address_; }
private:
struct ARPEntry {
EthernetAddress eth_addr;
size_t ttl;
};
// Human-readable name of the interface
std::string name_;
// The physical output port (+ a helper function `transmit` that uses it to send an Ethernet frame)
std::shared_ptr<OutputPort> port_;
void transmit( const EthernetFrame& frame ) const { port_->transmit( *this, frame ); }
// Ethernet (known as hardware, network-access-layer, or link-layer) address of the interface
EthernetAddress ethernet_address_;
// IP (known as internet-layer or network-layer) address of the interface
Address ip_address_;
// Datagrams that have been received
std::queue<InternetDatagram> datagrams_received_ {};
// ARP 表项的超时时间
static constexpr size_t arp_entry_ttl = 30 * 1000;
// ARP 回复的超时时间
static constexpr size_t arp_response_ttl = 5 * 1000;
// 将 ip 地址映射到 以太网帧的地址
unordered_map<uint32_t, ARPEntry> arp_table {};
// 记录 ip 地址与其相应时间,过期ip地址即丢弃
unordered_map<uint32_t, size_t> response_time {};
// 记录ip地址与要发送给这个ip地址的报文
unordered_map<uint32_t, list<InternetDatagram>> waiting_datagram {};
// 发送链路层数据帧,简化主要函数的代码
template <typename DatagramType>
void send(const EthernetAddress &dst, const EthernetAddress &src, const uint16_t &type, const DatagramType& dgram) const;
void send_ip(const EthernetAddress &dst, const EthernetAddress &src, const IPv4Datagram &dgram) const;
void send_arp(const EthernetAddress &dst, const EthernetAddress &src, const ARPMessage &arp) const;
};
上面用汉语注释的就是新加的成员变量和函数,主要新增了有
- ip地址映射MAC地址
arp_table
; - ip地址与响应时间的映射
response_time
; - ip地址与待发送的IP报文列表的映射
waitting_datagram
;
新增了一个模板函数send()
它是为了后文方便发送以太网数据帧,而不用重复创建数据帧;
然后有两个成员函数send_ip
以及 send_arp
它们是为了解决send IPv4和 ARP不能复用的问题,都调用模板函数send
。
成员函数的实现部分:
#include <iostream>
#include "arp_message.hh"
#include "exception.hh"
#include "network_interface.hh"
using namespace std;
//! \param[in] ethernet_address Ethernet (what ARP calls "hardware") address of the interface
//! \param[in] ip_address IP (what ARP calls "protocol") address of the interface
NetworkInterface::NetworkInterface( string_view name,
shared_ptr<OutputPort> port,
const EthernetAddress& ethernet_address,
const Address& ip_address )
: name_( name )
, port_( notnull( "OutputPort", move( port ) ) )
, ethernet_address_( ethernet_address )
, ip_address_( ip_address )
{
cerr << "DEBUG: Network interface has Ethernet address " << to_string( ethernet_address ) << " and IP address "
<< ip_address.ip() << "\n";
}
// 自己实现的send函数用来简化后面的代码
template<typename DatagramType>
void NetworkInterface::send( const EthernetAddress& dst,
const EthernetAddress& src,
const uint16_t& type,
const DatagramType& dgram ) const
{
EthernetFrame eth;
// 补充以太网帧的头部
eth.header.dst = dst;
eth.header.src = src;
eth.header.type = type;
// 补充以太网帧的数据载荷
Serializer serializer;
dgram.serialize( serializer );
eth.payload = serializer.output();
transmit( eth );
}
void NetworkInterface::send_ip( const EthernetAddress& dst,
const EthernetAddress& src,
const IPv4Datagram& dgram ) const
{
send( dst, src, EthernetHeader::TYPE_IPv4, dgram );
}
void NetworkInterface::send_arp( const EthernetAddress& dst,
const EthernetAddress& src,
const ARPMessage& arp ) const
{
send( dst, src, EthernetHeader::TYPE_ARP, arp );
}
//! \param[in] dgram the IPv4 datagram to be sent
//! \param[in] next_hop the IP address of the interface to send it to (typically a router or default gateway, but
//! may also be another host if directly connected to the same network as the destination) Note: the Address type
//! can be converted to a uint32_t (raw 32-bit IP address) by using the Address::ipv4_numeric() method.
void NetworkInterface::send_datagram( const InternetDatagram& dgram, const Address& next_hop )
{
// Your code here.
(void)dgram;
(void)next_hop;
// 超时丢弃
if ( dgram.header.ttl < 1 ) {
return;
}
// 如果arp_table有目的eth 地址
if ( arp_table.find( next_hop.ipv4_numeric() ) != arp_table.end() ) {
// EthernetFrame eth;
auto entry = arp_table[next_hop.ipv4_numeric()];
send_ip( entry.eth_addr, ethernet_address_, dgram );
}
// 否则发送ARP报文查询地址
else {
// 如果在等待队列中,没有出现查询该IP对应的地址,那么就发送ARP报文
if ( waiting_datagram.find( next_hop.ipv4_numeric() ) == waiting_datagram.end() ) {
waiting_datagram[next_hop.ipv4_numeric()].emplace_back( dgram );
// 首先创建一个ARP报文
ARPMessage arp_msg;
arp_msg.opcode = arp_msg.OPCODE_REQUEST;
arp_msg.sender_ethernet_address = ethernet_address_;
arp_msg.target_ethernet_address = {};
arp_msg.sender_ip_address = ip_address_.ipv4_numeric();
arp_msg.target_ip_address = next_hop.ipv4_numeric();
// 然后创建一个 eth报文
send_arp( ETHERNET_BROADCAST, ethernet_address_, arp_msg );
response_time[next_hop.ipv4_numeric()] = arp_response_ttl;
}
}
}
//! \param[in] frame the incoming Ethernet frame
void NetworkInterface::recv_frame( const EthernetFrame& frame )
{
// Your code here.
// 如果不是广播帧,或者不是目的地址是本机,忽略
if ( frame.header.dst != ETHERNET_BROADCAST && frame.header.dst != ethernet_address_ ) {
return;
}
// IP报文直接加入队列中
else if ( frame.header.type == frame.header.TYPE_IPv4 ) {
Parser parser( frame.payload );
InternetDatagram ip;
ip.parse( parser );
datagrams_received_.push( ip );
}
// 如果是ARP协议,要更新ARP表
else if ( frame.header.type == frame.header.TYPE_ARP ) {
ARPMessage arp_msg;
Parser parser( frame.payload );
arp_msg.parse( parser );
uint32_t ip = ip_address_.ipv4_numeric();
uint32_t src_ip = arp_msg.sender_ip_address;
// 如果是发送给本机的ARP请求报文,要应答这个请求
if ( arp_msg.opcode == arp_msg.OPCODE_REQUEST && arp_msg.target_ip_address == ip ) {
ARPMessage arp_reply;
arp_reply.opcode = arp_reply.OPCODE_REPLY;
arp_reply.sender_ip_address = ip;
arp_reply.sender_ethernet_address = ethernet_address_;
arp_reply.target_ip_address = src_ip;
arp_reply.target_ethernet_address = arp_msg.sender_ethernet_address;
send_arp( arp_msg.sender_ethernet_address, ethernet_address_, arp_reply );
}
// 其余就更新IP和MAC地址对应关系
arp_table[src_ip] = { arp_msg.sender_ethernet_address, arp_entry_ttl };
// 如果这个ip地址有没有发送完的数据报文
auto it = waiting_datagram.find( src_ip );
if ( it != waiting_datagram.end() ) {
for ( const auto& dgram : it->second ) {
send_ip( arp_msg.sender_ethernet_address, ethernet_address_, dgram );
}
waiting_datagram.erase( it );
}
}
}
//! \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 )
{
// Your code here.
// 遍历arp_table,清除超时项,不超时的给ttl减累计的ticks
for ( auto it = arp_table.begin(); it != arp_table.end(); ) {
if ( it->second.ttl <= ms_since_last_tick ) {
it = arp_table.erase( it );
} else {
it->second.ttl -= ms_since_last_tick;
it = next( it );
}
}
// 如果响应超时也去除,如果这个ip响应超时,同时去掉等待队列中的元素
for ( auto it = response_time.begin(); it != response_time.end(); ) {
if ( it->second <= ms_since_last_tick ) {
auto it2 = waiting_datagram.find( it->first );
if ( it2 != waiting_datagram.end() ) {
waiting_datagram.erase( it2 );
}
it = response_time.erase( it );
} else {
it->second -= ms_since_last_tick;
it = next( it );
}
}
}
整体来说就是实现了一个ARP协议,需要注意的就是当ARP REQUEST报文的目的地址不是本机的时候,要更新本机的ARP表的映射关系,剩余的发送报文和超时机制和前面的实验类似。