DHCP 协议分析及实现 - Ⅱ
前一篇文章介绍了 DHCP
协议定义以及相关信令之间的流转, 本文主要通过 Rust
从头实现一个 DHCP
协议客户端.
环境准备⌗
首先需要安装 Rust
, 目前 Rust
官方主要提供 Rustup
这一工具管理 Rust
各版本. 如果是 Windows
可直接下载 Rustup-init.exe
运行即可, 如果是 Linux
或 macOS
则可在 Shell
中运行一下命令快速安装:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Rustup
在安装 Rust
同时也会同时安装 Cargo
这一构建和包管理工具, 相对于 C/C++
而言极大地减轻依赖和构建管理的心智负担.
在安装完 Rust
和 Cargo
后由于涉及到网络报文, 因此还建议安装 Wireshark
(Linux
环境下使用 tcpdump
亦可)以便后续对报文进行分析.
IDE
可以选择 VS Code
或 JetBrains
新发布的 RustRover
, 如果选择 VS Code
的话为了进行调试还需同时安装 CodeLLDB
插件.
开发⌗
我们首先通过 cargo new inode-rs
新建一个项目, 其中 inode-rs
是项目名称. 为便于项目管理 Cargo
为我们提供了 workspace
的概念, 此处我们在 inode-rs
下新建 packet
和 cli
两个子项目. 此时项目结构看起来像这样:
.
├── Cargo.toml
├── cli
└── packet
为了后续开发我们还需要至少添加 bytes
, pnet
和 tokio
依赖. 其中 bytes
主要用于二进制报文的读写, pnet
可用于获取本机网卡信息, tokio
是一个异步运行时库, 我们使用其提供的 async/await
运行环境简化异步和多线程操作.
我们在 packet
子项目下依次定义子 mod
为 udp
并在 udp
模块下定义 dhcp
模块, 最终文件结构为 packet/udp/dhcp.rs
. 在 dhcp.rs
内定义具体的数据结构.
依据 RFC 2132
中关于 options
章节我们首先定义一个 DHCP Message Type
消息类型的枚举:
#[derive(Debug, Clone, Copy)]
pub enum DHCPMessageType {
DHCPDISCOVER = 0x01,
DHCPOFFER = 0x02,
DHCPREQUEST = 0x03,
DHCPDECLINE = 0x04,
DHCPACK = 0x05,
DHCPNAK = 0x06,
DHCPRELEASE = 0x07,
DHCPINFORM = 0x08,
}
同样, 依据 RFC 2131
中关于报文格式章节我们首先定义 op
枚举:
#[derive(Debug, Clone, Copy, Default)]
pub enum DHCPOP {
#[default]
BOOTREQUEST = 0x01,
BOOTREPLY = 0x02,
}
此处 DHCPOP
枚举由于在网络传输过程中需要序列化为 u8
, 在客户端接收到报文后再将对应的 u8
数值反序列化为 DHCPOP
枚举, 因此我们需要实现 Into
和 From
两个 Trait
:
impl Into<DHCPOP> for u8 {
fn into(self) -> DHCPOP {
match self {
0x01 => DHCPOP::BOOTREQUEST,
0x02 => DHCPOP::BOOTREPLY,
_ => DHCPOP::BOOTREQUEST,
}
}
}
impl From<DHCPOP> for u8 {
fn from(value: DHCPOP) -> Self {
value as u8
}
}
在实现 Into
Trait
时我们使用了 match
模式匹配
. 当前 Rust
要求针对 match
模式匹配必须是 穷举
的 - 即必须保护所有可能情况, 因此在对 u8
进行匹配时由于我们只关注 0x01
和 0x02
两种场景, 针对其他情况统一使用 _
进行匹配并返回一个默认的 DHCPOP::BOOTREQUEST
.
其他一些类似的枚举我们不在反复展示.
在定义了必要的一些 enum
后我们再定义一个 DHCPDiscover
结构体:
#[derive(Debug, Clone)]
pub struct DHCPDiscover {
pub op: DHCPOP,
pub htype: DHCPHType,
pub hlen: DHCPHLen,
pub hops: u8,
pub xid: u32,
pub secs: u16,
pub flags: u16,
pub ciaddr: u32,
pub yiaddr: u32,
pub siaddr: u32,
pub giaddr: u32,
pub chaddr: [u8; 16],
pub sname: [u8; 64],
pub file: [u8; 128],
pub options: Vec<u8>,
}
同时为 DHCPDiscover
实现 Default
Trait
:
impl Default for DHCPDiscover {
fn default() -> Self {
DHCPDiscover {
op: DHCPOP::BOOTREQUEST,
htype: DHCPHType::ETHERNET,
hlen: DHCPHLen::ETHERNET,
hops: 0x00,
xid: 0x00000000,
secs: 0x0000,
flags: 0x0000,
ciaddr: 0x00000000,
yiaddr: 0x00000000,
siaddr: 0x00000000,
giaddr: 0x00000000,
chaddr: [0x00; 16],
sname: [0; 64],
file: [0; 128],
options: vec![99, 130, 83, 99],
}
}
}
在为 DHCPDiscover
实现 Default
Trait
时我们为字段 options
预先存入了 magic cookie
即 [99, 130, 83, 99]
(10
进制表示).
除了实现 Default
Trait
我们同时还定义一个 fn with_mac(mac: &str)
的关联函数, 其作用是根据 MAC
地址初始化 DHCPDiscover
对象; 同时为了支持动态设置 options
字段, 我们为 DHCPDiscover
添加一个 fn insert_options(&mut self, options: Vec<DHCPOPTIONS>)
方法, 该方法关联到 DHCPDiscover
的实例并可变(mut
)借用其实例自身以更新相关属性:
impl DHCPDiscover {
pub fn with_mac(mac: &str) -> Self {
let mut mac_addr: Vec<u8> = if mac.contains("-") {
str::split(mac, "-")
.map(|u| u8::from_str_radix(u, 16).unwrap())
.collect()
} else {
str::split(mac, ":")
.map(|u| u8::from_str_radix(u, 16).unwrap())
.collect()
};
mac_addr.resize(16, 0);
let mut discover = DHCPDiscover::default();
discover.chaddr = mac_addr.try_into().unwrap();
discover
}
pub fn insert_options(&mut self, options: Vec<DHCPOPTIONS>) {
for ele in options.iter() {
self.options.push(ele.tp as u8);
self.options.push(ele.len);
self.options.extend(ele.va.clone());
}
self.options.push(0xff);
}
}
以上除了定义 enum
和 struct
外, 我们再定义一个 DHCPBytes
trait
:
pub trait DHCPBytes {
fn to_bytes(&self) -> BytesMut;
}
并要求所有实现该 trait
的元素都必须实现 to_bytes
方法以便将对应元素转换为二进制 bytes
从而在 UDP
报文中进行传输. 其中 BytesMut
由 bytes
这个 crate
提供, 而 bytes
库主要在于提供高效的针对字节缓冲的操作. Bytes
作为一个高效的存储与操作连续内存空间的容器, 其设计的主要目标在于网络编码以及其他一些应用场景.
impl DHCPBytes for DHCPDiscover {
fn to_bytes(&self) -> BytesMut {
let mut discover_bytes = BytesMut::new();
discover_bytes.put_u8(self.op as u8);
discover_bytes.put_u8(self.htype as u8);
discover_bytes.put_u8(self.hlen as u8);
discover_bytes.put_u8(self.hops);
discover_bytes.put_u32(self.xid);
discover_bytes.put_u16(self.secs);
discover_bytes.put_u16(self.flags);
discover_bytes.put_u32(self.ciaddr);
discover_bytes.put_u32(self.yiaddr);
discover_bytes.put_u32(self.siaddr);
discover_bytes.put_u32(self.giaddr);
for ele in self.chaddr.iter() {
discover_bytes.put_u8(*ele);
}
for ele in self.sname.iter() {
discover_bytes.put_u8(*ele);
}
for ele in self.file.iter() {
discover_bytes.put_u8(*ele);
}
for ele in self.options.iter() {
discover_bytes.put_u8(*ele);
}
discover_bytes
}
}
以上 DHCPDiscover
实现的 DHCPBytes
主要是按 RFC 2131
内定义的报文格式将对应字段转换为 Bytes
. 另外像其中 xid
, secs
和 ciaddr
等字段由于在标准中分别采用 4
, 2
和 4
字节, 分别对应了 32
, 16
和 32
BIT
, 因此为简化此处直接按对应 BIT
定义了无符整型数据.
在完成以上相关 enum
和 struct
定义的最后我们继续来实现 main()
方法:
#[tokio::main]
async fn main() -> Result<()> {
let socket = UdpSocket::bind("0.0.0.0:68").await?;
let broadcast = socket.set_broadcast(true);
if let Ok(r) = broadcast {
println!("开启广播模式");
}
let mut buf = [0; 1024];
let mac = "AC-48-78-43-C7-B8";
let mut v = vec![DHCPHType::ETHERNET as u8];
let a: Vec<u8> = str::split(mac, "-")
.map(|u| u8::from_str_radix(u, 16).unwrap())
.collect();
v.extend_from_slice(&a);
let parameter_request_list: Vec<u8> = vec![
DHCPOPTION::SubnetMask as u8,
DHCPOPTION::RouterOption as u8,
DHCPOPTION::DomainNameServerOption as u8,
DHCPOPTION::NetworkTimeProtocolServersOption as u8,
];
let mut discovery_message = DHCPDiscover::with_mac(mac);
discovery_message.insert_options(vec![
DHCPOPTIONS {
tp: DHCPOPTION::DHCPMessageType,
len: 1,
va: vec![DHCPMessageType::DHCPDISCOVER as u8],
},
DHCPOPTIONS {
tp: DHCPOPTION::ClientIdentifier,
len: v.len() as u8,
va: v.clone(),
},
DHCPOPTIONS {
tp: DHCPOPTION::ParameterRequestList,
len: parameter_request_list.len() as u8,
va: parameter_request_list.clone(),
},
]);
let tmp_bytes = discovery_message.to_bytes();
let broadcast_result = socket.send_to(&tmp_bytes, "255.255.255.255:67").await;
match broadcast_result {
Ok(n) => println!("发送数据包: {}", n),
Err(e) => println!("错误: {:?}", e),
}
Ok(())
}
首先通过 UdpSocket::bind<A: ToSocketAddrs>(addr: A)
函数绑定到本机对应端口, 值得注意的是此处在调用 bind
函数时后跟了一个 await?
关键字, 这里的 async/await
是 Rust
提供的一个内置使得编写异步代码像同步代码一样的工具, async
会将相应的代码块转换为一种实现了 Future
Trait
的状态机, 当调用 await
时具体的 Runtime
会挂起当前运行环境直至其返回结果 Future
准备好. Rust
并未在标准中提供 async/await
运行时, 因此我们需要通过 crate
添加对应依赖, 在此处我们使用 tokio
.
以上我们同时还通过 socket.set_broadcast(true)
显示地将 UDPSocket
实例设置为广播模式, 在前一篇文章(DHCP 协议分析及实现 - Ⅰ)内我们也曾介绍过由于在 DHCP
协议通信之初 客户端
尚无具体 IP
地址切也不知道对应 服务端
所在网络中位置, 而这这好利用 UDP
的广播特性使得客户端向局域网广播 DHCPDISCOVER
报文. 对应地通过向 255.255.255.255:67
发送广播报文.
至此我们完成了初步的 DHCP
协议中 DHCPDISCOVER
协议的转换与广播, 后续还有包括 DHCPOFFER
, DHCPREQUEST
和 DHCPACK
等消息的转换, 发送与解析我们不再赘述, 感兴趣的可以参考 liloew/inode-rs 完整实现.