Compare commits

...
Author SHA1 Message Date
FrankHan 5a0e0cd43b fix(ohos): expose valid config enum values
Return protobuf enum values through the OHRS schema API and keep the share-link test on the public config repository module.
2026-09-04 02:34:40 +08:00
FrankHan 8c64949dde fix(ohos): keep local proxy subnets off tun
Treat locally configured proxy CIDRs as advertisements rather than local VPN routes. Preserve the virtual CIDR, manual routes, and proxy CIDRs learned from remote nodes.
2026-09-04 02:34:39 +08:00
FrankHan 6e3460db7d fix(port-forward): support wildcard userspace listeners
Keep the existing Host listener intact while adding a DataPlane listener for force-smoltcp IPv4 wildcard rules. Keep literal loopback destinations on the local Host path instead of exporting them through an exit node.
2026-09-04 02:34:12 +08:00
7 changed files with 210 additions and 14 deletions
@@ -119,7 +119,11 @@ fn enum_options(kind: Kind) -> Vec<FieldOption> {
.values()
.map(|value| FieldOption {
label: value.name().to_string(),
value: value.number().to_string(),
// protobuf JSON uses enum names rather than their numeric wire values.
// Returning the number here made ArkTS write (for example) `1`, while
// NetworkConfig deserialization expects `"None"`, so field-level saves
// were rejected by the repository validation step.
value: value.name().to_string(),
})
.collect(),
_ => Vec::new(),
@@ -410,5 +414,17 @@ mod tests {
.iter()
.any(|option| option.label == "PublicServer")
);
let data_compress_algo = schema
.children
.iter()
.find(|field| field.name == "data_compress_algo")
.expect("data_compress_algo field");
let none = data_compress_algo
.enum_options
.iter()
.find(|option| option.label == "None")
.expect("compression None option");
assert_eq!(none.value, "None");
}
}
@@ -162,7 +162,7 @@ pub fn import_config_share_link(
#[cfg(test)]
mod tests {
use super::*;
use crate::config_repo::{create_config_record, init_config_store};
use crate::config::repository::{create_config_record, init_config_store};
use std::time::{SystemTime, UNIX_EPOCH};
fn test_root() -> String {
@@ -54,17 +54,14 @@ pub(crate) fn get_runtime_config_snapshot(config_id: &str) -> Option<RuntimeConf
.and_then(|guard| guard.get(config_id).cloned())
}
pub(crate) fn get_runtime_config_route_overrides(config_id: &str) -> (Vec<String>, Vec<String>) {
pub(crate) fn get_runtime_config_manual_routes(config_id: &str) -> Vec<String> {
RUNTIME_CONFIG_SNAPSHOTS
.lock()
.ok()
.and_then(|guard| {
guard.get(config_id).map(|snapshot| {
(
snapshot.config.routes.clone(),
snapshot.config.proxy_cidrs.clone(),
)
})
guard
.get(config_id)
.map(|snapshot| snapshot.config.routes.clone())
})
.unwrap_or_default()
}
@@ -1,4 +1,4 @@
use crate::config::repository::get_runtime_config_route_overrides;
use crate::config::repository::get_runtime_config_manual_routes;
use crate::runtime::state::runtime_state::RuntimeInstanceState;
use ipnet::IpNet;
use std::collections::HashSet;
@@ -59,8 +59,7 @@ pub(crate) fn aggregate_tun_routes(instance: &RuntimeInstanceState) -> Vec<Strin
.my_node_info
.as_ref()
.and_then(|info| info.virtual_ipv4_cidr.clone());
let (manual_routes, config_proxy_cidrs) =
get_runtime_config_route_overrides(&instance.config_id);
let manual_routes = get_runtime_config_manual_routes(&instance.config_id);
let runtime_proxy_cidrs = instance
.routes
.iter()
@@ -73,7 +72,9 @@ pub(crate) fn aggregate_tun_routes(instance: &RuntimeInstanceState) -> Vec<Strin
}
raw_routes.extend(manual_routes.iter().cloned());
raw_routes.extend(config_proxy_cidrs.iter().cloned());
// Locally configured proxy CIDRs are advertisements for networks reached
// through this node. Installing them into this node's TUN would recapture
// the proxy's own destination sockets instead of using the physical LAN.
raw_routes.extend(runtime_proxy_cidrs.iter().cloned());
simplify_routes(raw_routes)
}
@@ -90,3 +91,76 @@ pub(crate) fn aggregate_requested_tun_routes(instances: &[RuntimeInstanceState])
}
aggregated_routes
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::repository::{cache_runtime_config_snapshot, clear_runtime_config_snapshot};
use crate::runtime::state::runtime_state::{MyNodeInfo, RouteView};
use easytier::proto::api::manage::NetworkConfig;
fn runtime_instance(config_id: &str) -> RuntimeInstanceState {
RuntimeInstanceState {
config_id: config_id.to_string(),
instance_id: "test-instance".to_string(),
display_name: "test".to_string(),
running: true,
tun_required: true,
tun_attached: false,
magic_dns_enabled: false,
need_exit_node: false,
error_message: None,
my_node_info: Some(MyNodeInfo {
virtual_ipv4: Some("10.144.144.1".to_string()),
virtual_ipv4_cidr: Some("10.144.144.1/24".to_string()),
hostname: None,
version: None,
peer_id: Some(1),
listeners: Vec::new(),
vpn_portal_cfg: None,
udp_nat_type: None,
tcp_nat_type: None,
}),
events: Vec::new(),
routes: vec![RouteView {
peer_id: 2,
hostname: None,
ipv4: Some("10.144.144.2".to_string()),
ipv4_cidr: Some("10.144.144.2/24".to_string()),
ipv6_cidr: None,
proxy_cidrs: vec!["10.20.0.0/16".to_string()],
next_hop_peer_id: Some(2),
cost: Some(1),
path_latency: None,
udp_nat_type: None,
tcp_nat_type: None,
inst_id: None,
version: None,
is_public_server: None,
}],
peers: Vec::new(),
}
}
#[test]
fn local_proxy_cidr_is_not_installed_in_tun_routes() {
let config_id = "routing-test-local-proxy";
cache_runtime_config_snapshot(
config_id.to_string(),
"test".to_string(),
NetworkConfig {
routes: vec!["172.16.0.0/16".to_string()],
proxy_cidrs: vec!["192.168.1.0/24".to_string()],
..Default::default()
},
);
let routes = aggregate_tun_routes(&runtime_instance(config_id));
clear_runtime_config_snapshot(config_id);
assert!(routes.contains(&"10.144.144.0/24".to_string()));
assert!(routes.contains(&"172.16.0.0/16".to_string()));
assert!(routes.contains(&"10.20.0.0/16".to_string()));
assert!(!routes.contains(&"192.168.1.0/24".to_string()));
}
}
+5 -1
View File
@@ -63,6 +63,7 @@ use easytier::common::{
use easytier::instance::factory::{NativeInstanceManager, native_instance_manager_with_runtime};
use easytier::proto::api::manage::NetworkConfig;
use easytier::proto::api::manage::NetworkingMethod;
use easytier::proto::common::CompressionAlgoPb;
use easytier::web_client::{WebClient, WebClientHooks, run_web_client};
use kernel_bridge::{
start_local_socket_server as start_local_socket_server_inner,
@@ -669,8 +670,11 @@ fn resolve_instance_id_inner(instance_name: &str) -> Option<String> {
}
pub(crate) fn build_default_network_config_json() -> Result<String, String> {
let config = NetworkConfig::new_from_config(TomlConfigLoader::default())
let mut config = NetworkConfig::new_from_config(TomlConfigLoader::default())
.map_err(|e| format!("default_network_config failed {}", e))?;
// HarmonyOS 的配置编辑页将压缩算法作为显式选项展示。新建实例默认
// 使用 NONE,避免在用户没有主动选择时增加压缩开销。
config.data_compress_algo = Some(CompressionAlgoPb::None as i32);
serde_json::to_string(&config).map_err(|e| format!("default_network_config failed {}", e))
}
@@ -590,6 +590,8 @@ where
})?;
let overlay_destination = if local_virtual_destination {
true
} else if dst_ip.is_loopback() {
false
} else {
let (peers, _) = options
.deadline
+103
View File
@@ -256,6 +256,17 @@ where
Arc::downgrade(&connections),
"TCP port-forward connections",
));
if self.runtime_config.snapshot().services.proxy.force_smoltcp
&& bind_addr.is_ipv4()
&& bind_addr.ip().is_unspecified()
{
self.spawn_data_plane_tcp_port_forward(
bind_addr.port(),
dst_addr,
cancel.clone(),
connections.clone(),
);
}
let host = self.host.clone();
self.tasks.lock().unwrap().spawn(async move {
let mut listener = Some(listener);
@@ -303,6 +314,98 @@ where
Ok(())
}
fn spawn_data_plane_tcp_port_forward(
&self,
local_port: u16,
dst_addr: SocketAddr,
cancel: CancellationToken,
connections: Arc<std::sync::Mutex<JoinSet<()>>>,
) {
let data_plane = self.data_plane.clone();
self.tasks.lock().unwrap().spawn(async move {
loop {
let mut listener = match select! {
biased;
_ = cancel.cancelled() => break,
result = data_plane.data_plane_tcp_bind(local_port, Duration::from_secs(10)) => result,
} {
Ok(listener) => listener,
Err(error) => {
tracing::error!(
?error,
local_port,
"data-plane TCP port-forward bind failed"
);
select! {
biased;
_ = cancel.cancelled() => break,
_ = crate::foundation::time::sleep(TCP_PORT_FORWARD_REBIND_DELAY) => continue,
}
}
};
tracing::info!(
?dst_addr,
local_addr = ?listener.local_addr(),
"data-plane TCP port-forward listener bound"
);
loop {
let accepted = select! {
biased;
_ = cancel.cancelled() => return,
result = listener.accept() => result,
};
let (mut incoming, source_addr) = match accepted {
Ok(accepted) => accepted,
Err(error) => {
tracing::error!(
?error,
local_port,
"data-plane TCP port-forward accept failed; rebinding"
);
break;
}
};
let data_plane = data_plane.clone();
connections.lock().unwrap().spawn(async move {
let options = DataPlaneTcpConnectOptions::gateway(
Duration::from_secs(10),
TcpSocketPurpose::PortForward,
source_addr,
);
let mut outgoing = match data_plane.connect_tcp(dst_addr, options).await {
Ok(stream) => stream,
Err(error) => {
tracing::error!(?error, ?dst_addr, "port-forward connect failed");
return;
}
};
match tokio::io::copy_bidirectional(&mut incoming, &mut outgoing).await {
Ok((from_client, from_server)) => tracing::info!(
?dst_addr,
from_client,
from_server,
"port-forward connection finished"
),
Err(error) => tracing::error!(
?error,
?dst_addr,
"port-forward connection failed"
),
}
});
}
drop(listener);
select! {
biased;
_ = cancel.cancelled() => break,
_ = crate::foundation::time::sleep(TCP_PORT_FORWARD_REBIND_DELAY) => {}
}
}
});
}
async fn add_udp_port_forward(&self, cfg: &PortForwardConfig) -> anyhow::Result<()> {
let (bind_addr, dst_addr) = (cfg.bind_addr, cfg.dst_addr);
let forward = cfg.clone();