Getting started
首先是版本控制操作,从lab4分支创建用于lab5开发的分支,我是在Clion的Git图形操作界面设定的。然后按照文档中的的介绍,从远程仓库拉取lab5的实验内容
//在新分支dev-lab5下
git fetch
git merge origin/lab5-startercode
make -j4
会弹出一个询问界面,让你提交此次合并到你本地仓库分支的说明commit,可以不管,直接ctrl+X键。
首先来看一下整个CS144实验的人物安排,如下图:
红色方框中的就是本次实验需要实现的。
TCPsegment传输方式
传输TCPsegment到远端,有以下几种方式:
- TCP-in-UDP-in-IP:TCPsegment作为UDP报文的载荷,通常使用linux提供的网络接口(UDPsocket),让内核直接构造UPD报文头、IP报文头和以太网报文头,并将构造出的数据包传输到下一层。由于是在内核中完成,linux kernel保证了 唯一的本地地址和端口、目的地址和端口的组合,且保证 不同进程隔离
- TCP-in-IP:通常可以直接将TCPsegment作为IP报文的载荷,这种称为“TCP/IP”。如果想直接构造IP报文,需要使用Linux提供的接口(TUN虚拟网络设备),接着linux kernel负责构造以太网帧以及发送。
这部分参考以下Lab4中使用到的。请阅读以下头文件和源文件:tcp_helpers/ipv4_datagram.hh、tcp_helpers/ipv4_datagram.cc、tcp_helpers/tcp_over_ip.cc
- TCP-in-IP-in-Ethernet:前两种实现以来linux的网络协议栈。向TUN网络虚拟设备写入IP报文时,Linux必须构造一个IP报文作为载荷的链路层(以太网)帧。所以Linux必须指导下一跳的以太网目的地址、IP地址,如果不知道这种映射,则在网络中广播探测请求下一跳的IP、以太网帧地址。此功能通常叫网络接口(network interface,或者适配器,名字一般是:eth0,eht1,wlan0),它将要 输出流的IP报文 转换成以太网帧,接着将帧发送给 TAP虚拟网络设备,具体发送就由此设备完成
网络接口的功能: 查找(缓存)下一跳的IP地址和以太网地址,这个协议通常叫 Address Resolution Protocol,简称 ARP
本次Lab5实验就是完成该网络接口。
The Address Resolution Protocol 地址解析协议
简略了解一下ARP:
主机或路由不具备链路层地址( MAC地址),但其对应的网络接口具有该地址。某个网络接口要向另外一个目的网络接口发送以太网帧时,发送的网络接口需要将目的网络接口的MAC地址插入该帧的目的mac地址中,以太帧的结构如下:
其中 EtherType 为 0x800时指IPv4数据报,0x806为ARP数据报
并将该帧发送到所在局域网上。网络接口可能收到广播的帧,其将检查和丢弃 帧的目的MAC地址与自己不匹配的以太网帧。
为什么网络接口除了有 网络层地址(IP),还有 链路层地址(MAC地址)?
局域网是为了 任意网络层协议设计的,范围不止用于IP和Internet
如果只是用IP地址,每一次移动或者重启,都需要 重新配置地址
但拥有两种地址,就需要互相转化,由ARP实现,类似DNS服务,ARP只能为 在同一个子网上的主机或路由接口 解析IP地址。每一个主机和路由中都有一个ARP缓存表,该表包含了 IP地址 到 MAC地址 的映射关系,还可能有其他选项比如接口名字、网络类型等。本次中由于需要额外记录删除表中项目的时间,所以添加一个最大存活时间 TTL,表示从表中删除该映射关系的时间,如图:
IP地址 | MAC地址 | TTL |
---|---|---|
192.168.4.3 | 00:50:56:f1:23:1d | 13:56:30 |
192.168.4.9 | 00:23:64:d2:f1:23 | 00:23:36 |
如果缓存表中有目的IP地址,将很容易找到对应的MAC地址,否则将构造一个ARP请求报文(和ARP报文一样格式,发送端的IP地址和MAC地址填自己的,目的端的IP地址填入,MAC地址全为0),然后在该 子网上广播。只有IP地址相同的网络接口相应。该网络接口自己缓存一份发送端的IP地址和MAC地址,接着 单播方式发送ARP报文给前面发送此请求的网络接口,该ARP响应报文中包含自己的MAC地址。
由于 ARP协议不提供对网络上的ARP回复进行身份验证,导致可以实施 ARP欺骗攻击,例如中间人或者DOS攻击。
详情查阅RFC826
实现 Network Interface
在arp缓存表中,每个IP地址映射一个MAC地址,但由于额外需要增加一个TTL记录,所以可以先将MAC地址与TTL组成一个单独数据结构,如下:
struct ARPEntry{
EthernetAddress eth_addr;
size_t
};
此数据结构将作为NetworkInterface类中private属性的数据成员声明。
此数据成员为ARP缓存表中的项,所以再额外添加三个数据成员和一个公共函数:
- _arp_table:ARP缓存表,用来查询IP地址到MAC地址的映射,其中MAC地址和TTL构成ARPEntry
每个ARPEntry中TTL为30s
- _waiting_arp_response_ip_addr:保存已经发送了ARP请求的IP地址,后面跟着一个TTL
此TTL为5s
- _waiting_arp_internet_datagram:保存等待ARP返回报文的IP报文。收到返回相应报文,网络接口才知道这些IP报文要发送的目的地MAC地址
实现注意事项:
- ARPEntry中的 TTL为30s,到期了就要将其ARP缓存表中删除
- 发送IP报文时,没在ARP缓存表中发现MAC地址,立即广播发送ARP请求报文,同时将此IP报文缓存记录,直到获取到目的地MAC地址后发送
- 同一ARP请求5s内没有相应,则需重新发送
- 不同IP地址的ARP请求报文的发送间隔不能超过5s
- 当网络接口收到一个链路层(以太网)帧:
- 立即丢弃目的地MAC地址和本机不同的帧
- 除了ARP协议需要比较自己的IP地址外,不要再其他任何地方进行IP比较,因为网络接口位于链路层
- 对于APR请求报文的构造,目的地MAC地址为全零,收到ARP请求报文时也需要忽略此项(ARPMessage::target_ethernet_address)
- 无论是 ARP请求包还是 ARP响应包均可以更新当前的ARP缓存表
头文件中代码:
//! ARP 条目
struct ARPEntry {
EthernetAddress eth_addr; //!< 以太网帧地址
size_t ttl; //!< 最大存活时间
};
//! 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;
//! outbound queue of Ethernet frames that the NetworkInterface wants sent
std::queue<EthernetFrame> _frames_out{};
//! 内部维护的arp表
std::unordered_map<uint32_t, ARPEntry> _arp_table{};
//! 正在查询的ARP报文。如果发送了ARP请求报文后,在TTL时间内没收到返回响应报文,则丢弃该IP报文
std::unordered_map<uint32_t, size_t> _waiting_arp_response_ip_addr{};
//! 等待ARP报文返回的待处理IP报文,每一个IP地址都映射到 IP报文等待发送列表
std::unordered_map<uint32_t, std::list<std::pair<Address, InternetDatagram>>> _waiting_internet_datagram{};
//! \brief网络接口必须具备发送以太网帧的功能
void _send(const EthernetAddress &det, const uint16_t type, BufferList &&payload);
public:
//! ARP条目默认最大存活时间为30s, 由于计数是毫秒,需要从新计算
static constexpr uint32_t ARP_ENTRY_TTL_MS = 30 * 1000;
//! ARP请求报文默认等待(发送间隔)为5s,由于计数是毫秒,需要重新计算
static constexpr uint32_t ARP_RESPONSE_TTL_MS = 5 * 1000;
源文件代码:
void NetworkInterface::send_datagram(const InternetDatagram &dgram, const Address &next_hop) {
// 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();
//查找ARP缓存表中是否有下一跳的IP,如果找到就返回该项的迭代器
auto arpTable_it = _arp_table.find(next_hop_ip);
if (arpTable_it != _arp_table.end()) {
//对于找到IP地址的,直接发送IP数据报
_send(arpTable_it->second.eth_addr, EthernetHeader::TYPE_IPv4, dgram.serialize());
} else {
// 5s内没有对该IP发送国ARP查询报文,则发送
if (_waiting_arp_response_ip_addr.count(next_hop_ip) == 0) {
ARPMessage arp_msg;
//构造请求报文
arp_msg.opcode = ARPMessage::OPCODE_REQUEST;
arp_msg.sender_ethernet_address = _ethernet_address;
arp_msg.target_ethernet_address = {0};
arp_msg.sender_ip_address = _ip_address.ipv4_numeric();
arp_msg.target_ip_address = next_hop_ip;
//以广播模式发送ARP请求报文
_send(ETHERNET_BROADCAST, EthernetHeader::TYPE_ARP, arp_msg.serialize());
//需要将此报文加入等待ARP回复的记录表
_waiting_arp_response_ip_addr[next_hop_ip] = ARP_RESPONSE_TTL_MS;
}
//还需要将该未发送的IP报文加入等待发送记录表尾部
_waiting_internet_datagram[next_hop_ip].emplace_back(next_hop, dgram);
}
}
//! \param[in] frame the incoming Ethernet frame
optional<InternetDatagram> NetworkInterface::recv_frame(const EthernetFrame &frame) {
//直接过滤非广播帧和目的MAC地址不是自己的帧
if (frame.header().dst != ETHERNET_BROADCAST && frame.header().dst != _ethernet_address) {
return nullopt;
}
//如果协议类型是IPv4,则解析成功后返回
if (frame.header().type == EthernetHeader::TYPE_IPv4) {
InternetDatagram ret;
//需要成功解析才返回解析结果,否则返回空
if (ret.parse(frame.payload()) == ParseResult::NoError) {
return ret;
}
return nullopt;
}
//如果协议类型是ARP,则从中查看是否可以获得新的IP->MAC的映射关系
//可以从ARP请求和响应报文中获得新的arp缓存
if (frame.header().type == EthernetHeader::TYPE_ARP) {
ARPMessage arp_msg;
//解析失败,则返回空
if (arp_msg.parse(frame.payload()) != ParseResult::NoError) {
return nullopt;
}
const uint32_t self_ip = _ip_address.ipv4_numeric();
const uint32_t src_ip = arp_msg.sender_ip_address;
//处理发送给本机的ARP请求报文,需要将自己的MAC地址填入响应报文返回给请求的主机
if (arp_msg.opcode == ARPMessage::OPCODE_REQUEST && arp_msg.target_ip_address == self_ip) {
ARPMessage arp_reply;
//构造返回响应报文
arp_reply.opcode = ARPMessage::OPCODE_REPLY;
arp_reply.sender_ethernet_address = _ethernet_address;
arp_reply.target_ethernet_address = arp_msg.sender_ethernet_address;
arp_reply.sender_ip_address = self_ip;
arp_reply.target_ip_address = src_ip;
_send(arp_msg.sender_ethernet_address, EthernetHeader::TYPE_ARP, arp_reply.serialize());
}
//对于接收到的ARP报文都可以获得新的ARP缓存
_arp_table[src_ip] = {arp_msg.sender_ethernet_address, ARP_ENTRY_TTL_MS};
//查看等待发送的IP报文记录表中是否符合该IP的
auto to_send_ip = _waiting_internet_datagram.find(src_ip);
if (to_send_ip != _waiting_internet_datagram.end()) {
for (const auto &[next_hop, dgram] : to_send_ip->second) {
_send(arp_msg.sender_ethernet_address, EthernetHeader::TYPE_IPv4, dgram.serialize());
}
_waiting_internet_datagram.erase(to_send_ip);
}
}
return nullopt;
}
//! \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 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 = std::next(it);
}
}
//还需要删除等待ARP回复报文列表中超时的项,不做重发处理
for (auto it = _waiting_arp_response_ip_addr.begin(); it != _waiting_arp_response_ip_addr.end();) {
if (it->second <= ms_since_last_tick) {
//没有收到请求MAC地址的IP地址作为记录的回复报文,直接丢弃
auto it1 = _waiting_internet_datagram.find(it->first);
if (it1 != _waiting_internet_datagram.end()) {
_waiting_internet_datagram.erase(it1);
}
it = _waiting_arp_response_ip_addr.erase(it);
} else {
it->second -= ms_since_last_tick;
it = std::next(it);
}
}
}
//! \param[out] void 无返回值
//! \param[in] det 目的网络接口MAC地址
//! \param[in] type 网络协议类型,用uint16_t类型表示
//! \param[in] payload 载荷,为了BufferList类型右引用
void NetworkInterface::_send(const EthernetAddress &dst, const uint16_t type, BufferList &&payload) {
EthernetFrame eth_frame;
eth_frame.header().src = _ethernet_address;
eth_frame.header().dst = dst;
eth_frame.header().type = type;
eth_frame.payload() = std::move(payload);
_frames_out.push(std::move(eth_frame));
}
然后为了测试,需要修改webget.cc文件中的一行代码:
//将lab4用到的CS144TCPSocket注释掉,换成FullStackSocket
//CS144TCPSocket tcpsock;
FullStackSocket tcpsock;
先是执行arp相关的测试,在terminal中build目录下:
make -j4
ctest -V -R "^arp"
结果如下图:
然后执行Lab5的测试,同样在terminal中build目录下:
sudo make check_lab5
测试结果如下:
感觉前面设计不给力,导致后续实验的效率都好低呀········