解析原始字节
现在已经设置了虚拟网络接口并且它接收了数据位,实现 TCP 之旅的下一步是从接收到的数据字节中解析出数据包。默认情况下,除了从虚拟网络接口发送和接收的数据包之外,还会在数据包前面附加 4 个字节的数据。 Tun/TAP documentation section 3.2 告诉我们收到的数据的结构。前 2 个字节是 flags ,可以提供有关收到的数据包的更多信息,例如,内核设置的“TUN_PKT_STRIP”,用于向用户空间程序发出信号,表明数据包因缓冲区太小而被截断。接下来的两个字节是 proto 字段,它指定协议的 IP 版本。在前 4 个字节之后,其余数据是 原始协议。因此,我们将修改代码以解析标flags 和 proto字节。为此,可以使用 u16::from_be_bytes
方法读取数据包的前 2 个字节,并获取人类可读的flags 值。但要注意,网络顺序是大端字节序,因此我们使用 from big endian bytes 方法。将 循环体内容 替换为以下内容
loop {
let nbytes = nic.recv(&mut buf[..])?;
let flags = u16::from_be_bytes([buf[0], buf[1]]);
let proto = u16::from_be_bytes([buf[2], buf[3]]);
eprintln!("read {} bytes: {:x?}", nbytes - 4, &buf[4..nbytes]);
}
现在,当运行程序时,可以注意到 flags 和proto 字段被打印出来。在例子中,flags打印出值 0 , proto 打印出“86dd”。为了理解 proto 字段的含义,可以查看这张将 ether 类型 映射 到协议的表,可以查到我们解析的 proto 字段的值对应于互联网协议版本 6 (IPv6)。
由于我们在此实现中将重点关注 ipv4,因此可以通过添加此行在代码中添加过滤条件,以忽略 ether 类型不是 ipv4 的任何数据包
if proto != 0x0800{
//This is Not IPV4 skip it.
continue;
}
现在已经解析了内核在以太网帧前添加的前 4 个字节,剩下的就是 TCP 数据,需要解析它。由于我们主要关心的是协议的实现,因此使用一个第三方 crate 来帮助解析 IP 数据包和 TCP 信息。尽管使用 第三方 crate 来进行解析,但了解正在发生的情况仍然很重要。我们本质上是在代码中 解码 IPV4 头 .
一旦解码 IP 数据包,将能够获得一些重要信息,例如目标地址、源地址和协议。我们将用etherparse
包解析 IP 数据包头 。要将其添加到项目中,请将以下内容添加到cargo.toml中
etherparse = "0.13.0"
// Attempt to parse an IPv4 header from the provided buffer slice.
match etherparse::Ipv4HeaderSlice::from_slice(&buf[4..nbytes]) {
// If the parsing was successful, proceed with the parsed packet.
Ok(iph) => {
// Extract the source IP address from the parsed packet.
let src = iph.source_addr();
// Extract the destination IP address from the parsed packet.
let dst = iph.destination_addr();
// Extract the protocol number from the parsed packet.
// For TCP, this number is typically 6 (0x06).
let proto = iph.protocol();
// Check if the protocol number is not TCP (0x06).
if proto != 0x06 {
// If the packet is not a TCP packet, skip further processing.
continue;
}
// Attempt to parse the TCP header from the buffer slice.
// Here, we adjust the starting slice based on the length of the IPv4 header.
match etherparse::TcpHeaderSlice::from_slice(&buf[4 + p.slice().len()..]) {
// If TCP header parsing was successful, proceed.
Ok(tcph) => {
// Print the details: Source IP, Destination IP, and the Destination Port.
eprintln!("{} -> {}: TCP to port {}", src, dst, tcph.destination_port());
}
// Handle potential errors while parsing the TCP header.
Err(e) => {
eprintln!("An error occurred while parsing TCP packet: {:?}", e);
}
}
}
// Handle potential errors while parsing the IPv4 header.
Err(e) => {
eprintln!("An error occurred while parsing IP packet: {:?}", e);
}
}
运行上面的代码并使用 TCP 客户端 ping 我们的应用程序时, nc 192.168.0.2 80
应该看到 TCP 数据包与目标地址、源地址和协议一起被接收。
References 参考
- Corresponding Code 对应代码