【开源软件移植】EasyTier 适配鸿蒙 PC 全流程实战 —— 从 ArkTS 快速落地到 Rust HAR
【开源软件移植】EasyTier 适配鸿蒙 PC 全流程实战 —— 从 ArkTS 快速落地到 Rust HAR
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
场景限定:鸿蒙 PC,不展开手机、平板等其他设备类型。
构建环境:Arch Linux + HarmonyOS command-line-tools + ohrs。
移植产物仓库:https://atomgit.com/FrankHan2004/EasyTier
鸿蒙App仓库:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_easytier
写在前面
把 EasyTier 移植到鸿蒙 PC,第一阶段的目标很朴素:先跑起来。
EasyTier 上游核心能力主要由 Rust 实现,但鸿蒙 PC 适配并不是一开始就把所有编排逻辑全部放进 Rust。最早为了快速落地,ArkTS 侧承担了大量工作,包括实例管理、TUN 编排、socket 通信、路由整理、配置解析、运行状态维护等。这样做很适合第一轮验证,开发快、反馈快,能尽快判断 EasyTier 在鸿蒙 PC 上能不能跑通。
后面功能越来越多,ArkTS 侧的问题就明显了:内存占用变高、状态同步变复杂、UI 代码和内核编排逻辑耦合越来越重。于是后续适配开始逐步把这些能力重新下沉到 Rust,只把必要的 UI 展示和系统侧能力衔接留在 ArkTS。
这次移植最终形成的路线是:
ArkTS 快速落地验证
↓
Rust NAPI 最小导出
↓
ohrs 生成 HarmonyOS HAR
↓
配置模型从 TOML 文本过渡到 NetworkConfig JSON
↓
配置管理、运行态聚合、TUN 请求、路由整理下沉到 Rust
↓
鸿蒙 PC 工程通过 ohpm install 接入验证
最终产物已经在本地成功生成:
dist/arm64-v8a/libeasytier_ohrs.sopackage.har

ohrs build成功日志

