本文主要讲解如何在鸿蒙上实现 traceroute 能力。traceroute 是一种常见的网络分析工具,能够获取从本机到目的主机之间的所有路由器的信息。
一般情况下 traceroute 有多种实现方案:
-
基于UDP的Traceroute:
原理:通过发送具有逐渐增加的TTL(Time-To-Live)值的UDP数据包来实现。当数据包的TTL耗尽时,路由器会返回一个ICMP "Time Exceeded"消息,traceroute程序通过这些消息确定路径上的路由器。
优点:简单,易于实现。
缺点:有些网络可能会过滤UDP数据包,导致traceroute失败。 -
基于ICMP的Traceroute:
原理:类似于UDP traceroute,但使用ICMP Echo Request数据包(即ping命令使用的数据包)。 优点:在某些网络环境下比UDP更可靠,因为ICMP通常不会被过滤。
缺点:某些网络可能会限制ICMP流量,导致traceroute不完整或失败。 -
基于TCP的Traceroute:
原理:发送TCP SYN数据包,并逐渐增加TTL值。当TTL耗尽时,路由器返回ICMP "Time Exceeded"消息。当数据包到达目标主机时,如果目标端口未打开,目标主机会返回一个TCP RST(Reset)数据包,表示连接被拒绝。
优点:可以穿透一些只允许TCP流量的网络。
缺点:需要处理TCP连接的建立和关闭,实现相对复杂。
从网络和社区的一些搜索结果来看,使用基于 ICMP 包实现的更为常见,因此本文将使用该方案实现。
分析
从上面的描述中我们可以看到,基于 ICMP 实现 traceroute 与实现 ping 基本上没有太大的差异,区别是我们需要从 TTL=1 开始遍历发送和接受包,当接收到 EchoReply 的包的时候就认为达到目的主机而结束,从而实现 traceroute 的能力。
那么在实现上面我们完全可以参考之前的部分实现,写出类似于下面的代码:
// ... 省略部分之前实现
match socket.recv_from(recv_buffer.as_mut_slice()) {
Ok((size, src_addr)) => {
if let Some(ipv4_packet) =
Ipv4Packet::new(recv_buffer.as_mut_slice_initialized(size))
{
let icmp_payload = ipv4_packet.payload();
if let Some(icmp) = IcmpPacket::new(icmp_payload) {
match icmp.get_icmp_type() {
// 处理最终到达目的主机的逻辑
IcmpTypes::EchoReply => {
finished = true;
}
// 处理每一跳中的响应
IcmpTypes::TimeExceeded => {
let rtt = start_time.elapsed().as_secs_f64() * 1000.0;
res.rtt.push(rtt);
res.addr = Some(
src_addr.as_socket_ipv4().unwrap().ip().to_string(),
);
continue;
}
_ => {}
}
}
}
}
// 错误处理
_ => {}
}
// ... 省略部分后面的实现
如果你恰好使用的 Mac 设备进行编写和测试,又恰好只在 mac 上面进行了运行,你会发现所有的运行都是符合预期的。
但是你如果将相同的代码放在了鸿蒙上面实现,然后再运行发现最后居然没有任何结果?如果你捕获了错误然后会发现系统返回了一下的错误码:
实际上这个是因为不同系统网络协议栈的处理有所差异,在 Linux 系统上面我们使用 ICMP 实现时当 TTL 耗尽时,网络协议栈的处理会将响应的数据包作为系统错误进行处理,而非正常的响应包。而 Mac/iOS 的系统则会当成时正常的响应包进行处理,安卓也类似(没有实测过,从网络上的一些实现代码来看是这样的)。
鸿蒙与 Linux 行为保持一致,因此我们这里不再能够继续使用以上的代码实现,而是需要从 Linux 系统的错误处理队列中获取中间结果进行处理。
而 socket2 对于当前这种编程模式的处理并不好,因此我们需要使用更加底层和原始的 API 来实现,这里我们使用 nix 实现。
- nix 旨在为我们提供 *nix 系统 API 的 safe 版本的 API。
- 与 Nix/NixOS 区别开来。
现有的版本在鸿蒙系统构建会存在一些问题,新的适配代码合入之后还未发布新版本,因此我们需要先使用 git 协议来安装 nix 如下:
// cargo.toml
[dependencies]
nix = { git = "https://github.com/nix-rust/nix.git", branch = "master" }
实现
实现上面就与之前的 Ping 实现有所差异了,我们现在的过程如下所示:
- 创建 socket
- 发送 ICMP 包
- 处理 ICMP 回包(包括基于错误队列实现的)
创建 socket
由于现在使用 nix 进行编程,因此我们的创建过程有所不同:
use nix::sys::socket::socket;
// 创建 DGRAM 的socket
let socket_instance = socket(
AddressFamily::Inet,
SockType::Datagram,
SockFlag::empty(),
SockProtocol::Icmp,
)
.map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to create socket: {}", e),
)
})?;
创建完成之后,相较于之前的代码我们还需要额外设置开启接受处理信息。
setsockopt(&socket_instance, sockopt::Ipv4RecvErr, &true).map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to set EQUEUE: {}", e),
)
})?;
这样我们才能够正常的从错误队列中获取到响应的数据包。
发送 ICMP 包
ICMP 包的构建与之前没有任何差异,可以直接复用。差异点在于发送的时候,需要使用 nix 提供的 sendto 方法进行:
sendto(
socket_instance.as_raw_fd(),
icmp_packet.packet(),
&dest,
MsgFlags::empty(),
)
.map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to send packet: {}", e),
)
})?;
处理回包
现在我们处理回包就需要先尝试从错误队列中获取,当获取不到的时候就尝试去获取正常的回包。最终当获取的 ICMP 正常的响应包的时候就结束本次调用。
let mut buffer = vec![0u8; 1024];
let mut iov = [IoSliceMut::new(&mut buffer)];
let mut cmsg_buffer = vec![0u8; 64];
// 使用 recvmsg 尝试从错误消息队列中获取数据
match recvmsg::<SockaddrIn>(
socket_instance.as_raw_fd(),
&mut iov,
Some(&mut cmsg_buffer),
MsgFlags::MSG_ERRQUEUE,
) {
Ok(msg) => match msg.cmsgs() {
Ok(cmsgs) => {
for cmsg in cmsgs {
// 解析数据类型
if let ControlMessageOwned::Ipv4RecvErr(err, addr) = cmsg {
match IcmpType::new(err.ee_type) {
// 错误类型为超时
IcmpTypes::TimeExceeded => {
}
_ => {}
}
}
}
}
Err(_) => {}
},
Err(_) => {
// 从错误消息队列中获取不到时,就尝试接收正常的数据
match recvfrom::<SockaddrIn>(socket_instance.as_raw_fd(), &mut buffer) {
Ok((size, src_addr)) => {
if let Some(ipv4_packet) = Ipv4Packet::new(&buffer[..size]) {
let icmp_payload = ipv4_packet.payload();
if let Some(icmp) = IcmpPacket::new(icmp_payload) {
match icmp.get_icmp_type() {
//
IcmpTypes::EchoReply => {
finished = true;
break 'finish;
}
IcmpTypes::TimeExceeded => {
}
_ => {
// ...
}
}
}
}
}
Err(_) => {
return Err(Error::from_reason("Failed to receive packet"));
}
}
}
}
最后我们只需要像 ping 一样使用 Task 将其异步化即可,这样我们就完成了整个 traceroute 的实现。本文所有代码最终实现在 traceroute
测试
测试方案与 ping 的实现类似,代码如下调用即可:
import { traceRoute } from '@ohos-rs/traceroute'
const ret = await traceRoute("110.242.68.66");
console.log(`${ret}`)
以上就是所有内容,希望对你有所帮助~