关于如何发送TCP segment
- user负责 TCP
- user负责 TCP,IP
- user负责 TCP IP Ethernet
- CS144目标实现的就是从TCP到Ethernet全部在用户态实现.
- Ethernet报文的封装和解封装就由NetWorkInterface来做
实现一个NetWorkInterface
- 发送IP datagram –封装–> linker-layer frame
- 接收 linker-layer frame —解封–> IP datagram
- 在执行封装IP datagram成linker-layer frame时 , 需要ip->mac地址,这时就需要用到arp协议
- 涉及处理 arp table , 缓存未知mac的ipdatagram等
背景
本实验,我们将到达协议栈的最底层 并实现一个 InetWork Interface : 一个将Internet Datagram 送入世界的桥梁 ; 并且实现在每一跳之间传送的链路层Ethernet以太网帧(frame).
- 这个InetWork Interface组件适用于你之前实现的TCP/IP的底层 , 并且可以以另一种不同的上下文使用 : 当你在lab6中建立了路由器router,其将在network interface之间路由datagrams. Figure 1 展示了network interface在主机和路由器两种上下文下的层次位置
- 这个InetWork Interface组件适用于你之前实现的TCP/IP的底层 , 并且可以以另一种不同的上下文使用 : 当你在lab6中建立了路由器router,其将在network interface之间路由datagrams. Figure 1 展示了network interface在主机和路由器两种上下文下的层次位置
在过去的实验中 你实现了一个可以成功和其他主机交流TCP segment . 这些segment 实际上到底是如何传送到对端的tcp呢 ? 有以下几个选项
How are these segments actually conveyed to the peer’s TCP implementation?
如何封装并发送TCP segment
TCP-in-UDP-in-IP
TCP-in-UDP-in-IP
TCP segment 可以作为 user datagram 的 payload. 当tcp运行在用户态 , 这是一种最简单的实现.
1
# Ethernet Header # IP Header # UDP Header # [ TCPSegment (TCP Header # Http # payload) ]
Linux提供了一个udp socket的接口
- code : class UDPSocket -> socket(AF_INET, SOCK_DGRAM , 0)
- udp socket的user只需提供user datagram(udp datagram)的payload 和 目标 address(ip and port). kernel的send接口如下
1
2
3
4
5
6
7
8
9msghdr message{};
message.msg_name = const_cast<sockaddr *>(destination_address);
message.msg_namelen = destination_address_len;
message.msg_iov = iovecs.data();
message.msg_iovlen = iovecs.size();
sendmsg(fd_num, &message, 0);
// msghr message : 目标ip和port(sockaddr*) + 要发送数据payload
// 本lab中 该sendmsg 用于通过udp socket fd 发送 udp segment
// udp 的 payload 为 tcp segment. 存入 msghdr - kernel 负责构造UDP header , IP header 和 Ethernet header , 然后将packet发送到正确的下一跳(next hop)
- kernel 负责确保 每个socket都由独占的 本地和远端的端口组合(exclusive combination of local and remote addresses and port numbers).
- 由于kernel 负责构造 UDP 和 IP header , 他会保证不同应用之间的独立
听起来很神奇是吧,居然udp的负载是tcp segment. code见下
TCP-In-IP
TCP-in-IP
普遍情况下 , TCP Segment 是直接放入 Internet datagram中的 , 没有udp header夹在 ip header和tcp header之间
- 这就是人们所说的 “TCP/IP”
1
# Ethernet Header # IP Header # [ TCPSegment (TCP Header # Http Header # payload) ]
- 这就是人们所说的 “TCP/IP”
比上一个难实现一点. Linux 提供接口 : TUN device
- 让应用提供完整的Internet datagram. 也即应用需要提供IP Header 而不仅仅是payload
- kernel 负责剩余部分
- writing the Ethernet header 构造 Ethernet Header
- actually sending via the physical Ethernet card 实际通过网卡发送帧
我们已经完整这个了.
- 在lab4中, 我们有一个object 代表 Inernet datagrams 并且 其知道如何解析和序列化自身. (tcp helpers/ipv4_datagram)
- The CS144TCPSocket uses these tools to connect your TCPConnection to a TUN device
- 实验都做完之后 先分析fullstack 然后分析cs144tcp
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//! \class TCPSpongeSocket
//! This class involves the simultaneous operation of two threads.
//!
//! One, the "owner" or foreground thread, interacts with this class in much the
//! same way as one would interact with a TCPSocket: it connects or listens, writes to
//! and reads from a reliable data stream, etc. Only the owner thread calls public
//! methods of this class.
//!
//! The other, the "TCPConnection" thread, takes care of the back-end tasks that the kernel would
//! perform for a TCPSocket: reading and parsing datagrams from the wire, filtering out
//! segments unrelated to the connection, etc.
//!
//! There are a few notable differences between the TCPSpongeSocket and TCPSocket interfaces:
//!
//! - a TCPSpongeSocket can only accept a single connection
//! - listen_and_accept() is a blocking function call that acts as both [listen(2)](\ref man2::listen)
//! and [accept(2)](\ref man2::accept)
//! - if TCPSpongeSocket is destructed while a TCP connection is open, the connection is
//! immediately terminated with a RST (call `wait_until_closed` to avoid this)
//! Helper class that makes a TCPOverIPv4SpongeSocket behave more like a (kernel) TCPSocket
class CS144TCPSocket : public TCPOverIPv4SpongeSocket {
public:
CS144TCPSocket();
void connect(const Address &address);
};
TCP-in-IP-in-Ethernet
TCP-in-IP-in-Ethernet
- 在TCP-in-IP方法中 , 我们依然依赖于Linux kernel 去实现部分networking stack.
- 每次我们的code write一个IP datagram到TUN device , Linux kernel需要负责创建一个正确的链路层frame(如Ethernet frame). 将我们的IPdatagram作为帧的payload.
- 这就意味着Linux kernel需要负责依据给定的IP addr , 来找出下一跳的正确Mac addr ; 如果 kernel还不知道该< ip-mac > , 其需要广播一个ARP查询分组 (asks, “Who claims the following IP address? What’s your Ethernet address?” and waits for a response.)
- 这些函数由network interface负责执行
- network interface 功能 : 转换 IP datagram和 linker-layer frame(Ethernet)
- 在真实系统中 , network interface 的经典命名是(eth0, eth1, wlan0, etc.)
- lab5 , 实现network interface , 并将其插入到我们TCP/IP协议栈的最底层.
- 我们的代码将生成未经加工的 Ethernet frame.
- 该frame会被传递给Linux的TAP device(比 TUN device更底层). TAP device传输未经加工的我们生成的linker-layer frames.
- 下图中
1
# Ethernet Header # IP Header # UDP Header # [ TCPSegment (TCP Header # Http # payload) ]
- TAP device介绍
- tap是链路层的虚拟网络设备,等同于一个以太网设备,它可以收发第二层数据报文包,如以太网数据帧。Tap最常见的用途就是做为虚拟机的网卡,因为它和普通的物理网卡更加相近,也经常用作普通机器的虚拟网卡。
- TAP device接收上层构造好的链路层帧(link-layer frames)并直接发送出去 ;
- TUN device接收上层的IP数据报(IP datagrams) , TUN负责构造链路层帧(link-layer frames) 再发送出去
- network interface的主要工作都在于ARP协议 : 获取IP addr对应的 Ethernet addr.
地址解析协议 Address Resolution Protocol
ARP : Address Resolution Protocol
- 负责网络层地址(IP地址)和链路层地址(MAC地址)的转换
- 负责网络层地址(IP地址)和链路层地址(MAC地址)的转换
在LAN上的每个IP节点都有一个ARP表
ARP表:包括一些LAN节点IP/MAC地址的映射
- < IP address; MAC address; TTL>
- TTL时间是指MAC地址映射失效的时间 ,典型是20min
A要发送帧给B(B的IP地址已知),A需要根据ipB解析出macB.
- 如果arp table中有该ipB的entry,那么易得mac地址
- 如果arp table中没有该ipB的entry(如222.222.222.222),那么如何 ?
- A广播包含B的IP地址的ARP查询包
- ARP查询分组
- 目的地址使用MAC广播地址,子网LAN上的所有节点都会收到该查询包
- Dest MAC address = FF-FF-FF-FF-FF-FF
- 子网中的其他适配器接收到该用于arp查询的帧,将该帧中的ARP分组向上传递给arp模块,arp模块检验该arp分组要查询的ip地址是否是本机的ip地址.
- 无论是否匹配 , 都缓存下收到的该查询分组< sender_ip - sender_mac_addr >
- 若匹配, B给A发送回一个响应ARP分组,告知其B的MAC地址 ; 该响应分组的目的地址为A的MAC地址
- A广播包含B的IP地址的ARP查询包
ARP报文结构
ARP是即插即用的 : 节点自己创建ARP的表项 ; 无需网络管理员干预
ARP协议是网络层协议还是链路层协议.
- ARP分组既包含链路层地址,又包含网络层地址.
- 将ARP看成是跨越链路层和网络层边界的协议
1
2
3
4
5
6
7
8
9
10===================
传输层
TCP/UDP
===================
网络层
ICMP
IP
======# ARP #=======
链路层
Ethernet 以太网帧
- A发出的ARP查询报文 , B响应arp分组
- dst_mac src_mac type 是 Ethernet报文字段. 只有mac addr 而没有ip addr
- opcode开始 是 arp报文. arp message中既有ip addr又有mac addr.
- arp查询分组的目的mac地址是广播地址 , 该BROADCAST地址填入链路层linker-layer(Ethernet)的dst mac addr , 而非 arp 报文字段中的dst mac addr. arp 报文字段中的dst mac addr应该填0
dst_mac src_mac type opcode sender_mac sender_ip target_mac target_ip FF-FF-FF-FF-FF-FF mac_A ARP REQUET mac_A ip_A 00-00-00-00-00-00 ip_B mac_A mac_B ARP REPLY mac_B ip_B mac_A ip_A
接口及实现
简单 按照逻辑实现就好 比lab0-4简单多了去了
class NetworkInterface 重要成员
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本network interface mac addr
//! Ethernet (known as hardware, network-access-layer, or link-layer) address of the interface
EthernetAddress _ethernet_address;
本network interface ip addr
//! IP (known as internet-layer or network-layer) address of the interface
Address _ip_address;
frames to send
//! outbound queue of Ethernet frames that the NetworkInterface wants sent
std::queue<EthernetFrame> _frames_out{};
arp_table: <mac addr , ttl>
using MacAddrInfo = std::pair<EthernetAddress,int>;
static const int TTL = 30 * 1000; // keep each <ip-mac> for 30s
// ARP table : <IP addr , MAC addr>
std::unordered_map<uint32_t,MacAddrInfo> _arp_table{};
由于不知道ip对应的mac , 等待被发送的datagrams
// data buffer : <IP addr , datagrams>
std::unordered_map<uint32_t,std::vector<InternetDatagram> > _data_buffer{};
// ip - time_since_last_req
对于每个ip的请求过去的时间
std::unordered_map<uint32_t,int> _wait_for_req{};
static const int WAITING_TIME = 5 * 1000;network interface 功能 : 在 IP datagram 和 linker-layer frame(Ethernet) 之间转换
- 发送IP datagram -> linker-layer frame
- 接收 linker-layer frame -> IP datagram
- 在执行封装ipdatagram成linker-layer frame时 , 需要ip->mac地址,这时就需要用到arp协议
void NetworkInterface::send_datagram(const InternetDatagram &dgram,const Address &next_hop)
- This method is called when the caller (e.g., your TCPConnection or a router) wants to send an outbound Internet (IP) datagram to the next hop.1
- 将 IP datagram 封装成 Ethernet frame , 发送到下一跳next_hop
- 需要获取next_hop对应的mac addr
- send TYPE_IPv4 : 已知mac , 则直接使用mac addr,填入ethernet字段,封装成帧,发送
- send TYPE_ARP : 未知mac , 广播arp request分组(查询分组封装成帧并发送),将该IP datagram缓存起来 收到arp reply后发送.
- 对于同一ip对应的mac 的arp request分组 , 每5s至多发送一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if (find <ip-mac> and not expired) {
// Encapsulate the IP datagram into Ethernet frame and send it
ethernet_frame = buildEthernetFrame(TYPE_IPv4,string(datagram));
send ipv4 frame
} else {
// If the network interface already sent an ARP request about the same IP address in the last five seconds, don’t send a second request—just wait for a reply to the first one
if(pass 5s since last req)
{
arp = buildArpRequest(next_hop_ip);
ethernet_frame = buildEthernetFrame(EthernetHeader::TYPE_ARP,string(arp));
send arp req
}
// queue the dgram
_data_buffer[next_hop_ip].push_back(dgram);
}
- 对于同一ip对应的mac 的arp request分组 , 每5s至多发送一个
- 需要获取next_hop对应的mac addr
optional
NetworkInterface::recv_frame(const EthernetFrame &frame) - This method is called when an Ethernet frame arrives from the network. The code should ignore any frames not destined for the network interface (meaning, the Ethernet destination is either the broadcast address or the interface’s own Ethernet address stored in the ethernet address member variable).
- 接收从network中发来的linker-layer frame.
- 对于不是本网卡需要接收的frame,丢弃
- 本网卡接收的frame : frame.dst_mac = local_mac || frame.dst_mac = ff-ff-ff-ff-ff-ff
- 对于frame的payload
- IPv4 : return the resulting InternetDatagram to the caller.
- ARP :
- 无论是reply还是request,记录下该分组的< sender_ip - sender_mac > . 每个entry的ttl为30s。
- 及时发送之前由于mac未知而缓存的frame
- 对于arp request , 发送一个arp reply. 封装成帧, 发送.
- 无论是reply还是request,记录下该分组的< sender_ip - sender_mac > . 每个entry的ttl为30s。
- 丢弃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20if (checkInValidFrame(frame)) { // dst is local mac addr or broad cast
return {};
}
if (recv IPv4) {
return ipv4_data;
}
else if (recv ARP){
record <ip-mac> in arp table
clear waiting buffer of ip : send waiting datagrams
if (arp is REQUEST and arp.dst_ip is to us) {
arp_reply = buildArpReply;
ethernet_frame = buildEthernetFrame(
TYPE_ARP, arp_reply.serialize());
send an ARP reply
}
}
return {};
void NetworkInterface::tick(const size_t ms_since_last_tick)
- 处理ARP table(即IP mac表)中的entry过期(TTL)
- 处理等待ARP reply的ip