package.har产物目录截图
一、整体架构与思路
EasyTier 适配鸿蒙 PC 后,整体结构可以分成三层:
┌──────────────────────────────────────────────┐
│ 鸿蒙 PC 应用层 │
│ ArkTS 页面 / 系统权限 / TUN fd / UI 展示 │
└───────────────────────┬──────────────────────┘
│ NAPI 调用
┌───────────────────────▼──────────────────────┐
│ easytier-ohrs HAR 包 │
│ index.ets → libeasytier_ohrs.so │
│ 配置 API / 运行态 API / TUN 请求 / socket 推送 │
└───────────────────────┬──────────────────────┘
│ Rust 调用
┌───────────────────────▼──────────────────────┐
│ EasyTier Rust Core │
│ 实例启动 / 网络配置 / 路由信息 / peer 状态 │
└──────────────────────────────────────────────┘
这个结构里,ArkTS 不再负责所有内核状态编排,而是主要负责页面展示、用户交互,以及 HarmonyOS 系统侧必须由应用层处理的能力。Rust 侧负责 EasyTier 自身更熟悉的逻辑,例如配置校验、实例管理、运行态聚合、路由整理和 TUN 请求生成。
这样的分层有两个好处:
- ArkTS 侧更轻,减少内存和状态同步压力。
- EasyTier 核心逻辑更集中,后续跟随上游演进时更容易维护。
二、环境部署:Rust、ohrs 和 command-line-tools
后面的 Native 导出、HAR 打包和真机验证都依赖基础环境,所以环境部署需要放在移植流程前面。
2.1 参考资料
Rust 基础环境建议直接使用官方安装方式,不建议把第三方博客里的安装命令作为主要依据:
- Rust 官方安装页:https://rust-lang.org/zh-CN/tools/install/
- ohos-rs 快速开始文档:https://ohos.rs/docs/basic/quick-start.html
- HarmonyOS command-line-tools 下载页:https://developer.huawei.com/consumer/cn/download/command-line-tools-for-hmos
2.2 安装 Rust 与 OHOS target
Rust 安装可以使用官方提供的 rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装完成后确认版本:
rustc --version
cargo --version
rustup --version
HarmonyOS Rust 构建需要额外安装 OHOS target。鸿蒙 PC 侧 HAR 构建使用的是 aarch64-unknown-linux-ohos:
rustup target add aarch64-unknown-linux-ohos
2.3 ohos-rs 和 ohrs 的作用
这里用到的 ohos-rs,可以理解为一套面向 HarmonyOS 原生模块开发的 Rust 工具链和生态封装。它解决的不是 EasyTier 自身的业务逻辑问题,而是 Rust 代码如何以 HarmonyOS 可以识别的形式完成编译、导出和打包。
在这次移植里,ohos-rs 主要承担三类工作:
- 通过
napi-ohos、napi-derive-ohos把 Rust 函数导出成 ArkTS 可以调用的 NAPI 接口。 - 通过
napi_build_ohos::setup()生成 HarmonyOS 原生模块构建所需的信息。 - 通过
ohrs命令完成 Rust Native 库编译和 HAR 产物生成。
简单说,EasyTier 的核心逻辑仍然来自 Rust 本体,ohos-rs 负责把这些 Rust 能力包装成 HarmonyOS 工程能加载、能调用、能安装的组件。
ohrs 是 ohos-rs 提供的命令行工具,用来简化 Rust 原生模块在 HarmonyOS 上的初始化、编译和 HAR 产物生成。这里主要使用它的三个命令:
ohrs doctor:检查 HarmonyOS Rust 构建环境。ohrs build:编译 Rust Native 库。ohrs artifact:生成可安装的package.har。
ohrs 可以通过 cargo 安装:
cargo install ohrs
安装完成后确认版本:
ohrs --version
2.4 准备 command-line-tools 和路径占位
HarmonyOS command-line-tools 需要从华为开发者官网下载安装包,并解压到需要存放 SDK 和工具链的目录。这里用 <command-line-tools-root> 表示这个解压后的实际目录,例如:
<command-line-tools-root>/bin
<command-line-tools-root>/sdk/default/openharmony
其中 <command-line-tools-root>/sdk/default/openharmony 是后续用于 OHOS_NDK_HOME 的 HarmonyOS SDK/NDK 根目录。
这里用 <repo-root> 表示 EasyTier 源码的实际存储路径,也就是包含 easytier/、easytier-contrib/、Cargo.toml 的 EasyTier 项目根目录。
本次实测环境中的路径示例为:
<command-line-tools-root> = /home/frankhan/Documents/UPG/et/command-line-tools
<repo-root> = /home/frankhan/Documents/UPG/et/EasyTier
2.5 实测环境与环境变量
实测构建环境如下:
| 项目 | 实际值 |
|---|---|
| 宿主机 | Arch Linux |
| Rust | rustc 1.94.0 |
| cargo | 1.94.0 |
| ohrs | 1.3.1 |
| ohpm | 6.1.1.830 |
| command-line-tools | <command-line-tools-root> |
| HarmonyOS SDK 根目录 | <command-line-tools-root>/sdk/default/openharmony |
| 目标架构 | arm64-v8a |
| 目标场景 | 鸿蒙 PC |
基础环境准备完成后,先导出 SDK 路径:
export OHOS_NDK_HOME="<command-line-tools-root>/sdk/default/openharmony"
export PATH="<command-line-tools-root>/bin:${PATH}"
进入 EasyTier 的 HarmonyOS 适配目录:
cd <repo-root>/easytier-contrib/easytier-ohrs
加载环境脚本:
source env.sh
env.sh 主要做几件事:
- 检查
OHOS_NDK_HOME是否已经设置。 - 检查
native/llvm和native/sysroot是否存在。 - 设置
CC、CXX、AR、LD等交叉编译变量。 - 设置
PKG_CONFIG_PATH、PKG_CONFIG_LIBDIR、PKG_CONFIG_SYSROOT_DIR。
加载成功后可以看到类似输出:
OpenHarmonyOS environment is ready:
OHOS_NDK_HOME: /.../sdk/default/openharmony
OHOS_TOOLCHAIN_DIR: /.../native/llvm
OHOS_SYSROOT: /.../native/sysroot
PKG_CONFIG_PATH: /.../native/sysroot/usr/lib/pkgconfig:...
三、移植流程总览
参考实际适配过程,整个 EasyTier 鸿蒙 PC 移植可以拆成几个阶段:
0. 底层可行性确认
rust-tun 编译适配 / ArkTS 创建 TUN / TUN fd 传递
1. 最小 Native 导出
先暴露 setTunFd、runNetworkInstance、collectNetworkInfos 等核心接口
2. HAR 包交付
补齐 package/ 结构,用 ohrs artifact 生成 package.har
3. 配置模型重构
从 TOML 字符串过渡到 NetworkConfig JSON
4. 能力下沉
配置仓库、运行态快照、TUN 请求、路由聚合、本地 socket 下沉到 Rust
5. 鸿蒙 PC 验证
ohpm install 安装 HAR,ArkTS 做最小调用验证
这条路径不是一开始就设计完整的“最终架构”,而是先解决能不能跑,再解决怎么交付,再解决怎么维护。
四、阶段 0:先确认底层可行性
网络类项目迁移到鸿蒙 PC,最先要确认的不是 UI,而是底层能力。EasyTier 依赖 TUN、socket、路由、虚拟 IP、peer 状态等能力,如果这些底层能力没有基础,上层页面写得再完整也跑不起来。
这里的 TUN 适配不能简单理解为“把鸿蒙侧 TUN 管理完整接入 rust-tun”。由于 HarmonyOS 系统能力的特殊性,TUN 设备需要在 ArkTS 侧通过系统接口创建,再把拿到的 fd 传给 Rust 侧。Rust 侧做的是接收这个 fd,并把它交给 EasyTier 内核使用。
当前 EasyTier 使用的 TUN 依赖来自:
tun = { package = "tun-easytier", git = "https://github.com/EasyTier/rust-tun", ... }
这个 rust-tun 分支基于原版 https://github.com/meh/rust-tun 演进。早期使用上游更新分支时,OHOS 目标仍然无法直接编译通过,因此又针对 OHOS 做了部分补充和修正,让 EasyTier 现有 TUN 抽象在 OHOS target 下具备可编译的适配入口。
也就是说,rust-tun 这一层主要解决的是编译适配和抽象兼容问题,不是让 Rust 侧完整创建和管理 HarmonyOS 的 TUN 设备。真正的 TUN 创建仍然发生在 ArkTS 侧,Rust 侧通过 set_tun_fd 接收 fd。
EasyTier 核心里也有多处 OHOS 条件编译处理,例如:
easytier/build/main.rseasytier/src/launcher.rseasytier/src/instance/instance.rseasytier/src/instance/virtual_nic.rseasytier/src/connector/mod.rseasytier/src/common/network.rseasytier/src/common/global_ctx.rseasytier/src/tunnel/common.rseasytier/src/peers/peer_manager.rs
这些条件编译不是为了“让代码编过”随手加的,而是因为 OHOS 在几个关键网络路径上和普通 Linux、Android、macOS 都不完全一样。如果不把这些分支限制在对应平台上,要么会编译失败,要么会在运行时走错网络路径。
4.1 build/main.rs:把 OHOS 归入 mobile 类平台
EasyTier 的构建脚本里先定义了一个 mobile cfg alias:
cfg_aliases! {
mobile: {
any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
)
}
}
这个定义很关键。OHOS 适配并不是完全走普通 Linux 桌面路径,而是更接近移动端/受限系统路径:TUN 不是 Rust 自己创建,而是外部系统能力创建后把 fd 传进来;部分网络能力也不能假设拥有完整桌面 Linux 权限。
有了这个 alias 后,后面的 #[cfg(mobile)] 就可以统一覆盖 Android、iOS、macOS Network Extension 和 OHOS。这样 OHOS 不需要到处单独复制一份逻辑,而是复用“外部创建 TUN、Rust 接收 fd、运行时再挂接 NIC”的移动端架构。
4.2 launcher.rs / instance.rs / virtual_nic.rs:TUN fd 从 ArkTS 侧注入
被归入 mobile 后,启动流程会多一条等待 TUN fd 的路径:
#[cfg(mobile)]
async fn run_routine_for_mobile(
instance: &Instance,
data: &EasyTierData,
tasks: &mut JoinSet<()>,
) {
let global_ctx = instance.get_global_ctx();
let peer_mgr = instance.get_peer_manager();
let nic_ctx = instance.get_nic_ctx();
let peer_packet_receiver = instance.get_peer_packet_receiver();
let mut tun_fd_receiver = data.tun_fd.1.lock().unwrap().take().unwrap();
tasks.spawn(async move {
loop {
let Some(tun_fd) = tun_fd_receiver.recv().await.flatten() else {
return;
};
let res = Instance::setup_nic_ctx_for_mobile(
nic_ctx.clone(),
global_ctx.clone(),
peer_mgr.clone(),
peer_packet_receiver.clone(),
tun_fd,
)
.await;
}
});
}
真正创建 TUN 设备的逻辑在 HarmonyOS 系统侧,也就是 ArkTS 侧;Rust 侧收到 fd 后,用 raw_fd 把这个已存在的 TUN 设备接入 EasyTier:
#[cfg(mobile)]
pub async fn create_dev_for_mobile(
&mut self,
tun_fd: std::os::fd::RawFd,
) -> Result<Box<dyn Tunnel>, Error> {
let mut config = Configuration::default();
config.layer(Layer::L3);
config.raw_fd(tun_fd);
config.close_fd_on_drop(false);
config.up();
// ...
}
这里的条件编译限制非常必要。如果 OHOS 走普通桌面 Linux 的 tun::create() 路径,Rust 会尝试自己创建和配置 TUN 设备,这和鸿蒙侧实际的系统能力调用方式不匹配。正确做法是 ArkTS 负责申请系统 TUN,Rust 只接收 fd 并挂到 EasyTier 的转发链路上。
同时,instance.rs 里一些非移动端的静态 IP 检查、自动创建 NIC 流程也会被 #[cfg(not(mobile))] 排除。原因也是同一个:OHOS 的 TUN 生命周期由 ArkTS 和系统能力控制,不能直接套用桌面端“Rust 创建 TUN 并配置 IP”的流程。
4.3 instance.rs:ICMP proxy 失败不能阻断实例启动
instance.rs 里有一段和 ICMP proxy 启动相关的处理:
if let Err(e) = self.icmp_proxy.start().await {
tracing::error!("start icmp proxy failed: {:?}", e);
if cfg!(not(any(
target_os = "android",
any(
target_os = "ios",
all(target_os = "macos", feature = "macos-ne")
),
target_env = "ohos"
))) {
// android, ios and ohos not support icmp proxy
return Err(e);
}
}
ICMP proxy 这类能力通常依赖更底层的 raw socket 或系统网络权限。在 HarmonyOS 上,这条路径并不能按普通桌面 Linux 的方式工作。如果这里不对 OHOS 做特殊处理,icmp_proxy.start() 一旦失败,整个 EasyTier 实例就会启动失败。
但是对于鸿蒙 PC 适配来说,ICMP proxy 不是第一阶段跑通 EasyTier 的必要条件。TCP、UDP、TUN fd、实例启动、运行态收集这些核心链路仍然可以继续工作。因此这里的处理方式是:OHOS 上 ICMP proxy 启动失败只记录日志,不把错误继续向上抛,避免一个不支持的辅助能力阻断整个实例。
这个分支本质上是在做“能力降级”:平台不支持的部分不强行启用,但主流程继续跑。
4.4 virtual_nic.rs:TUN packet information header 的协议字段不同
virtual_nic.rs 里处理的是 TUN 包转换。这里有一个看起来很小但很关键的差异:不同平台的 packet information header 里,协议字段取值并不一样。
OHOS 被放在 Linux/Android 这一组:
#[cfg(any(target_os = "linux", target_os = "android", target_env = "ohos"))]
fn into_pi_field(self) -> Result<u16, io::Error> {
use nix::libc;
match self {
PacketProtocol::IPv4 => Ok(libc::ETH_P_IP as u16),
PacketProtocol::IPv6 => Ok(libc::ETH_P_IPV6 as u16),
PacketProtocol::Other(_) => Err(io::Error::other("neither an IPv4 nor IPv6 packet")),
}
}
而 macOS、iOS、FreeBSD 用的是另一套:
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
fn into_pi_field(self) -> Result<u16, io::Error> {
use nix::libc;
match self {
PacketProtocol::IPv4 => Ok(libc::PF_INET as u16),
PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16),
PacketProtocol::Other(_) => Err(io::Error::other("neither an IPv4 nor IPv6 packet")),
}
}
这里不能混用。Linux 风格 TUN PI header 里写的是 ETH_P_IP、ETH_P_IPV6 这种以太网协议号;BSD/macOS/iOS 那边更接近地址族语义,用的是 PF_INET、PF_INET6。
鸿蒙侧虽然 TUN 是 ArkTS 创建的,但 Rust 侧仍然需要把 EasyTier 内部的 packet 转成写入 TUN fd 的字节流。如果这里把 OHOS 归到 macOS/iOS 那组,协议字段就会写错,后续 IPv4/IPv6 包识别可能出现异常。因此这里必须用 target_env = "ohos" 明确让 OHOS 走 Linux/Android 风格的协议字段。
4.5 connector/mod.rs:OHOS 不主动枚举本机地址给连接器绑定
连接 peer 时,桌面端可以根据本机网卡地址给 connector 设置可用的 bind address,这样在多网卡场景下可以控制连接从哪个地址发起。但 OHOS 被排除在这条路径之外:
async fn set_bind_addr_for_peer_connector(
connector: &mut (impl TunnelConnector + ?Sized),
is_ipv4: bool,
global_ctx: &ArcGlobalCtx,
) {
if cfg!(any(
target_os = "android",
any(
target_os = "ios",
all(target_os = "macos", feature = "macos-ne")
),
target_env = "ohos"
)) {
return;
}
let ips = global_ctx.get_ip_collector().collect_ip_addrs().await;
// 给 connector 设置本机 IPv4/IPv6 bind 地址
// ...
}
这个分支的原因和系统网络模型有关。鸿蒙 PC 上存在系统网络、应用侧 TUN、EasyTier 虚拟网络等多层路径,简单枚举本机地址再强行绑定 connector,反而可能绑到不适合的地址,导致连接失败或走错出口。
所以 OHOS 这里更适合让系统路由和上层 TUN 协作路径决定连接出口,而不是复用桌面 Linux 那套“枚举所有本机 IP,再交给 connector 绑定”的逻辑。
4.6 common/network.rs:接口过滤不能套用 Linux /sys/class/net 规则
Linux 下 EasyTier 会过滤掉 loopback、down 状态、point-to-point、TUN/TAP 等接口,其中 TUN/TAP 判断会访问 /sys/class/net/{iface}/tun_flags:
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
impl InterfaceFilter {
async fn is_tun_tap_device(&self) -> bool {
let path = format!("/sys/class/net/{}/tun_flags", self.iface.name);
tokio::fs::metadata(&path).await.is_ok()
}
async fn filter_iface(&self) -> bool {
!self.iface.is_point_to_point()
&& !self.iface.is_loopback()
&& self.iface.is_up()
&& self.iface.is_lower_up()
&& !self.is_tun_tap_device().await
&& self.has_valid_ip().await
}
}
OHOS 则走移动端简化过滤:
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
impl InterfaceFilter {
async fn filter_iface(&self) -> bool {
true
}
}
原因是 OHOS 虽然 target 上接近 Linux,但它不是普通桌面 Linux 运行环境。/sys/class/net 这类路径和接口属性不一定能按标准 Linux 方式访问或解释,尤其 TUN 又是 ArkTS 侧通过系统能力创建的。如果继续套用 Linux 的过滤逻辑,可能把有效接口误过滤掉,或者因为访问系统路径失败引入额外问题。
因此这里专门写成 target_os = "linux", not(target_env = "ohos"),把 OHOS 从普通 Linux 过滤路径里拆出来。
4.7 tunnel/common.rs:socket 绑定网卡的系统 API 不同
tunnel/common.rs 里处理 socket bind。EasyTier 在一些场景下需要把 socket 绑定到指定网络接口,否则多网卡、多路由环境下可能会从错误的出口发包。
Linux、Android、Fuchsia、OHOS 走的是 bind_device 这一套:
#[cfg(any(
target_os = "android",
target_os = "fuchsia",
target_os = "linux",
target_env = "ohos"
))]
if let Some(dev_name) = bind_dev {
tracing::trace!(dev_name = ?dev_name, "bind device");
socket2_socket.bind_device(Some(dev_name.as_bytes()))?;
}
macOS/iOS 则是另外一套 IP_BOUND_IF,按网卡 index 绑定。
这个分支限制的原因也很直接:不同系统暴露的 socket 选项不一样。OHOS 更接近 Linux 这一侧,使用设备名绑定网卡;如果走 macOS/iOS 的分支,接口和语义都不对。如果完全不做绑定,在多网络环境下又可能出现“地址绑定成功,但实际发包接口不符合预期”的问题。
所以这里的 OHOS 条件编译是为了让 socket 出口选择走正确的系统 API。
4.8 common/global_ctx.rs:API 17 阶段的 TUN 绕过问题与出口兜底
global_ctx.rs 里有一个很短但影响很大的分支:
pub fn enable_exit_node(&self) -> bool {
self.flags.load().enable_exit_node || cfg!(target_env = "ohos")
}
这段代码不能简单理解成“OHOS 用户默认开启出口节点功能”。它现在更多是一个兼容兜底分支,主要用于保证后续如果仍有老版本 OHOS 设备需要运行 EasyTier,相关网络功能不会因为历史系统行为差异而失效。
在 API 17 阶段,鸿蒙侧曾尝试通过 vpnExtension 的 protect socket 方式让隧道连接绕过 TUN。理论上,隧道自身的 socket 被 protect 后,隧道流量不应该再回到 TUN 中。但实际运行时仍会出现少量包进入 TUN 的情况,这会导致虚拟网和外部网络之间的连接不稳定:一部分包按预期走隧道,一部分包又被系统重新送入 TUN,路径变得不可控。
为了解决这类老系统上的不稳定情况,当时采用过更保守的处理方式:不再完全依赖 protect socket 把部分包从 TUN 路径中“摘出去”,而是允许流量统一进入 TUN,再由 Rust 侧根据 EasyTier 内部路由决定如何处理。
在这个兼容策略下,如果一个包能匹配到 EasyTier 虚拟网络内部的 peer,就按虚拟网路由转发;如果内部路由失败,但目标又不属于当前虚拟网络,就把它按“出口节点”语义处理,从本机真实网络发出去。这样即使老版本 OHOS 设备上的 TUN 绕过行为不稳定,虚拟网络访问外部网络时仍然有一条可用出口路径。
所以 cfg!(target_env = "ohos") 这里的意义是保留一条 OHOS 兼容兜底路径。它主要是为了在以后需要兼容老 OHOS 设备时,仍然能够通过“进入 TUN 后按内部路由/出口兜底处理”的方式保障功能。
4.9 peer_manager.rs:OHOS 的回流路径不能套用普通平台的防回环逻辑
peer_manager.rs 里的 OHOS 分支就是上面这个兼容策略在发包路径上的落点。IPv4 目标找不到明确 peer,且目标不在当前网络内时,OHOS 上会做一个兜底处理:
#[cfg(target_env = "ohos")]
{
if dst_peers.is_empty()
&& !self
.global_ctx
.is_ip_in_same_network(&std::net::IpAddr::V4(*ipv4_addr))
{
tracing::trace!("no peer id for ipv4: {}, set exit_node for ohos", ipv4_addr);
dst_peers.push(self.my_peer_id.clone());
is_exit_node = true;
}
}
后面还有一段非 OHOS 平台才启用的防回环逻辑:
#[cfg(not(target_env = "ohos"))]
{
if not_send_to_self
&& *peer_id == self.my_peer_id
&& !self.global_ctx.is_ip_local_virtual_ip(&ip_addr)
{
hdr.set_not_send_to_tun(true);
hdr.set_no_proxy(true);
}
}
普通平台上,发送给自己的包很容易形成代理回环,所以这里会设置 not_send_to_tun 和 no_proxy,避免包再次进入 TUN 或 proxy 路径。
但在老 OHOS 设备的兼容场景里,目标正好不同:外部流量进入 TUN 后,如果 EasyTier 内部路由找不到目标 peer,需要允许它落到“本机作为出口”的路径上,再从本机真实网络发出。如果直接套用普通平台的防回环标记,就可能把这条出口兜底路径提前切断,导致外部网络访问失败。
因此这里用 target_env = "ohos" 单独限制:OHOS 可以在找不到目标 peer 时兜底到自身作为 exit node,同时不启用普通平台的 not_send_to_tun/no_proxy 防回环处理。这个分支配合前面的 enable_exit_node || cfg!(target_env = "ohos"),保留的是老 OHOS 设备可用的网络兜底能力,而不是当前架构必须依赖的唯一转发路径。
4.10 easytier-ohrs 日志:Rust tracing 转到 HiLog
除了 EasyTier core,easytier-ohrs 包装层也有 OHOS 条件编译。比如日志桥接里会在 OHOS 下把 Rust tracing 事件转发到 HiLog:
fn tracing_callback(event: &Event, fields: HashMap<String, String>) {
let metadata = event.metadata();
#[cfg(target_env = "ohos")]
{
let loc = metadata.target().split("::").last().unwrap();
match *metadata.level() {
Level::TRACE => hilog_debug!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>()),
Level::DEBUG => hilog_debug!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>()),
Level::INFO => hilog_info!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>()),
Level::WARN => hilog_warn!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>()),
Level::ERROR => hilog_error!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>()),
}
}
}
这类条件编译不是网络链路的一部分,但对鸿蒙 PC 移植调试很重要。Rust 侧如果还停留在 stdout/stderr 或普通日志输出,真机排查会很不方便。接入 HiLog 后,NAPI 层、实例启动、TUN fd 注入、配置解析、运行态聚合这些问题都能在鸿蒙日志系统里统一查看。
这一阶段的判断标准很直接:ArkTS 侧能不能创建 TUN 并拿到 fd,Rust 侧能不能接收这个 fd,EasyTier 实例能不能启动并收集基础运行信息。
五、阶段 1:先做最小 Native 导出
第一版适配没有一上来就把配置仓库、运行态、路由聚合都放到 Rust。早期 Rust 侧只提供最小导出接口,用来验证核心链路。
当时比较关键的接口包括:
#[napi]
pub fn set_tun_fd(inst_id: String, fd: i32) -> bool
#[napi]
pub fn parse_config(cfg_str: String) -> bool
#[napi]
pub fn run_network_instance(cfg_str: String) -> bool
#[napi]
pub fn stop_network_instance(inst_names: Vec<String>)
#[napi]
pub fn collect_network_infos() -> Vec<KeyValuePair>
这几个接口解决了第一阶段最关键的问题:
- ArkTS 侧可以把系统申请到的 TUN fd 交给 Rust。
- Rust 侧可以解析配置并启动 EasyTier 实例。
- ArkTS 侧可以拿到基础运行信息,用来判断实例是否启动成功。
这种设计很适合快速落地。早期 ArkTS 侧做更多编排不是问题,反而能加快试错速度。因为这时最重要的是验证“EasyTier 能不能在鸿蒙 PC 上真正跑起来”。
但这个阶段也埋下了后续需要重构的原因:配置、运行状态、路由和 TUN 请求都在 ArkTS 侧组织后,随着功能增加,上层代码会越来越重。
六、阶段 2:从 Native 库整理成 HAR 包
能生成 .so 只是第一步,鸿蒙工程最终需要的是可以安装、可以声明依赖、可以复用的包。
因此后续补齐了 HAR 包结构:
package/
├── oh-package.json5
├── index.ets
├── src/main/module.json5
├── README.md
├── LICENSE
└── CHANGELOG.md
其中 index.ets 很关键:
import * as api from 'libeasytier_ohrs.so';
export * from 'libeasytier_ohrs.so';
export default api;
这段代码看起来很短,但它解决的是包的入口问题。应用工程安装 HAR 后,只需要通过包名导入 EasyTier,不需要直接关心 libeasytier_ohrs.so 在 HAR 里的路径。
HAR 形态带来的价值很明显:
- Native 库、ArkTS 入口、类型声明、包元信息放在一起。
- 应用工程通过
ohpm install package.har接入。 - 后续 EasyTier 更新时,只需要重新构建并安装新 HAR。
这一步之后,EasyTier 才真正从“能编译的 Native 库”变成“鸿蒙 PC 工程能消费的组件”。
七、阶段 3:配置模型从 TOML 过渡到 NetworkConfig
早期接口直接接收 TOML 字符串:
#[napi]
pub fn run_network_instance(cfg_str: String) -> bool {
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
Ok(cfg) => cfg,
Err(e) => {
hilog_error!("[Rust] parse config failed {}", e);
return false;
}
};
// 启动实例
}
这种方式适合第一版验证,因为 TOML 本来就是 EasyTier 的传统配置表达,Rust 侧可以直接复用 TomlConfigLoader。
但当 ArkTS 侧需要做配置编辑、表单绑定、默认配置生成、导入导出时,直接维护一整段 TOML 文本就不太合适了。它不利于字段级编辑,也不利于 UI 层做结构化展示。
后续改成了 NetworkConfig JSON 边界:
#[napi]
pub fn run_network_instance(cfg_json: String) -> bool {
let cfg = match serde_json::from_str::<NetworkConfig>(&cfg_json) {
Ok(cfg) => match cfg.gen_config() {
Ok(toml) => toml,
Err(e) => {
hilog_error!("[Rust] parse config failed {}", e);
return false;
}
},
Err(e) => {
hilog_error!("[Rust] parse config failed {}", e);
return false;
}
};
// 启动实例
}
这次改动的核心不是“换一种序列化格式”,而是重新整理 ArkTS 和 Rust 的边界:
- ArkTS 侧更适合操作 JSON 对象。
- Rust 侧继续负责把
NetworkConfig转成 EasyTier 内核真正需要的配置。 - 配置校验、默认配置、导入导出可以逐步统一到 Rust。
后续配置能力继续下沉,就是从这里开始变得顺畅的。
八、阶段 4:把配置和运行态能力下沉到 Rust
随着功能增加,ArkTS 侧承载的逻辑越来越多:配置列表、配置字段编辑、导入导出、运行态展示、TUN 请求、路由信息整理等。如果这些都放在 ArkTS 侧,UI 代码会越来越重,也会产生更多中间状态。
后续适配把这些能力逐步移回 Rust,主要包括:
- 配置仓库初始化、保存、查询、删除。
- 配置字段级读写。
- TOML 导入导出。
- 旧配置迁移。
- 配置 schema 和字段映射。
- 分享链接生成与解析。
- 运行态快照聚合。
- TUN 请求和路由聚合。
- 本地 socket 向 ArkTS 推送运行态与 TUN 请求。
8.1 配置仓库为什么要放到 Rust
配置不是单纯的 UI 状态,它和 EasyTier 实例启动、默认配置生成、字段校验、导入导出都有关系。
Rust 侧保存配置时,会先校验并规范化配置,再把字段拆出来存储:
pub fn save_config_record(
config_id: String,
display_name: String,
config_json: String,
) -> Option<StoredConfigRecord> {
let config = match validation::validate_config_json(&config_json, config_id.clone()) {
Ok(config) => config,
Err(e) => {
hilog_error!("[Rust] save_config_record failed {}", e);
return None;
}
};
let fields = match validation::config_to_top_level_map(&config) {
Some(fields) => fields,
None => return None,
};
let conn = open_db()?;
let tx = conn.unchecked_transaction().ok()?;
// 保存配置 meta 和字段
tx.commit().ok()?;
// ...
}
这样改完后,ArkTS 侧不再需要关心配置文件怎么组织、字段怎么拆、旧配置怎么迁移,只需要调用 saveConfig、getConfig、setConfigField 这类接口。
8.2 运行态快照为什么要放到 Rust
运行信息来自 EasyTier 的 INSTANCE_MANAGER。如果 ArkTS 侧每次都自己拉取、拼装、判断 TUN 状态和路由信息,逻辑会非常散。
Rust 侧聚合运行态后,可以直接生成更适合 UI 消费的状态结构:
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
let infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
Ok(infos) => infos,
Err(err) => {
hilog_error!("[Rust] collect network infos failed {}", err);
return RuntimeAggregateState {
instances: vec![],
// ...
};
}
};
// 将 EasyTier 内核运行信息整理成 ArkTS 更容易消费的状态结构
// ...
}
这种设计让 ArkTS 侧拿到的不是一堆零散底层信息,而是已经聚合好的 RuntimeAggregateState。
8.3 为什么要增加本地 socket server
TUN 请求和运行态变化并不适合完全靠 ArkTS 主动轮询。Rust 侧更接近数据源,也更清楚什么时候需要通知上层。
因此后续增加了本地 socket server,用来向 ArkTS 侧广播 runtime_snapshot 和 tun_request:
pub fn start_local_socket_server() -> bool {
let socket_path = match kernel_socket_path() {
Some(path) => path,
None => {
hilog_error!("[Rust] kernel socket path unavailable");
return false;
}
};
// 监听本地 socket,向 ArkTS 侧广播 runtime_snapshot 和 tun_request
// ...
}
这样 ArkTS 侧不需要频繁主动拼装状态,也不需要自己判断路由变化是否需要重新发起 TUN 请求。Rust 侧负责聚合和去重,ArkTS 侧负责接收事件并调用系统能力。
这一阶段的重构,本质上是在解决早期快速落地后的架构债:逻辑从 ArkTS 侧收回到 Rust,接口边界变清晰,内存和耦合问题也更容易控制。
九、Linux 环境下编译 HAR
完成第二章的基础环境部署,并在 easytier-ohrs 目录执行 source env.sh 后,就可以开始编译 HAR。
执行 ohrs 构建命令:
ohrs doctor
ohrs build --release --arch aarch
ohrs artifact
三条命令分别对应:
ohrs doctor:检查 HarmonyOS Rust 构建环境。ohrs build --release --arch aarch:编译 Rust 核心并生成libeasytier_ohrs.so。ohrs artifact:把 Native 库、类型声明和 HAR 包结构整合成package.har。
本次环境里 ohrs doctor 会提示部分额外 target 未安装,但不影响 aarch 构建成功。最终判断标准还是看下面两个文件是否生成:
dist/arm64-v8a/libeasytier_ohrs.sopackage.har


