feat(ohos): add nearby console integration and improve UDP path selection (#2486)

* feat(ohos): complete nearby console integration

Use the core management RPC surface for ephemeral nearby deployments, harden session lifecycle and packet validation, support tunnel-to-NIC packet conversion, and timestamp HarmonyOS traffic samples.

* fix(core): prefer verified UDP hole-punch paths

Treat zero latency as unmeasured so a newly admitted UDP path cannot replace a working relay before liveness is confirmed.

---------

Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local>
This commit is contained in:
韩嘉乐
2026-08-10 14:15:09 +08:00
committed by GitHub
co-authored by FrankHan
parent d375d7e455
commit 23d55373a4
7 changed files with 1276 additions and 17 deletions
+8
View File
@@ -1244,6 +1244,8 @@ dependencies = [
"derive_builder", "derive_builder",
"easytier-proto", "easytier-proto",
"futures", "futures",
"getrandom 0.2.16",
"getrandom 0.3.3",
"guarden", "guarden",
"hmac", "hmac",
"http-body-util", "http-body-util",
@@ -1277,6 +1279,7 @@ dependencies = [
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
"wasm-bindgen",
"webpki-roots 0.26.11", "webpki-roots 0.26.11",
"wildmatch", "wildmatch",
"x25519-dalek", "x25519-dalek",
@@ -1288,10 +1291,15 @@ dependencies = [
name = "easytier-ohrs" name = "easytier-ohrs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
"bytes",
"easytier", "easytier",
"easytier-core",
"easytier-proto",
"flate2", "flate2",
"futures",
"gethostname 1.1.0", "gethostname 1.1.0",
"ipnet", "ipnet",
"napi-build-ohos", "napi-build-ohos",
@@ -7,9 +7,18 @@ edition = "2024"
crate-type=["cdylib"] crate-type=["cdylib"]
[dependencies] [dependencies]
anyhow = "1.0"
async-trait = "0.1" async-trait = "0.1"
base64 = "0.22" base64 = "0.22"
bytes = "1.5"
easytier-core = { path = "../../easytier-core", default-features = false }
easytier-proto = { path = "../../easytier-proto", default-features = false, features = [
"api",
"core",
"json-rpc",
] }
flate2 = "1.1" flate2 = "1.1"
futures = "0.3"
gethostname = "1.1" gethostname = "1.1"
easytier = { path = "../../easytier" } easytier = { path = "../../easytier" }
napi-derive-ohos = "1.1" napi-derive-ohos = "1.1"
@@ -20,7 +20,7 @@ use std::path::PathBuf;
use std::sync::Mutex; use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
struct LocalSocketState { struct LocalSocketState {
stop_flag: std::sync::Arc<AtomicBool>, stop_flag: std::sync::Arc<AtomicBool>,
@@ -38,6 +38,7 @@ const EVENT_RECEIVER_SYNC_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct TrafficStatsPayload { struct TrafficStatsPayload {
sampled_at_ms: i64,
instances: Vec<InstanceTrafficStats>, instances: Vec<InstanceTrafficStats>,
} }
@@ -224,7 +225,7 @@ fn tun_candidate_ids(snapshot: &RuntimeAggregateState) -> HashSet<String> {
.collect() .collect()
} }
fn collect_traffic_stats() -> TrafficStatsPayload { fn collect_traffic_stats(sampled_at_ms: i64) -> TrafficStatsPayload {
let running_instances = INSTANCE_MANAGER let running_instances = INSTANCE_MANAGER
.instances() .instances()
.into_iter() .into_iter()
@@ -285,7 +286,17 @@ fn collect_traffic_stats() -> TrafficStatsPayload {
instances instances
}); });
TrafficStatsPayload { instances } TrafficStatsPayload {
sampled_at_ms,
instances,
}
}
fn unix_time_millis() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis().min(i64::MAX as u128) as i64)
.unwrap_or_default()
} }
pub fn start_local_socket_server() -> bool { pub fn start_local_socket_server() -> bool {
@@ -409,7 +420,7 @@ pub fn start_local_socket_server() -> bool {
.unwrap_or(true); .unwrap_or(true);
if should_collect_traffic_stats { if should_collect_traffic_stats {
last_traffic_stats_at = Some(now); last_traffic_stats_at = Some(now);
match serde_json::to_string(&collect_traffic_stats()) { match serde_json::to_string(&collect_traffic_stats(unix_time_millis())) {
Ok(json) => { Ok(json) => {
let _ = broadcast_local_socket_json_payload_message( let _ = broadcast_local_socket_json_payload_message(
&mut clients, &mut clients,
+96 -2
View File
@@ -37,6 +37,7 @@ macro_rules! ohrs_log_debug {
mod config; mod config;
mod exports; mod exports;
mod kernel_bridge; mod kernel_bridge;
mod nearby_management;
mod platform; mod platform;
mod runtime; mod runtime;
@@ -57,7 +58,7 @@ use easytier::common::config::NetworkConfigExt;
use easytier::common::constants::EASYTIER_VERSION; use easytier::common::constants::EASYTIER_VERSION;
use easytier::common::{ use easytier::common::{
MachineIdOptions, MachineIdOptions,
config::{ConfigFileControl, ConfigLoader, TomlConfigLoader}, config::{ConfigLoader, TomlConfigLoader},
}; };
use easytier::instance::factory::{NativeInstanceManager, native_instance_manager_with_runtime}; use easytier::instance::factory::{NativeInstanceManager, native_instance_manager_with_runtime};
use easytier::proto::api::manage::NetworkConfig; use easytier::proto::api::manage::NetworkConfig;
@@ -68,6 +69,7 @@ use kernel_bridge::{
stop_local_socket_server as stop_local_socket_server_inner, stop_local_socket_server as stop_local_socket_server_inner,
}; };
use napi_derive_ohos::napi; use napi_derive_ohos::napi;
use napi_ohos::bindgen_prelude::Uint8Array;
use runtime::state::runtime_state::{RuntimeAggregateState, RuntimeInstanceState}; use runtime::state::runtime_state::{RuntimeAggregateState, RuntimeInstanceState};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::format; use std::format;
@@ -380,6 +382,7 @@ fn stop_runtime_inner() -> bool {
&& ok; && ok;
} }
maybe_stop_local_socket_server(); maybe_stop_local_socket_server();
let _ = nearby_management::stop_runtime_management_server();
ok ok
} }
@@ -730,7 +733,12 @@ pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
return false; return false;
} }
match INSTANCE_MANAGER.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG) { let config_control = nearby_management::runtime_management_config_control(inst_id);
if !nearby_management::ensure_runtime_management_server_started() {
return false;
}
match INSTANCE_MANAGER.run_network_instance(cfg, config_control) {
Ok(_) => { Ok(_) => {
cache_runtime_config_snapshot(inst_id.to_string(), inst_id.to_string(), config); cache_runtime_config_snapshot(inst_id.to_string(), inst_id.to_string(), config);
true true
@@ -959,6 +967,92 @@ pub fn run_network_instance(cfg_json: String) -> bool {
run_network_instance_from_json(&cfg_json) run_network_instance_from_json(&cfg_json)
} }
/// Starts the management server in the VPN Extension process even when no
/// network instance is active, allowing a nearby controller to deploy a
/// one-shot config through the canonical Core RPC surface.
#[napi]
pub fn start_nearby_management_host() -> bool {
nearby_management::ensure_runtime_management_server_started()
}
#[napi]
pub fn stop_nearby_management_host() -> bool {
nearby_management::stop_runtime_management_server()
}
#[napi]
pub fn drain_nearby_host_commands() -> Vec<nearby_management::NearbyHostCommand> {
nearby_management::drain_nearby_host_commands()
}
#[napi]
pub fn complete_nearby_host_command(
request_id: String,
success: bool,
error: Option<String>,
) -> bool {
nearby_management::complete_nearby_host_command(request_id, success, error)
}
/// Returns 1 for canonical Core RPC packets, 2 for the OHOS-private settings
/// envelope, and 0 for malformed or unsupported data.
#[napi]
pub fn nearby_management_packet_kind(packet: Uint8Array) -> i32 {
nearby_management::nearby_management_packet_kind(packet)
}
#[napi]
pub fn encode_nearby_ohos_packet(envelope_json: String) -> Option<Uint8Array> {
nearby_management::encode_nearby_ohos_packet(envelope_json)
}
#[napi]
pub fn decode_nearby_ohos_packet(packet: Uint8Array) -> Option<String> {
nearby_management::decode_nearby_ohos_packet(packet)
}
/// Opens one Core RPC endpoint for a HarmonyOS collaboration session.
///
/// The Harmony layer transports the returned native packets verbatim with
/// `abilityConnectionManager.sendData`; all RPC framing stays inside Core.
#[napi]
pub fn open_nearby_management_session(session_key: String, host: bool) -> bool {
nearby_management::open_nearby_management_session(session_key, host)
}
#[napi]
pub fn close_nearby_management_session(session_key: String) -> bool {
nearby_management::close_nearby_management_session(session_key)
}
#[napi]
pub fn push_nearby_management_packet(session_key: String, packet: Uint8Array) -> bool {
nearby_management::push_nearby_management_packet(session_key, packet)
}
#[napi]
pub fn drain_nearby_management_packets(session_key: String) -> Vec<Uint8Array> {
nearby_management::drain_nearby_management_packets(session_key)
}
#[napi]
pub async fn call_nearby_management_json_rpc(
session_key: String,
service_name: String,
method_name: String,
domain_name: Option<String>,
payload_json: String,
) -> String {
nearby_management::call_nearby_management_json_rpc(
session_key,
service_name,
method_name,
domain_name,
payload_json,
)
.await
}
#[napi] #[napi]
pub fn collect_network_infos() -> Vec<KeyValuePair> { pub fn collect_network_infos() -> Vec<KeyValuePair> {
exports::runtime_api::collect_network_infos() exports::runtime_api::collect_network_infos()
File diff suppressed because it is too large Load Diff
@@ -206,7 +206,9 @@ where
op(true); op(true);
false false
} else { } else {
tracing::info!("hole punching transport admitted successfully"); tracing::info!(
"hole punching transport admitted; awaiting liveness measurement"
);
true true
} }
} }
+37 -10
View File
@@ -26,6 +26,10 @@ use tokio_util::task::AbortOnDropHandle;
type ArcPeerConn = Arc<PeerConn>; type ArcPeerConn = Arc<PeerConn>;
type ConnMap = Arc<DashMap<PeerConnId, ArcPeerConn>>; type ConnMap = Arc<DashMap<PeerConnId, ArcPeerConn>>;
fn conn_latency_sort_key(latency_us: u64, is_hole_punched: bool) -> (bool, u64) {
(is_hole_punched && latency_us == 0, latency_us)
}
pub struct Peer { pub struct Peer {
pub peer_node_id: PeerId, pub peer_node_id: PeerId,
conns: ConnMap, conns: ConnMap,
@@ -212,16 +216,19 @@ impl Peer {
return Some(conn); return Some(conn);
} }
// find a conn with the smallest latency // A zero latency on a hole-punched connection means the ping loop has not
let mut min_latency = u64::MAX; // confirmed liveness yet. Prefer any other connection, so a freshly admitted
let mut selected = None; // hole-punched path cannot steal traffic before its first successful ping.
for conn in self.conns.iter() { let selected = self
let latency = conn.value().get_stats().latency_us; .conns
if latency < min_latency { .iter()
min_latency = latency; .min_by_key(|conn| {
selected = Some(conn.value().clone()); conn_latency_sort_key(
} conn.value().get_stats().latency_us,
} conn.value().is_hole_punched(),
)
})
.map(|conn| conn.value().clone());
if let Some(conn) = selected.as_ref() { if let Some(conn) = selected.as_ref() {
self.default_conn.store(Some(conn.clone())); self.default_conn.store(Some(conn.clone()));
@@ -321,3 +328,23 @@ impl Drop for Peer {
tracing::info!("peer {} drop", self.peer_node_id); tracing::info!("peer {} drop", self.peer_node_id);
} }
} }
#[cfg(test)]
mod tests {
use super::conn_latency_sort_key;
#[test]
fn measured_relay_precedes_unverified_hole_punch_path() {
assert!(conn_latency_sort_key(20_000, false) < conn_latency_sort_key(0, true));
}
#[test]
fn unmeasured_regular_connection_keeps_existing_priority() {
assert!(conn_latency_sort_key(0, false) < conn_latency_sort_key(20_000, false));
}
#[test]
fn verified_lower_latency_path_can_be_preferred() {
assert!(conn_latency_sort_key(5_000, true) < conn_latency_sort_key(20_000, false));
}
}