December 23, 2018
原文链接 http://www.saminiir.com/lets-code-tcp-ip-stack-1-ethernet-arp/
TCP 中最重要的部分:
retransmission timeout computation - 超时重试
TUN 用来操作 L3 的流量 TAP 用来操作 L2 的流量
TAP 相当于一个以太网设备,他用来操作 L2 的数据包如以太网数据帧 TUN 模拟了网络层设备,操作 L3 的数据包如 IP 数据封包
操作系统使用 TAP 和 TUN 设备向绑定该设备的用户空间程序发送数据,同样的用户空间的程序也会向这两个设备发送数据。感觉类似于真实物理设备与用户程序之间的一层代理。
/*
* Taken from Kernel Documentation/networking/tuntap.txt
*/
int tun_alloc(char *dev)
{
struct ifreq ifr;
int fd, err;
if( (fd = open("/dev/net/tap", O_RDWR)) < 0 ) {
print_error("Cannot open TUN/TAP dev");
exit(1);
}
CLEAR(ifr);
/* Flags: IFF_TUN - TUN device (no Ethernet headers)
* IFF_TAP - TAP device
*
* IFF_NO_PI - Do not provide packet information
*/
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
if( *dev ) {
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
}
if( (err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0 ){
print_error("ERR: Could not ioctl tun: %s\n", strerror(errno));
close(fd);
return err;
}
strcpy(dev, ifr.ifr_name);
return fd;
}
用 open 方法拿到 “/dev/net/tap” 的文件描述符放在 fd 里,之后就可以用 fd 读取和写入以太网虚拟设备的缓冲区中的数据。
使用上面的代码就可以启动一个 TAP 设备,IFFNOPI 表示不需要额外的包的信息。
如果想要启动一个 TUN 设备,只需要把第 21 行的 IFFTAP 改为 IFFTUN 即可。
https://zh.wikipedia.org/wiki/%E4%BB%A5%E5%A4%AA%E7%BD%91%E5%B8%A7%E6%A0%BC%E5%BC%8F
以太网的第一个版本比现在的速度慢很多,大概在 10MB/s ,并且是半双工的,即要么在读,要么在写,不能同时读和写。
100 BASE - T 以太网标准中,使用双绞线来实现全双工通信以及更高的吞吐速度。此外,以太网交换机的普及加速了 CSMA/CD 的过时。
它的工作原理是: 发送数据前 先侦听信道是否空闲 ,若空闲,则立即发送数据。若信道忙碌,则等待一段时间至信道中的信息传输结束后再发送数据;若在上一段信息发送结束后,同时有两个或两个以上的节点都提出发送请求,则判定为冲突。若侦听到冲突,则立即停止发送数据,等待一段随机时间,再重新尝试。
简单总结为:先听后发,边发边听,冲突停发,随机延迟后重发
有人将CSMA/CD的工作过程形象的比喻成很多人在一间黑屋子中举行讨论会,参加会议的人都是只能听到其他人的声音。每个人在说话前必须先倾听,只有等会场安静下来后,他才能够发言。人们将发言前监听以确定是否已有人在发言的动作称为”载波监听”;将在会场安静的情况下每人都有平等机会讲话成为“多路访问”;如果有两人或两人以上同时说话,大家就无法听清其中任何一人的发言,这种情况称为发生“冲突”。发言人在发言过程中要及时发现是否发生冲突,这个动作称为“冲突检测”。如果发言人发现冲突已经发生,这时他需要停止讲话,然后随机后退延迟,再次重复上述过程,直至讲话成功。如果失败次数太多,他也许就放弃这次发言的想法。通常尝试16次后放弃。
了解完这些,我们来看看以太帧的数据结构:
#include <linux/if_ether.h>
struct eth_hdr
{
unsigned char dmac[6]; // 目标 MAC 地址
unsigned char smac[6]; // 源 MAC 地址
uint16_t ethertype; // 有效负载的长度或者类型
unsigned char payload[];
} __attribute__((packed));
ethertype 的值大于或等于 1536 ,其中就包含有效负载的类型信息(IPv4, ARP 等),小于该值,则其只包含有效负载的长度信息。在开头引入了 if_ether.h 文件,用来提供 ethertype 及其 16 进制之间的映射。
以太帧中可能会包含 标签 信息,这些标签可以用来描述他属于哪个 VLAN 或者优先级(QOS)
payload 字段包含以太帧负载的指针,这里会包含 IPv4 或者 ARP 的数据包。
在上面的声明中,还缺少了对冗余校验的定义,在真实的以太帧中,需要包含一个帧校验码,用来循环冗余校验帧的完整性。
在上面声明的 struct 最后使用了 packed,这是用来告知 GNU 的 C 编译器不要用 padding bytes 的方式优化 struct 的内存结构以使其数据结构对齐。
数据结构对齐是代码编译后在内存中的布局和使用方式。包括:数据对齐、数据结构填充(padding)、包入(packing)
现代计算机一般是32比特或64比特地址对齐,如果要访问的变量没有对齐,可能会触发总线错误。
当数据小于计算机的字(word)尺寸,可能把几个数据元素放在一个字中,称为包入(packing)。
许多编程语言自动处理数据结构对齐。
更多相关讨论可以看看这里 —— https://stackoverflow.com/questions/4306186/structure-padding-and-packing
if (tun_read(buf, BUFLEN) < 0) {
print_error("ERR: Read from tun_fd: %s\n", strerror(errno));
}
struct eth_hdr *hdr = init_eth_hdr(buf);
// 查看 ethertype 字段值,并根据其值判断下一步逻辑
handle_frame(&netdev, hdr);
ARP 协议用来将 MAC 地址动态映射到协议地址(例如:IPv4 地址)
ARP 数据包的格式相对简单:
struct arp_hdr
{
uint16_t hwtype; // 硬件类型,eg: 以太网(0x0001)
uint16_t protype; // 协议类型
unsigned char hwsize; // 硬件地址长度
unsigned char prosize; // 协议地址长度
uint16_t opcode; // 操作码
unsigned char data[]; // 实际的负载信息
} __attribute__((packed));
// data 中还会包含 IPv4 信息
struct arp_ipv4
{
unsigned char smac[6]; // 源 MAC
uint32_t sip; // 源 IP
unsigned char dmac[6]; // 目标 MAC
uint32_t dip; // 目标 IP
} __attribute__((packed));
规范中描述的实现方式:
?Do I have the hardware type in ar$hrd?
Yes: (almost definitely)
[optionally check the hardware length ar$hln]
?Do I speak the protocol in ar$pro?
Yes:
[optionally check the protocol length ar$pln]
Merge_flag := false
If the pair <protocol type, sender protocol address> is
already in my translation table, update the sender
hardware address field of the entry with the new
information in the packet and set Merge_flag to true.
?Am I the target protocol address?
Yes:
If Merge_flag is false, add the triplet <protocol type,
sender protocol address, sender hardware address> to
the translation table.
?Is the opcode ares_op$REQUEST? (NOW look at the opcode!!)
Yes:
Swap hardware and protocol fields, putting the local
hardware and protocol addresses in the sender fields.
Set the ar$op field to ares_op$REPLY
Send the packet to the (new) target hardware address on
the same hardware on which the request was received
看上去也不是很复杂,就是一些操作检查,然后针对不同的行为做相应的处理。
ARP 的实现中,会把 IP 和 MAC 地址的关系维护在一张缓存表里,以减少 ARP 广播请求。
作者对 ARP 的实现 —— https://github.com/saminiir/level-ip/blob/e9ceb08f01a5499b85f03e2d615309c655b97e8f/src/arp.c#L53
痕迹
没有过去,就没法认定现在的自己