在 Rust 中借助 libpnet 重造一个 tcpdump 的轮子
软件工程师、技术图书译者。译有《图解云计算架构》《图解量子计算机》《图解TCP/IP(第6版)》《计算机是怎样跑起来的》《自制搜索引擎》等。今天,我们看一看如何在 Rust 中借助libpnet这个库(crate)来制作一个类似 tcpdump 的工具,以此来捕获并显示网络中的数据包。
软件工程师、技术图书译者。译有《图解云计算架构》《图解量子计算机》《图解TCP/IP(第6版)》《计算机是怎样跑起来的》《自制搜索引擎》等。
今天,我们看一看如何在 Rust 中借助 libpnet
这个库(crate)来制作一个类似 tcpdump 的工具,以此来捕获并显示网络中的数据包。该工具具有如下功能:
- 捕获流入指定网络接口(NIC)的数据包
- 捕获通过 IPv4 或 IPv6 传输的 TCP 和 UDP 的数据包
- 显示数据包的源以及目标的 IP 地址和端口号
- 以十六进制数和可打印的 ASCII 字符的格式显示应用层的数据
复习一下枯燥的网络知识
在开始编写代码之前,让我们先来复习一些看起来很枯燥的网络知识。本文将不同层的 PDU〔Protocol Data Unit,协议数据单元〕统称为数据包,并在括号中给出正式的名称。
众所周知,网络采用分层模型来描述数据从一台计算机传输到另一台计算机的过程。与软件架构中的层级结构一样,网络层级结构中的每一层也是接受由下一层提供的服务,并为上一层提供服务。这样一来,每一层中的协议都可以独立使用,即使改变某些层中的协议,也不会波及整个系统,提升了系统的扩展性和灵活性。
TCP/IP 协议分层模型是目前广泛使用的分层模型,模型中共有 4 层,自下而上依次是数据链路层、网络层、传输层、应用层。从发送数据的计算机的角度来看,每一层在处理由上一层(可将应用程序看作应用层的上一层)传过来的负载(实际的数据)时,需要根据当前层的协议,在负载前面附加相应的首部信息,再将附带了首部的数据传递给下一层。而从接收端的角度来看,每一层都会对接收到的数据进行首部与负载的分离,并将去掉首部的负载传递给上一层,最终将发送端的数据恢复为原状。
位于不同分层中的协议各有各的用途,如网络层的 IP 协议能够尽力(best effort)将数据包传递到目标主机,而 TCP 协议提供了可靠的数据传输。
这些不同层次的协议有没有共性呢?其实是有的,那就是每一层的协议都有地址字段,每一层(应用层除外)都有字段表明上一层所采用的协议的类型。
图中红框内的字段都表示地址(或许可以将域名看作是应用层的地址),绿框内的字段都表示上一层协议的类型。传输层的源端口号和目的端口号既是地址(表明是哪个进程),又可看作是上一层应用层的协议类型。
网络知识就先复习到这里,下面我们来看代码部分。希望后面 Rust 的代码能让这些枯燥的知识会变得生动起来。
代码分析
本节只梳理了主要的代码,完整的代码放在文末供大家参考。
首先,我们通过如下命令创建一个项目。
$ cargo new packet-capture
然后在 Cargo.toml
中加入 libpnet
这个依赖的 crate
。
[dependencies]
pnet = "0.34.0"
之所以要使用第三方的 libpnet
(docs.rs/pnet/latest…),是因为标准模块 std::net
只提供了传输层套接字(如 TCP 套接字或 UDP 套接字)的相关功能,无法处理传输层以下的协议的数据包。而 libpnet
能够处理数据链路层到传输层的各种协议。使用这个 libpnet
不但可以轻松创建和发送各种协议的数据包,还可以将接收到的数据包层层拆解。
我们自制的精简版 tcpdump 的整体流程如下图所示。
这里的核心逻辑只有一个:先分离首部和负载,然后根据首部选择下一层的协议,再将负载交给下一层的协议处理。
选择一个要监视的网络接口
首先是选择一个要监视的网络接口。
use pnet::datalink;
// ...
fn main() {
let args: Vec<String> = env::args().collect();
// ...
let interface_name = &args[1]; // ①
let interfaces = datalink::interfaces(); // ②
let interface = interfaces
.into_iter()
.filter(|iface| iface.name == *interface_name)
.next()
.expect("Failed to get interface");
// ...
我们自制的 tcpdump 暂时只接收一个参数——要监视(捕获其中的数据包)的网络接口的名称①(相当于 tcpdump
的 -i
参数)。先根据这个名称找到对应的网络接口(interface
变量)。datalink::interfaces()
会返回本机可用的网络接口列表②(类似 ifconfig
命令)。
根据名称确定了要监视的网络接口以后,就可以调用 datalink::channel(&interface, Default::default())
来获取数据链路层的 channel
①,从而在数据链路层上收发数据了。这里的 Default::default()
表示使用默认的配置。
fn main() {
// ...
let (_tx, mut rx) = match datalink::channel(&interface, Default::default()) { // ①
Ok(Ethernet(tx, rx)) => (tx, rx), // ②
Ok(_) => panic!("Unhandled channel type"),
Err(e) => {
panic!("Failed to create datalink channel {}", e)
}
};
// ...
datalink::channel()
的返回值类型为 Result<Channel, Error>
,而 Channel
的类型是一个 enum,
pub enum Channel {
Ethernet(Box<dyn DataLinkSender, Global>, Box<dyn DataLinkReceiver, Global>),
}
这里使用 match
表达式来处理 datalink::channel()
返回的 Channel
类型的 enum。Ethernet
是 enum 中的一个成员(variant,类型自然是Channel
),并关联了两项数据,而模式 Ethernet(tx, rx)
②刚好也有两个变量 tx
和 rx
,所以 tx
的值就是第一项数据,rx
的值就是第二项数据。
也就是说我们通过 tx
和 rx
把与 Channel::Ethernet(DataLinkSender, DataLinkReceiver)
这个 enum 的成员关联的两项数据提取出来了。_tx
用于在数据链路层上发送数据,rx
用于接收数据。
下面,我们来看如何处理通过 rx
接收到的数据链路层上的数据。
处理在数据链路层接收到的数据
fn main() {
loop {
match rx.next() { // ①
Ok(frame) => {
let frame = EthernetPacket::new(frame).unwrap(); // ②
match frame.get_ethertype() { // ③
EtherTypes::Ipv4 => {
ipv4_handler(&frame); // ④
},
EtherTypes::Ipv6 => {
ipv6_handler(&frame);
}
_ => {
println!("Not a ipv4 or ipv6");
}
}
},
Err(e) => {
panic!("Failed to read: {}", e);
}
}
}
}
首先要通过在一个 loop
循环中反复调用 rx.next()
来不断获取数据链路层中的数据包①。
rx.next()
的返回值类型为 Result<&[u8], Error>
,表示在数据链路层接收到的内容——要么是一个以太网的数据包(也叫数据帧〔ethernet frame〕),类型为 &[u8]
,要么是一个错误信息。这里继续使用 match
来处理这两种情况。
对于扁平的、字节序列形式的以太网的数据包,我们通过调用 EthernetPacket::new(frame)
将它解析成结构化的数据类型 EthernetPacket
②,并存放到frame
变量中。
去掉了以太网数据包的首部后,若从数据链路层的角度看,剩余部分是以太网数据包的负载(payload),而若从网络层的角度来看,这一部分是整个网络层数据包。网络层有多种协议,最常见的是 IP 协议,可 IP 协议也有多个版本,我们怎么知道位于以太网数据包负载中的是 IP 协议的数据包呢?如果是,是 IP v4 的还是 IP v6 的;如果不是,那是什么协议的呢?
答案就是分析以太网数据包的首部。
如图所以,以太网数据包的首部中有一个 EtherType
字段,它决定了上层网络层的协议。libpnet
提供了 get_ethertype()
这样一个方法用于获取网络层的协议③。接下来又是 match
表达式。
下面以最常见的 IP v4 数据包为例④,看看如何将数据包交由网络层处理。
将数据包交由网络层处理
fn ipv4_handler(ethernet: &EthernetPacket) {
if let Some(packet) = Ipv4Packet::new(ethernet.payload()) { // ①
match packet.get_next_level_protocol() { // ②
IpNextHeaderProtocols::Tcp => {
tcp_handler(&packet); // ③
},
IpNextHeaderProtocols::Udp => {
udp_handler(&packet);
},
_ => {
println!("Not a tcp or a udp packet");
}
}
}
}
我们刚刚提到,去掉了以太网数据包的首部后,剩余的负载部分就是一个网络层的数据包。所以首先调用 payload()
函数来获取以太网数据包的负载,然后通过 Ipv4Packet::new()
函数将这个负载转化成结构化的 IP v4 的数据包①。
网络层的上一层是传输层,传输层同样有 TCP、UDP 等多种协议。那相似的问题又来了,如何知道传输层使用的是哪种协议呢?答案还是分析数据包的首部,以 IP v4 的数据包为例,只不过这次是在 IP v4 的数据包 packet
上通过 get_next_level_protocol()
函数来获取其中封装了哪一种传输层协议的数据包。
接下来以 TCP 数据包为例③,看看如何将数据包交由传输层处理。
将数据包交由传输层处理
终于到了传输层。类似以太网数据包的负载就是网络层的数据包,去掉了网络层的首部后,剩余的负载(payload)就是一个传输层的数据包。
下面以处理传输层 TCP 的数据包(也叫数据段〔segment〕)为例来梳理代码。处理 UDP 的数据包(也叫数据报〔datagram〕)的逻辑与此大同小异。
fn tcp_handler(packet: &dyn GettableEndPoints) {
let tcp = TcpPacket::new(packet.get_payload());
if let Some(tcp) = tcp {
print_packet_info(packet, &tcp, "TCP"); // ①
}
}
fn print_packet_info(l3: &dyn GettableEndPoints, l4: &dyn GettableEndPoints, proto: &str) {
println!("Captured a {} packet from {}|{} to {}|{}\n",
proto,
l3.get_source(),
l4.get_source(),
l3.get_destination(),
l4.get_destination()
);
let payload = l4.get_payload();
let len = payload.len();
for i in 0..len {
print!("{:<02X} ", payload[i]);
// ...
}
// ...
}
我们在这里调用了 print_packet_info()
来输出网络层和传输层的数据包的内容,包括源和目标地址,封装在传输层负载中的应用层数据的内容等。
从 l3
和 l4
这两个参数名称可以看出,这个函数要打印第 3 层(网络层)和第 4 层(传输层)的数据包的信息。在文章一开头回顾网络基础知识时曾提到,无论是哪一层的数据包都有地址字段,这是协议间的共性,于是我们通过 GettableEndPoints
这个 trait 来体现这种共性,
pub trait GettableEndPoints {
fn get_source(&self) -> String;
fn get_destination(&self) -> String;
fn get_payload(&self) -> &[u8];
}
这可以类比于在其他语言中定义了一个 interface GettableEndPoints
。
定义好 trait 后,还要为代表各协议的数据包的 struct
实现这个 trait。
impl<'a> GettableEndPoints for Ipv4Packet<'a> {
fn get_source(&self) -> String {
self.get_source().to_string()
}
// ...
}
impl<'a> GettableEndPoints for Ipv6Packet<'a> {
// ...
}
impl<'a> GettableEndPoints for TcpPacket<'a> {
// ...
}
impl<'a> GettableEndPoints for UdpPacket<'a> {
// ...
}
运行结果
好了,所有代码都梳理完了。赶紧来运行一下这个“简陋”的 tcpdump
的轮子,
$ sudo $(which cargo) run -- enp0s3
这里需要用 sudo
提升权限。若打开另一个终端执行curl www.baidu.com
,就能看到类似下面的输出!
Captured a TCP packet from 10.0.2.15|57108 to 182.61.200.7|80
============================================================
Captured a TCP packet from 182.61.200.7|80 to 10.0.2.15|57108
============================================================
Captured a TCP packet from 10.0.2.15|57108 to 182.61.200.7|80
============================================================
Captured a TCP packet from 10.0.2.15|57108 to 182.61.200.7|80
47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A 48 6F 73 74 | GET...HTTP......Host
3A 20 77 77 77 2E 62 61 69 64 75 2E 63 6F 6D 0D 0A 55 73 65 | ..www.baidu.com..Use
72 2D 41 67 65 6E 74 3A 20 63 75 72 6C 2F 37 2E 35 38 2E 30 | r.Agent..curl.......
0D 0A 41 63 63 65 70 74 3A 20 2A 2F 2A 0D 0A 0D 0A | ..Accept.........
============================================================
可以看到源 IP 地址,目标 IP 地址,源和目标端口号,以及 HTTP 的请求,还是挺酷的。
怎么样,借助 libpnet
来处理网络中的数据包还是挺简单的吧,核心逻辑只有一个,分离首部和负载,然后根据首部选择下一层的协议,将负载交给下一层的协议处理。
现在有没有觉得枯燥的网络协议变得有点意思了呢?
Rust 源代码
Cargo.toml
[package]
name = "packet-capture"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pnet = "0.34.0"
src/main.rs
use pnet::datalink;
use pnet::datalink::Channel::Ethernet;
use pnet::packet::ethernet::{EtherTypes, EthernetPacket};
use pnet::packet::ipv4::Ipv4Packet;
use pnet::packet::ipv6::Ipv6Packet;
use pnet::packet::tcp::TcpPacket;
use pnet::packet::udp::UdpPacket;
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::Packet;
use std::env;
mod packets;
use packets::GettableEndPoints;
const WIDTH:usize = 20;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Please specify target interface name");
std::process::exit(1);
}
let interface_name = &args[1];
let interfaces = datalink::interfaces();
let interface = interfaces
.into_iter()
.filter(|iface| iface.name == *interface_name)
.next()
.expect("Failed to get interface");
let (_tx, mut rx) = match datalink::channel(&interface, Default::default()) {
Ok(Ethernet(tx, rx)) => (tx, rx),
// Ok(_) => panic!("Unhandled channel type"),
Err(e) => {
panic!("Failed to create datalink channel {}", e)
}
};
loop {
match rx.next() {
Ok(frame) => {
let frame = EthernetPacket::new(frame).unwrap();
match frame.get_ethertype() {
EtherTypes::Ipv4 => {
ipv4_handler(&frame);
},
EtherTypes::Ipv6 => {
ipv6_handler(&frame);
}
_ => {
println!("Not a ipv4 or ipv6");
}
}
},
Err(e) => {
panic!("Failed to read: {}", e);
}
}
}
}
#[test]
fn test_get_all_interfaces() {
let interfaces = datalink::interfaces();
let interface_name = "en0";
let interface = interfaces
.into_iter()
.filter(|iface| iface.name == interface_name)
.next()
.expect("Failed to get interface");
println!("{:?}", interface);
}
fn print_packet_info(l3: &GettableEndPoints, l4: &GettableEndPoints, proto: &str) {
println!("Captured a {} packet from {}|{} to {}|{}\n",
proto,
l3.get_source(),
l4.get_source(),
l3.get_destination(),
l4.get_destination()
);
let payload = l4.get_payload();
let len = payload.len();
for i in 0..len {
print!("{:<02X} ", payload[i]);
if i%WIDTH == WIDTH-1 || i == len-1 {
for _j in 0..WIDTH-1-(i % (WIDTH)) {
print!(" ");
}
print!("| ");
for j in i-i%WIDTH..i+1 {
if payload[j].is_ascii_alphabetic() {
print!("{}", payload[j] as char);
} else {
print!(".");
}
}
print!("\n");
}
}
println!("{}", "=".repeat(WIDTH * 3));
print!("\n");
}
fn ipv4_handler(ethernet: &EthernetPacket) {
if let Some(packet) = Ipv4Packet::new(ethernet.payload()){
match packet.get_next_level_protocol() {
IpNextHeaderProtocols::Tcp => {
tcp_handler(&packet);
},
IpNextHeaderProtocols::Udp => {
udp_handler(&packet);
},
_ => {
println!("Not a tcp or a udp packet");
}
}
}
}
fn ipv6_handler(ethernet: &EthernetPacket) {
if let Some(packet) = Ipv6Packet::new(ethernet.payload()){
match packet.get_next_header() {
IpNextHeaderProtocols::Tcp => {
tcp_handler(&packet);
},
IpNextHeaderProtocols::Udp => {
udp_handler(&packet);
},
_ => {
println!("Not a tcp or a udp packet");
}
}
}
}
fn tcp_handler(packet: &dyn GettableEndPoints) {
let tcp = TcpPacket::new(packet.get_payload());
if let Some(tcp) = tcp {
print_packet_info(packet, &tcp, "TCP");
}
}
fn udp_handler(packet: &dyn GettableEndPoints) {
let udp = UdpPacket::new(packet.get_payload());
if let Some(udp) = udp {
print_packet_info(packet, &udp, "UDP");
}
}
src/packets.rs
use pnet::packet::ipv4::Ipv4Packet;
use pnet::packet::ipv6::Ipv6Packet;
use pnet::packet::tcp::TcpPacket;
use pnet::packet::udp::UdpPacket;
use pnet::packet::Packet;
pub trait GettableEndPoints {
fn get_source(&self) -> String;
fn get_destination(&self) -> String;
fn get_payload(&self) -> &[u8];
}
impl<'a> GettableEndPoints for Ipv4Packet<'a> {
fn get_source(&self) -> String {
self.get_source().to_string()
}
fn get_destination(&self) -> String {
self.get_destination().to_string()
}
fn get_payload(&self) -> &[u8] {
self.payload()
}
}
impl<'a> GettableEndPoints for Ipv6Packet<'a> {
fn get_source(&self) -> String {
self.get_source().to_string()
}
fn get_destination(&self) -> String {
self.get_destination().to_string()
}
fn get_payload(&self) -> &[u8] {
self.payload()
}
}
impl<'a> GettableEndPoints for TcpPacket<'a> {
fn get_source(&self) -> String {
self.get_source().to_string()
}
fn get_destination(&self) -> String {
self.get_destination().to_string()
}
fn get_payload(&self) -> &[u8] {
self.payload()
}
}
impl<'a> GettableEndPoints for UdpPacket<'a> {
fn get_source(&self) -> String {
self.get_source().to_string()
}
fn get_destination(&self) -> String {
self.get_destination().to_string()
}
fn get_payload(&self) -> &[u8] {
self.payload()
}
}
更多推荐
所有评论(0)