鸿蒙网络协议编程 - traceroute

345 阅读5分钟

本文主要讲解如何在鸿蒙上实现 traceroute 能力。traceroute 是一种常见的网络分析工具,能够获取从本机到目的主机之间的所有路由器的信息。

一般情况下 traceroute 有多种实现方案:

  1. 基于UDP的Traceroute:
    原理:通过发送具有逐渐增加的TTL(Time-To-Live)值的UDP数据包来实现。当数据包的TTL耗尽时,路由器会返回一个ICMP "Time Exceeded"消息,traceroute程序通过这些消息确定路径上的路由器。
    优点:简单,易于实现。
    缺点:有些网络可能会过滤UDP数据包,导致traceroute失败。

  2. 基于ICMP的Traceroute:
    原理:类似于UDP traceroute,但使用ICMP Echo Request数据包(即ping命令使用的数据包)。 优点:在某些网络环境下比UDP更可靠,因为ICMP通常不会被过滤。
    缺点:某些网络可能会限制ICMP流量,导致traceroute不完整或失败。

  3. 基于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 上面进行了运行,你会发现所有的运行都是符合预期的。

但是你如果将相同的代码放在了鸿蒙上面实现,然后再运行发现最后居然没有任何结果?如果你捕获了错误然后会发现系统返回了一下的错误码:

17389976781913.jpg

实际上这个是因为不同系统网络协议栈的处理有所差异,在 Linux 系统上面我们使用 ICMP 实现时当 TTL 耗尽时,网络协议栈的处理会将响应的数据包作为系统错误进行处理,而非正常的响应包。而 Mac/iOS 的系统则会当成时正常的响应包进行处理,安卓也类似(没有实测过,从网络上的一些实现代码来看是这样的)。

鸿蒙与 Linux 行为保持一致,因此我们这里不再能够继续使用以上的代码实现,而是需要从 Linux 系统的错误处理队列中获取中间结果进行处理。

而 socket2 对于当前这种编程模式的处理并不好,因此我们需要使用更加底层和原始的 API 来实现,这里我们使用 nix 实现。

  1. nix 旨在为我们提供 *nix 系统 API 的 safe 版本的 API。
  2. 与 Nix/NixOS 区别开来。

现有的版本在鸿蒙系统构建会存在一些问题,新的适配代码合入之后还未发布新版本,因此我们需要先使用 git 协议来安装 nix 如下:

// cargo.toml

[dependencies]
nix = { git = "https://github.com/nix-rust/nix.git", branch = "master" }

实现

实现上面就与之前的 Ping 实现有所差异了,我们现在的过程如下所示:

  1. 创建 socket
  2. 发送 ICMP 包
  3. 处理 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}`)

17390001638847.jpg

以上就是所有内容,希望对你有所帮助~