dist/arm64-v8a与 HAR 产物截图
十、鸿蒙 PC 工程验证
HAR 生成后,在目标鸿蒙 PC 工程根目录安装:
ohpm install <repo-root>/easytier-contrib/easytier-ohrs/package.har
如果之前安装过旧版本,可以先卸载再安装:
ohpm uninstall easytier-ohrs
ohpm install <repo-root>/easytier-contrib/easytier-ohrs/package.har

ohpm install package.har成功输出
ArkTS 侧先做最小验证:
import EasyTier from 'easytier-ohrs';
@Entry
@ComponentV2
struct Demo {
version = EasyTier.easytierVersion();
defaultConfig = EasyTier.defaultNetworkConfig();
valid = EasyTier.parseNetworkConfig(this.defaultConfig);
build() {
Column() {
Text(`EasyTier version: ${this.version}`)
Text(`Default config valid: ${this.valid}`)
}
}
}
这个验证页主要确认四件事:
- HAR 是否成功安装。
libeasytier_ohrs.so是否能被加载。- NAPI 导出函数是否能被 ArkTS 调用。
- 默认配置生成和配置校验是否正常。

鸿蒙 PC 侧 EasyTier 验证页
最小验证通过后,再继续验证实例启动、停止、运行状态查询和 TUN 交互。这样排查路径比较清楚,能区分是包加载问题、NAPI 调用问题、配置问题,还是 EasyTier 运行时问题。
十一、适配过程中遇到的几个问题
11.1 旧版基础库说明不一定还适用
旧版说明中曾提到构建前需要手工编译:
gliblibffipcre2zlib
基于当前 EasyTier 源码版本和 SDK 的实测结果,不进行上面依赖的编译,直接执行下面命令已经可以完成构建:
ohrs build --release --arch aarch
所以复现时可以先按当前流程跑。如果遇到新的依赖问题,再根据具体报错补处理。
11.2 ohrs doctor 不一定要全绿
本次环境中,ohrs doctor 会提示部分 target 未安装,但 aarch 构建没有受到影响。
更稳妥的判断方式是:
1. 执行 ohrs build --release --arch aarch
2. 检查 dist/arm64-v8a/libeasytier_ohrs.so 是否生成
3. 执行 ohrs artifact
4. 检查 package.har 是否生成
只要这条链路完整跑通,当前目标架构的迁移构建就成立。
11.3 不要手工复制 .so
推荐链路是:
ohrs build --release --arch aarch
ohrs artifact
然后在应用工程中安装 package.har。
如果直接复制 .so,很容易出现 Rust 侧重新编译了,但鸿蒙工程里仍然引用旧 Native 库的问题。HAR 至少能把 Native 库、ArkTS 入口、类型声明和包信息一起更新。
11.4 package.har 不是普通 zip 包
本次实测中,package.har 的实际文件格式是 gzip tar,不是普通 zip 包。
所以用 unzip 检查它可能会误判。这里按 ohrs artifact 的产物理解即可。
十二、完整复现命令
export OHOS_NDK_HOME="<command-line-tools-root>/sdk/default/openharmony"
export PATH="<command-line-tools-root>/bin:${PATH}"
cd <repo-root>/easytier-contrib/easytier-ohrs
source env.sh
ohrs build --release --arch aarch
ohrs artifact
ls dist/arm64-v8a/libeasytier_ohrs.so package.har
如果最后能看到 dist/arm64-v8a/libeasytier_ohrs.so 和 package.har,说明 EasyTier 到鸿蒙 PC 的 HAR 构建链路已经跑通。
十三、复盘
EasyTier 这次鸿蒙 PC 适配的关键不是某一条编译命令,而是迁移顺序。
比较合理的路径是:
先用 ArkTS 快速落地,把核心链路跑通
↓
发现 ArkTS 侧状态和编排逻辑越来越重
↓
将配置管理、运行态聚合、TUN 请求、路由整理下沉到 Rust
↓
用 NAPI 重新整理 ArkTS 和 Rust 的边界
↓
用 HAR 作为最终交付形态
早期 ArkTS 侧承担更多逻辑,是为了快速验证;后续 Rust 侧承接更多能力,是为了解决内存占用、代码耦合和长期维护问题。
对于 EasyTier 这类网络工具来说,移植重点不只是能不能在页面里调用几个函数,而是 Native 层能不能形成稳定、可维护、可复用的适配边界。这个边界稳定之后,鸿蒙 PC 工程才能像依赖普通 HAR 一样复用 EasyTier 的核心能力。
更多推荐



所有评论(0)