feat(mini): add compact native EasyTier client (#2479)

Add a native EasyTier proof-of-concept binary with TCP and UDP
transports, TUN, UDP hole punching, AES-GCM, and a read-only RPC
portal.

Introduce a release-derived mini profile and musl linker policy so
x86_64, big-endian MIPS, and little-endian MIPS stay below the strict
5,000,000-byte target without UPX.
This commit is contained in:
KKRainbow
2026-08-09 19:43:30 +08:00
committed by GitHub
parent 1e40350c89
commit d375d7e455
47 changed files with 1688 additions and 302 deletions
+2 -1
View File
@@ -121,7 +121,8 @@ extended-services = [
"wrapped-transport",
"proxy-cidr-monitor",
]
management = ["management-rpc", "config-write", "extended-services", "rich-config-errors", "easytier-proto/json-rpc"]
web-client = ["management-rpc", "config-write"]
management = ["web-client", "extended-services", "rich-config-errors", "easytier-proto/json-rpc"]
management-rpc = ["easytier-proto/api"]
proxy-cidr-monitor = []
rich-config-errors = ["dep:ariadne"]
+2 -2
View File
@@ -1,8 +1,8 @@
//! Static configuration schema plus the live runtime configuration store.
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub mod api;
#[cfg(any(feature = "management", feature = "browser-config"))]
#[cfg(any(feature = "web-client", feature = "browser-config"))]
pub mod api_input;
#[cfg(all(
feature = "browser-config",
+17
View File
@@ -95,6 +95,23 @@ impl CoreRuntimeConfigStore {
.send_modify(|version| *version += 1);
}
pub(crate) fn replace_with_current(
&self,
mut config: CoreInstanceRuntimeConfig,
merge: impl FnOnce(&CoreInstanceRuntimeConfig, &mut CoreInstanceRuntimeConfig),
) -> Arc<CoreInstanceRuntimeConfig> {
let _update = self.inner.update.lock();
let current = self.inner.snapshot.load_full();
merge(&current, &mut config);
let config = Arc::new(config);
self.inner.snapshot.store(config.clone());
self.inner.peer_changes.send_modify(|version| *version += 1);
self.inner
.service_changes
.send_modify(|version| *version += 1);
config
}
pub fn update_services(&self, update: impl FnOnce(&mut CoreRuntimeConfig)) {
let _update = self.inner.update.lock();
let mut config = self.inner.snapshot.load_full().as_ref().clone();
+1 -1
View File
@@ -624,7 +624,7 @@ impl TomlConfig {
}
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
mod snapshot;
impl ConfigLoader for TomlConfig {
+230 -24
View File
@@ -4,11 +4,11 @@ use std::collections::BTreeSet;
use crate::{
config::{
IpPrefix, NodeConfig, ProxyNetworkConfig, RouteConfig,
EncryptionAlgorithm, IpPrefix, NodeConfig, ProxyNetworkConfig, RouteConfig,
gateway::{GatewayRuntimeConfig, ProxyRuntimeConfig},
peers::{AclRuleConfig, HostRoutingPolicy, PublicIpv6ProviderConfig},
runtime::CoreRuntimeConfig,
toml::{ConfigLoader as _, TomlConfig},
toml::{ConfigLoader as _, Flags, TomlConfig},
},
connectivity::{
direct::DirectConnectorOptions,
@@ -16,13 +16,17 @@ use crate::{
stun::StunServerConfig,
},
listener::plan::ListenerRuntimeConfig,
packet::CompressorAlgo,
peers::{
context::PeerRuntimeSnapshotInput,
peer_manager::{PortablePeerManagerConfig, RouteAlgoType},
},
socket::{NetNamespace, SocketContext, tcp::TcpBindOptions, udp::UdpBindOptions},
tunnel::encrypt::algorithm_is_available,
};
use easytier_proto::common::CompressionAlgoPb;
use super::{CoreConnectivityConfig, CoreInstanceConfig};
const OSPF_UPDATE_MY_FOREIGN_NETWORK_INTERVAL_SEC: u64 = 10;
@@ -44,6 +48,15 @@ pub struct CoreInstanceHostConfig {
pub icmp_failure_is_fatal: bool,
pub public_ipv6_provider_supported: bool,
pub gateway_enabled: bool,
pub proxy_enabled: bool,
pub vpn_portal_enabled: bool,
pub magic_dns_enabled: bool,
pub kcp_enabled: bool,
pub quic_enabled: bool,
pub udp_broadcast_enabled: bool,
pub upnp_enabled: bool,
pub tcp_hole_punching_enabled: bool,
pub ignore_unsupported_config: bool,
pub easytier_version: String,
pub endpoint_protocols: Vec<String>,
}
@@ -60,12 +73,87 @@ impl Default for CoreInstanceHostConfig {
icmp_failure_is_fatal: false,
public_ipv6_provider_supported: false,
gateway_enabled: true,
proxy_enabled: true,
vpn_portal_enabled: true,
magic_dns_enabled: true,
kcp_enabled: true,
quic_enabled: true,
udp_broadcast_enabled: true,
upnp_enabled: true,
tcp_hole_punching_enabled: true,
ignore_unsupported_config: false,
easytier_version: env!("CARGO_PKG_VERSION").to_owned(),
endpoint_protocols: ManualEndpointDiscoveryConfig::default().srv_protocols,
}
}
}
impl CoreInstanceHostConfig {
pub(crate) fn accepts_runtime_url(&self, url: &url::Url) -> bool {
!self.ignore_unsupported_config
|| self
.endpoint_protocols
.iter()
.any(|scheme| scheme.eq_ignore_ascii_case(url.scheme()))
}
fn runtime_flags(&self, mut flags: Flags) -> Flags {
if !self.ignore_unsupported_config {
return flags;
}
if !self.smoltcp_available {
flags.no_tun = false;
flags.use_smoltcp = false;
}
if !self.proxy_enabled {
flags.enable_exit_node = false;
}
if !self.magic_dns_enabled {
flags.accept_dns = false;
}
if !self.kcp_enabled {
flags.enable_kcp_proxy = false;
flags.disable_kcp_input = true;
flags.disable_relay_kcp = true;
flags.enable_relay_foreign_network_kcp = false;
}
if !self.quic_enabled {
flags.enable_quic_proxy = false;
flags.disable_quic_input = true;
flags.disable_relay_quic = true;
flags.enable_relay_foreign_network_quic = false;
}
if !self.udp_broadcast_enabled {
flags.enable_udp_broadcast_relay = false;
}
if !self.upnp_enabled {
flags.disable_upnp = true;
}
if !self.tcp_hole_punching_enabled {
flags.disable_tcp_hole_punching = true;
}
if CompressionAlgoPb::try_from(flags.data_compress_algo)
.ok()
.and_then(|algorithm| CompressorAlgo::try_from(algorithm).ok())
.is_some_and(|algorithm| !algorithm.is_available())
{
flags.data_compress_algo = CompressionAlgoPb::None as i32;
}
if flags
.encryption_algorithm
.parse::<EncryptionAlgorithm>()
.is_ok_and(|algorithm| !algorithm_is_available(algorithm))
&& algorithm_is_available(EncryptionAlgorithm::AesGcm)
{
flags.encryption_algorithm = EncryptionAlgorithm::AesGcm.to_string();
}
flags
}
}
impl CoreInstanceConfig {
/// Normalizes the complete shared TOML model using OS-independent defaults.
///
@@ -81,7 +169,7 @@ impl CoreInstanceConfig {
config: &TomlConfig,
host: &CoreInstanceHostConfig,
) -> anyhow::Result<Self> {
let flags = config.get_flags();
let flags = host.runtime_flags(config.get_flags());
let instance_id = config.get_id();
let identity: crate::config::NetworkIdentity = config.get_network_identity().into();
let network_name = identity.network_name.clone();
@@ -93,6 +181,16 @@ impl CoreInstanceConfig {
_ => host.hostname_fallback.clone().unwrap_or_default(),
};
let acl = config.get_acl();
let peers = config
.get_peers()
.into_iter()
.filter(|peer| host.accepts_runtime_url(&peer.uri))
.collect::<Vec<_>>();
let proxy_networks = if host.ignore_unsupported_config && !host.proxy_enabled {
Vec::new()
} else {
config.get_proxy_cidrs()
};
let peer_snapshot =
crate::config::peers::PeerRuntimeSnapshot::from_host_input(PeerRuntimeSnapshotInput {
@@ -111,8 +209,7 @@ impl CoreInstanceConfig {
address: value.address().into(),
prefix_len: value.network_length(),
}),
proxy_networks: config
.get_proxy_cidrs()
proxy_networks: proxy_networks
.into_iter()
.map(|proxy| ProxyNetworkConfig {
real: IpPrefix {
@@ -134,12 +231,13 @@ impl CoreInstanceConfig {
host_routing: host.host_routing,
acl: acl.clone(),
easytier_version: host.easytier_version.clone(),
vpn_portal_cidr: config
.get_vpn_portal_config()
vpn_portal_cidr: (!host.ignore_unsupported_config || host.vpn_portal_enabled)
.then(|| config.get_vpn_portal_config())
.flatten()
.map(|portal| portal.client_cidr),
pinned_peers: config
.get_peers()
.into_iter()
pinned_peers: peers
.iter()
.cloned()
.map(|peer| (peer.uri, peer.peer_public_key))
.collect(),
ospf_update_my_foreign_network_interval_sec:
@@ -151,19 +249,28 @@ impl CoreInstanceConfig {
let peer = PortablePeerManagerConfig {
snapshot: peer_snapshot,
route_algo: RouteAlgoType::Ospf,
exit_nodes: config.get_exit_nodes(),
foreign_context_default_flags: TomlConfig::default().get_flags(),
exit_nodes: if host.ignore_unsupported_config && !host.proxy_enabled {
Vec::new()
} else {
config.get_exit_nodes()
},
foreign_context_default_flags: host.runtime_flags(TomlConfig::default().get_flags()),
};
let tcp_bind = TcpBindOptions::default().with_context(socket_context.clone());
let udp_bind = UdpBindOptions::direct_connect().with_context(socket_context.clone());
let listeners = Some(ListenerRuntimeConfig::new(
config.get_listener_uris(),
config
.get_listener_uris()
.into_iter()
.filter(|url| host.accepts_runtime_url(url))
.collect(),
flags.enable_ipv6,
socket_context.clone(),
));
let socks5_bind = config
.get_socks5_portal()
let socks5_bind = (!host.ignore_unsupported_config || host.gateway_enabled)
.then(|| config.get_socks5_portal())
.flatten()
.map(|url| {
let host = url
.host_str()
@@ -186,7 +293,11 @@ impl CoreInstanceConfig {
dhcp_ipv4: config.get_dhcp(),
gateway: GatewayRuntimeConfig {
socks5_bind,
port_forwards: config.get_port_forwards(),
port_forwards: if host.ignore_unsupported_config && !host.gateway_enabled {
Vec::new()
} else {
config.get_port_forwards()
},
},
manual_routes: config
.get_routes()
@@ -200,10 +311,15 @@ impl CoreInstanceConfig {
icmp_failure_is_fatal: host.icmp_failure_is_fatal,
udp_response_ipv4_mtu: 1280,
},
public_ipv6_auto: config.get_ipv6_public_addr_auto(),
public_ipv6_auto: config.get_ipv6_public_addr_auto()
&& (!host.ignore_unsupported_config || host.public_ipv6_provider_supported),
public_ipv6_provider: PublicIpv6ProviderConfig {
provider_enabled: config.get_ipv6_public_addr_provider(),
configured_prefix: config.get_ipv6_public_addr_prefix(),
provider_enabled: config.get_ipv6_public_addr_provider()
&& (!host.ignore_unsupported_config || host.public_ipv6_provider_supported),
configured_prefix: (!host.ignore_unsupported_config
|| host.public_ipv6_provider_supported)
.then(|| config.get_ipv6_public_addr_prefix())
.flatten(),
provider_supported: host.public_ipv6_provider_supported,
},
};
@@ -212,11 +328,7 @@ impl CoreInstanceConfig {
instance_name: config.get_inst_name(),
peer,
connectivity: CoreConnectivityConfig {
initial_peers: config
.get_peers()
.into_iter()
.map(|peer| peer.uri)
.collect(),
initial_peers: peers.into_iter().map(|peer| peer.uri).collect(),
listeners,
runtime,
startup_plan: super::CoreInstanceStartupPlan {
@@ -341,6 +453,7 @@ disable_p2p = true
gateway_enabled: false,
easytier_version: "host-version".to_owned(),
endpoint_protocols: vec!["host-protocol".to_owned()],
..Default::default()
};
let normalized = CoreInstanceConfig::from_toml_with_host(&config, &host).unwrap();
@@ -383,4 +496,97 @@ disable_p2p = true
["host-protocol"]
);
}
#[test]
fn ignored_capabilities_stay_in_toml_but_not_runtime_config() {
let config = TomlConfig::new_from_str(
r#"
listeners = ["tcp://127.0.0.1:11010", "quic://127.0.0.1:11011"]
proxy_network = [{ cidr = "10.20.0.0/16" }]
[[peer]]
uri = "tcp://127.0.0.1:11010"
[[peer]]
uri = "quic://127.0.0.1:11011"
[flags]
enable_exit_node = true
enable_kcp_proxy = true
accept_dns = true
encryption_algorithm = "chacha20"
data_compress_algo = "Zstd"
"#,
)
.unwrap();
config.set_exit_nodes(vec!["10.144.144.2".parse().unwrap()]);
config.set_ipv6_public_addr_provider(true);
config.get_id();
let before = config.dump();
let host = CoreInstanceHostConfig {
ignore_unsupported_config: true,
smoltcp_available: true,
proxy_enabled: false,
gateway_enabled: false,
public_ipv6_provider_supported: false,
magic_dns_enabled: false,
kcp_enabled: false,
quic_enabled: false,
endpoint_protocols: vec!["tcp".to_owned(), "udp".to_owned()],
..Default::default()
};
let normalized = CoreInstanceConfig::from_toml_with_host(&config, &host).unwrap();
assert_eq!(config.dump(), before);
assert_eq!(normalized.connectivity.initial_peers.len(), 1);
assert_eq!(
normalized
.connectivity
.listeners
.as_ref()
.unwrap()
.urls
.len(),
1
);
assert!(
normalized
.peer
.snapshot
.runtime
.core
.routes
.proxy_networks
.is_empty()
);
assert!(normalized.peer.exit_nodes.is_empty());
assert!(!normalized.connectivity.runtime.proxy.enable_exit_node);
assert!(
!normalized
.connectivity
.runtime
.public_ipv6_provider
.provider_enabled
);
let flags = &normalized.peer.snapshot.flags;
assert!(!flags.enable_kcp_proxy);
assert!(flags.disable_kcp_input);
assert!(!flags.accept_dns);
let expected_encryption = if algorithm_is_available(EncryptionAlgorithm::ChaCha20) {
EncryptionAlgorithm::ChaCha20
} else {
EncryptionAlgorithm::AesGcm
};
assert_eq!(flags.encryption_algorithm, expected_encryption.to_string());
assert_eq!(
flags.data_compress_algo,
if CompressorAlgo::ZstdDefault.is_available() {
CompressionAlgoPb::Zstd as i32
} else {
CompressionAlgoPb::None as i32
}
);
}
}
@@ -1,7 +1,3 @@
use std::sync::Arc;
use crate::config::runtime::CoreInstanceRuntimeConfig;
use super::{CoreInstance, CoreInstanceHost, CoreInstanceHostConfig};
impl<H> CoreInstance<H>
@@ -12,10 +8,6 @@ where
self.management.toml_config()
}
pub(crate) fn runtime_config_snapshot(&self) -> Arc<CoreInstanceRuntimeConfig> {
self.runtime_config.snapshot()
}
pub(crate) fn host_config(&self) -> &CoreInstanceHostConfig {
self.management.host_config()
}
@@ -1,9 +1,9 @@
use crate::{config::toml::TomlConfig, instance::CoreInstanceHostConfig};
pub(super) struct ManagementState {
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
toml_config: Option<TomlConfig>,
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
host_config: CoreInstanceHostConfig,
}
@@ -12,22 +12,22 @@ impl ManagementState {
toml_config: Option<TomlConfig>,
host_config: CoreInstanceHostConfig,
) -> Self {
#[cfg(not(feature = "management"))]
#[cfg(not(feature = "web-client"))]
let _ = (toml_config, host_config);
Self {
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
toml_config,
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
host_config,
}
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub(super) fn toml_config(&self) -> Option<TomlConfig> {
self.toml_config.clone()
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub(super) fn host_config(&self) -> &CoreInstanceHostConfig {
&self.host_config
}
+7 -7
View File
@@ -17,12 +17,12 @@ use uuid::Uuid;
use crate::config::toml::TomlConfig;
use crate::instance::{CoreInstance, CoreInstanceHost};
use crate::process_runtime::CoreProcessRuntime;
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
use crate::{
config::toml::{ConfigLoader as _, ConfigSource},
management::network_instance_running_info,
};
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
use easytier_proto::api::manage::NetworkInstanceRunningInfo;
/// Stable identity required by the instance collection.
@@ -439,25 +439,25 @@ where
}
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub fn config(&self, instance_id: Uuid) -> Option<TomlConfig> {
self.get(instance_id)
.and_then(|instance| instance.toml_config())
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub fn config_source(&self, instance_id: Uuid) -> Option<ConfigSource> {
self.config(instance_id)
.map(|config| config.get_network_config_source())
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub async fn network_info(&self, instance_id: Uuid) -> Option<NetworkInstanceRunningInfo> {
let instance = self.get(instance_id)?;
network_instance_running_info(instance.as_ref()).await.ok()
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub async fn collect_network_infos(
&self,
) -> anyhow::Result<std::collections::BTreeMap<Uuid, NetworkInstanceRunningInfo>> {
@@ -471,7 +471,7 @@ where
Ok(result)
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub fn collect_network_infos_sync(
&self,
) -> anyhow::Result<std::collections::BTreeMap<Uuid, NetworkInstanceRunningInfo>> {
+36 -12
View File
@@ -6,7 +6,7 @@ mod config;
mod data_plane_extension;
mod lifecycle;
mod management;
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
mod management_extension;
mod management_state;
pub mod manager;
@@ -214,6 +214,23 @@ fn retain_core_peer_identity(
peer.runtime.core.node.instance_id = instance_id;
}
fn retain_runtime_owned_peer_state(
current: &CoreInstanceRuntimeConfig,
next: &mut CoreInstanceRuntimeConfig,
peer_id: crate::config::PeerId,
) {
retain_core_peer_identity(
&mut next.peer,
peer_id,
current.peer.runtime.core.node.instance_id,
);
let next_peer = Arc::make_mut(&mut next.peer);
next_peer.runtime.stun_info = current.peer.runtime.stun_info.clone();
if current.services.dhcp_ipv4 && next.services.dhcp_ipv4 {
next_peer.runtime.core.routes.ipv4 = current.peer.runtime.core.routes.ipv4.clone();
}
}
/// Host-owned resources that must be prepared for the complete Instance
/// lifetime, such as a native packet interface.
#[async_trait::async_trait]
@@ -238,10 +255,15 @@ pub trait InstanceRuntimeHost: std::any::Any + Send + Sync + 'static {
/// Applies Host-side cached views of fields already committed to the
/// shared TOML model.
#[cfg(feature = "management")]
fn synchronize_config(&self, _patch: &crate::proto::api::config::InstanceConfigPatch) {}
#[cfg(feature = "web-client")]
fn synchronize_config(
&self,
_patch: &crate::proto::api::config::InstanceConfigPatch,
_config: &CoreInstanceRuntimeConfig,
) {
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
fn publish_config_patch(&self, _patch: crate::proto::api::config::InstanceConfigPatch) {}
fn attach_tun_fd(&self, _fd: i32) -> anyhow::Result<()> {
@@ -846,7 +868,7 @@ where
pub(crate) async fn update_runtime_config_under_operation(
&self,
mut config: CoreInstanceRuntimeConfig,
config: CoreInstanceRuntimeConfig,
) -> anyhow::Result<()> {
if matches!(
self.state(),
@@ -856,21 +878,23 @@ where
}
self.validate_runtime_config_capabilities(&config)?;
let current = self.runtime_config.snapshot();
retain_core_peer_identity(
&mut config.peer,
self.peer_id(),
current.peer.runtime.core.node.instance_id,
);
let refresh_acl_groups = current.peer.peer_group_memberships
!= config.peer.peer_group_memberships
|| current.peer.acl_group_declarations != config.peer.acl_group_declarations;
if current.services.acl != config.services.acl {
self.reload_acl_config_inner(&config.services.acl).await?;
}
// Foreign-network watchers read this state after the runtime-config
// notification, so publish it before replacing the watched snapshot.
self.sync_peer_runtime_state(&config.peer);
self.runtime_config.replace(config);
let peer_id = self.peer_id();
let published = self
.runtime_config
.replace_with_current(config, |current, next| {
retain_runtime_owned_peer_state(current, next, peer_id);
});
self.proxy_cidr_table
.update_snapshot(proxy_cidr_snapshot(self.runtime_config.snapshot().as_ref()));
.update_snapshot(proxy_cidr_snapshot(&published));
if refresh_acl_groups {
self.refresh_acl_groups().await;
}
+151 -4
View File
@@ -466,6 +466,57 @@ mod portable_runtime {
build_with_engines(config, WrappedTransportEngines::default())
}
#[cfg(feature = "dhcp-ipv4")]
#[tokio::test]
async fn runtime_update_preserves_dhcp_owned_ipv4() {
let mut initial = test_config("dhcp-runtime-update");
initial.connectivity.runtime.dhcp_ipv4 = true;
let instance = build_instance(initial).unwrap();
let lease = IpPrefix {
address: "10.126.126.7".parse().unwrap(),
prefix_len: 24,
};
instance.runtime_config.update_peer_with(|peer| {
peer.runtime.core.routes.ipv4 = Some(lease.clone());
});
let mut replacement = test_config("dhcp-runtime-update");
replacement.connectivity.runtime.dhcp_ipv4 = true;
instance
.update_runtime_config(runtime_snapshot(&replacement))
.await
.unwrap();
assert_eq!(
instance
.runtime_config
.snapshot()
.peer
.runtime
.core
.routes
.ipv4,
Some(lease)
);
let static_replacement = test_config("dhcp-runtime-update");
instance
.update_runtime_config(runtime_snapshot(&static_replacement))
.await
.unwrap();
assert_eq!(
instance
.runtime_config
.snapshot()
.peer
.runtime
.core
.routes
.ipv4,
None
);
}
#[tokio::test]
async fn core_instance_is_a_direct_managed_record() {
let instance = build_instance(test_config("managed-directly")).unwrap();
@@ -600,7 +651,7 @@ hostname = "core-owned-config"
response.config.unwrap().hostname.as_deref(),
Some("patched-in-core")
);
let runtime = instance.runtime_config_snapshot();
let runtime = instance.runtime_config.snapshot();
assert!(runtime.services.proxy.enable_exit_node);
assert!(runtime.services.public_ipv6_provider.provider_supported);
assert_eq!(runtime.peer.easytier_version, "host-version");
@@ -685,7 +736,8 @@ hostname = "core-owned-config"
);
assert!(
instance
.runtime_config_snapshot()
.runtime_config
.snapshot()
.services
.gateway
.port_forwards
@@ -694,6 +746,71 @@ hostname = "core-owned-config"
instance.stop().await;
}
#[cfg(feature = "web-client")]
#[tokio::test]
async fn ignored_gateway_patch_remains_in_toml_config() {
use easytier_proto::{
api::config::{ConfigPatchAction, InstanceConfigPatch, PortForwardPatch, UrlPatch},
common::{PortForwardConfigPb, SocketType},
};
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
let toml_config = crate::config::toml::TomlConfig::new_from_str(
"instance_name = \"ignored-gateway-patch\"",
)
.unwrap();
let mut host_adapters = adapters(None, Arc::new(packet_sink));
host_adapters.config.ignore_unsupported_config = true;
host_adapters.config.gateway_enabled = false;
host_adapters.config.endpoint_protocols = vec!["tcp".to_owned(), "udp".to_owned()];
let instance = CoreInstance::from_toml(toml_config, host_adapters).unwrap();
instance.start().await.unwrap();
crate::management::apply_config_patch(
&instance,
InstanceConfigPatch {
port_forwards: vec![PortForwardPatch {
action: ConfigPatchAction::Add as i32,
cfg: Some(PortForwardConfigPb {
bind_addr: Some(
"127.0.0.1:18080"
.parse::<std::net::SocketAddr>()
.unwrap()
.into(),
),
dst_addr: Some(
"10.144.144.2:8080"
.parse::<std::net::SocketAddr>()
.unwrap()
.into(),
),
socket_type: SocketType::Tcp as i32,
}),
}],
connectors: vec![UrlPatch {
action: ConfigPatchAction::Add as i32,
url: Some("quic://127.0.0.1:11010".parse::<url::Url>().unwrap().into()),
}],
..Default::default()
},
)
.await
.unwrap();
assert_eq!(instance.toml_config().unwrap().get_port_forwards().len(), 1);
assert!(
instance
.runtime_config
.snapshot()
.services
.gateway
.port_forwards
.is_empty()
);
assert!(instance.list_connectors().is_empty());
instance.stop().await;
}
#[tokio::test]
async fn dropping_core_instance_requests_host_shutdown() {
struct DropAwareRuntimeHost(Arc<AtomicBool>);
@@ -992,19 +1109,24 @@ hostname = "core-owned-config"
assert!(instances.instances().is_empty());
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
#[tokio::test]
async fn process_management_rpc_owns_instance_create_list_and_delete() {
use crate::{
config::toml::TomlConfig,
instance::manager::InstanceFactory,
management::{InstanceManager, ProcessManagementRpc, UnsupportedConfigFileStorage},
management::{
InstanceManager, ProcessManagementRpc, UnsupportedConfigFileStorage,
register_web_client_rpc,
},
rpc::service_registry::ServiceRegistry,
};
use easytier_proto::{
api::manage::{
DeleteNetworkInstanceRequest, ListNetworkInstanceRequest, NetworkConfig,
NetworkingMethod, RunNetworkInstanceRequest, WebClientService,
},
common::RpcDescriptor,
rpc_types::controller::BaseController,
};
@@ -1038,6 +1160,31 @@ hostname = "core-owned-config"
ManagementTestFactory(CoreProcessRuntime::new()),
Some(tokio::runtime::Handle::current()),
));
let registry = ServiceRegistry::new();
register_web_client_rpc(
instances.clone(),
&registry,
Arc::new(()),
Arc::new(UnsupportedConfigFileStorage),
);
assert_eq!(
registry.get_method_name(&RpcDescriptor {
domain_name: String::new(),
service_name: "ConfigRpc".to_owned(),
proto_name: "ConfigRpc".to_owned(),
method_index: 2,
}),
Some("get_config".to_owned())
);
assert_eq!(
registry.get_method_name(&RpcDescriptor {
domain_name: String::new(),
service_name: "WebClientService".to_owned(),
proto_name: "WebClientService".to_owned(),
method_index: 1,
}),
Some("validate_config".to_owned())
);
let rpc = ProcessManagementRpc::<ManagementTestFactory>::new(
instances.clone(),
Arc::new(()),
@@ -31,7 +31,8 @@ where
.toml_config()
.ok_or_else(|| anyhow::anyhow!("shared TOML configuration is not available"))?;
let candidate = config.detached_snapshot();
let parsed_prefix = validate_public_ipv6_patch(instance, &config, &patch)?;
let parsed_prefix =
parse_ipv6_public_addr_prefix_patch(patch.ipv6_public_addr_prefix.as_deref())?;
let patch_for_host = patch.clone();
// Preserve the existing ordered partial-commit contract: earlier valid
@@ -54,8 +55,11 @@ where
result?;
let result = patch_exit_nodes_config(&candidate, patch.exit_nodes);
validate_and_commit_candidate(instance, &config, &candidate)?;
instance.update_exit_nodes(result?).await;
let normalized = validate_and_commit_candidate(instance, &config, &candidate)?;
result?;
instance
.update_exit_nodes(normalized.peer.exit_nodes.clone())
.await;
let result = patch_mapped_listeners(&candidate, patch.mapped_listeners);
validate_and_commit_candidate(instance, &config, &candidate)?;
@@ -91,10 +95,11 @@ where
candidate.set_ipv6_public_addr_prefix(prefix);
provider_config_changed = true;
}
validate_and_commit_candidate(instance, &config, &candidate)?;
let normalized = validate_and_commit_candidate(instance, &config, &candidate)?;
let runtime = runtime_config_from_normalized(&normalized);
instance
.instance_runtime
.synchronize_config(&patch_for_host);
.synchronize_config(&patch_for_host, &runtime);
Ok(provider_config_changed)
}
.await;
@@ -106,9 +111,12 @@ where
instance
.instance_runtime
.publish_config_patch(patch_for_host);
#[cfg(feature = "public-ipv6-provider")]
if provider_config_changed && instance.state() == CoreInstanceState::Running {
instance.reconcile_public_ipv6_provider().await;
}
#[cfg(not(feature = "public-ipv6-provider"))]
let _ = provider_config_changed;
Ok(())
}
@@ -116,14 +124,16 @@ fn validate_and_commit_candidate<H>(
instance: &CoreInstance<H>,
shared: &TomlConfig,
candidate: &TomlConfig,
) -> anyhow::Result<()>
) -> anyhow::Result<CoreInstanceConfig>
where
H: CoreInstanceHost,
{
let runtime = runtime_config_from_toml(instance, candidate)?;
let normalized = CoreInstanceConfig::from_toml_with_host(candidate, instance.host_config())?;
let runtime = runtime_config_from_normalized(&normalized);
runtime.services.public_ipv6_provider.validate()?;
instance.validate_runtime_config_capabilities(&runtime)?;
shared.replace_from_snapshot(candidate);
Ok(())
Ok(normalized)
}
fn runtime_config_from_toml<H>(
@@ -134,15 +144,14 @@ where
H: CoreInstanceHost,
{
let normalized = CoreInstanceConfig::from_toml_with_host(config, instance.host_config())?;
let current = instance.runtime_config_snapshot();
let services = normalized.connectivity.runtime;
let mut peer = normalized.peer.snapshot;
peer.runtime.stun_info = current.peer.runtime.stun_info.clone();
Ok(runtime_config_from_normalized(&normalized))
}
Ok(CoreInstanceRuntimeConfig {
services,
peer: Arc::new(peer),
})
fn runtime_config_from_normalized(config: &CoreInstanceConfig) -> CoreInstanceRuntimeConfig {
CoreInstanceRuntimeConfig {
services: config.connectivity.runtime.clone(),
peer: Arc::new(config.peer.snapshot.clone()),
}
}
fn parse_ipv6_public_addr_prefix_patch(
@@ -160,34 +169,6 @@ fn parse_ipv6_public_addr_prefix_patch(
})?)))
}
fn validate_public_ipv6_patch<H>(
instance: &CoreInstance<H>,
config: &TomlConfig,
patch: &InstanceConfigPatch,
) -> anyhow::Result<Option<Option<cidr::Ipv6Cidr>>>
where
H: CoreInstanceHost,
{
let parsed_prefix =
parse_ipv6_public_addr_prefix_patch(patch.ipv6_public_addr_prefix.as_deref())?;
let provider_enabled = patch
.ipv6_public_addr_provider
.unwrap_or(config.get_ipv6_public_addr_provider());
let configured_prefix = parsed_prefix.unwrap_or_else(|| config.get_ipv6_public_addr_prefix());
let provider_supported = instance
.runtime_config_snapshot()
.services
.public_ipv6_provider
.provider_supported;
crate::config::peers::PublicIpv6ProviderConfig {
provider_enabled,
configured_prefix,
provider_supported,
}
.validate()?;
Ok(parsed_prefix)
}
fn trace_patchables<T: Debug>(patches: &[Patchable<T>]) {
for patch in patches {
match patch.action {
@@ -336,13 +317,25 @@ where
H: CoreInstanceHost,
{
for patch in patches {
let Some(url) = patch.url.map(Into::<url::Url>::into) else {
tracing::warn!("ignored connector patch without URL");
return Ok(());
};
match ConfigPatchAction::try_from(patch.action) {
Ok(ConfigPatchAction::Add) => instance.add_connector(url)?,
Ok(ConfigPatchAction::Add) => {
let Some(url) = patch.url.map(Into::<url::Url>::into) else {
tracing::warn!("ignored connector add without URL");
continue;
};
if !instance.host_config().accepts_runtime_url(&url) {
continue;
}
instance.add_connector(url)?;
}
Ok(ConfigPatchAction::Remove) => {
let Some(url) = patch.url.map(Into::<url::Url>::into) else {
tracing::warn!("ignored connector remove without URL");
continue;
};
if !instance.host_config().accepts_runtime_url(&url) {
continue;
}
if !instance.remove_connector(&url) {
anyhow::bail!("connector not found: {url}");
}
@@ -50,7 +50,10 @@ where
.map(Into::into)
.collect::<Vec<_>>();
let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone());
#[cfg(feature = "vpn-portal")]
let vpn_portal_cfg = Some(instance.vpn_portal_info().await.client_config);
#[cfg(not(feature = "vpn-portal"))]
let vpn_portal_cfg = Some(String::new());
let dev_name = instance
.toml_config()
.map(|config| config.get_flags().dev_name)
+34 -4
View File
@@ -1,19 +1,24 @@
#[cfg(feature = "management")]
mod compiled;
mod config_patch;
mod instance_info;
#[cfg(feature = "management")]
mod logger_rpc;
#[cfg(feature = "management")]
pub(super) mod packet_proxy;
mod process_rpc;
#[cfg(feature = "management")]
pub mod remote_client;
mod web_client;
use std::sync::Arc;
#[cfg(any(not(feature = "management"), test))]
use easytier_proto::api::config::ConfigRpcServer;
use easytier_proto::api::manage::WebClientServiceServer;
#[cfg(feature = "management")]
use easytier_proto::{
api::{
logger::{LoggerRpc, LoggerRpcServer},
manage::WebClientServiceServer,
},
api::logger::{LoggerRpc, LoggerRpcServer},
rpc_types::controller::BaseController,
};
@@ -30,9 +35,11 @@ use super::{
ConfigFileControl, ConfigFilePermission, DaemonGuard, resolve_optional_instance_by_name,
};
#[cfg(feature = "management")]
pub use compiled::register_instance_management_rpc;
pub use config_patch::apply_config_patch;
pub use instance_info::network_instance_running_info;
#[cfg(feature = "management")]
pub use logger_rpc::{
LoggerControl, LoggerManagementRpc, UnsupportedLoggerControl, log_level_name, parse_log_level,
};
@@ -44,6 +51,7 @@ pub use process_rpc::{
pub(crate) use web_client::WebClientBackend;
pub use web_client::{ConfigServerEndpoint, WebClient, WebClientConfig};
#[cfg(feature = "management")]
pub use super::instance_rpc::full::call_instance_json_rpc;
pub fn config_source_from_rpc(source: i32) -> Option<ConfigSource> {
@@ -62,6 +70,7 @@ pub fn config_source_to_rpc(source: ConfigSource) -> i32 {
}
/// Registers the complete process-level management surface once.
#[cfg(feature = "management")]
pub fn register_management_rpc<F, H>(
instances: Arc<InstanceManager<F>>,
registry: &ServiceRegistry,
@@ -81,6 +90,27 @@ pub fn register_management_rpc<F, H>(
);
}
/// Registers the compact reverse-RPC surface required by easytier-web.
#[cfg(any(not(feature = "management"), test))]
pub(crate) fn register_web_client_rpc<F, H>(
instances: Arc<InstanceManager<F>>,
registry: &ServiceRegistry,
hooks: Arc<dyn InstanceMutationHooks>,
storage: Arc<dyn ConfigFileStorage>,
) where
F: InstanceFactory<Instance = CoreInstance<H>, CreateContext = ()>,
F::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
H: CoreInstanceHost,
{
let config_rpc = super::instance_rpc::InstanceManagementRpc::<F>::new(instances.clone());
registry.register(ConfigRpcServer::new(config_rpc), "");
registry.register(
WebClientServiceServer::new(ProcessManagementRpc::<F>::new(instances, hooks, storage)),
"",
);
}
#[cfg(feature = "management")]
pub async fn call_management_json_rpc<F, H>(
manager: &Arc<InstanceManager<F>>,
logger: Arc<dyn LoggerControl>,
@@ -23,10 +23,11 @@ use crate::{
tunnel::{Tunnel, web_security},
};
use super::{
ConfigFileStorage, DaemonGuard, InstanceManager, InstanceMutationHooks, LoggerControl,
register_management_rpc,
};
#[cfg(not(feature = "management"))]
use super::register_web_client_rpc;
use super::{ConfigFileStorage, DaemonGuard, InstanceManager, InstanceMutationHooks};
#[cfg(feature = "management")]
use super::{LoggerControl, register_management_rpc};
const RETRY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
// Keep retry ownership in this loop when transport or protocol handshakes stall.
@@ -108,6 +109,7 @@ where
instances: Arc<InstanceManager<F>>,
hooks: Arc<dyn InstanceMutationHooks>,
storage: Arc<dyn ConfigFileStorage>,
#[cfg(feature = "management")]
logger: Arc<dyn LoggerControl>,
}
@@ -119,6 +121,7 @@ where
H: CoreInstanceHost,
{
fn register(&self, registry: &ServiceRegistry) {
#[cfg(feature = "management")]
register_management_rpc(
self.instances.clone(),
registry,
@@ -126,6 +129,13 @@ where
self.storage.clone(),
self.logger.clone(),
);
#[cfg(not(feature = "management"))]
register_web_client_rpc(
self.instances.clone(),
registry,
self.hooks.clone(),
self.storage.clone(),
);
}
async fn instance_ids(&self) -> anyhow::Result<Vec<uuid::Uuid>> {
@@ -159,13 +169,14 @@ where
instances: Arc<InstanceManager<F>>,
hooks: Arc<dyn InstanceMutationHooks>,
storage: Arc<dyn ConfigFileStorage>,
logger: Arc<dyn LoggerControl>,
#[cfg(feature = "management")] logger: Arc<dyn LoggerControl>,
) -> Self {
let manager_guard = instances.register_daemon();
let backend = Arc::new(NativeWebClientBackend {
instances,
hooks,
storage,
#[cfg(feature = "management")]
logger,
});
Self::start(connector, config, backend, Some(manager_guard))
@@ -0,0 +1,48 @@
use easytier_proto::{
api::config::{
ConfigRpc, GetConfigRequest, GetConfigResponse, PatchConfigRequest, PatchConfigResponse,
},
rpc_types::{self, controller::BaseController},
};
use crate::{
config::{api::network_config_from_toml, toml::ConfigLoader as _},
management::apply_config_patch,
};
use super::{ReadOnlyInstanceResolver, ResolvedInstanceManagementRpc};
#[async_trait::async_trait]
impl<R> ConfigRpc for ResolvedInstanceManagementRpc<R>
where
R: ReadOnlyInstanceResolver,
{
type Controller = BaseController;
async fn patch_config(
&self,
_: BaseController,
request: PatchConfigRequest,
) -> rpc_types::error::Result<PatchConfigResponse> {
let instance = self.instance(request.instance.as_ref())?;
if let Some(patch) = request.patch {
apply_config_patch(&instance, patch).await?;
}
Ok(PatchConfigResponse::default())
}
async fn get_config(
&self,
_: BaseController,
request: GetConfigRequest,
) -> rpc_types::error::Result<GetConfigResponse> {
let config = self
.instance(request.instance.as_ref())?
.toml_config()
.ok_or_else(|| anyhow::anyhow!("shared TOML configuration is not available"))?;
Ok(GetConfigResponse {
config: Some(network_config_from_toml(&config)),
toml_config: config.dump(),
})
}
}
@@ -2,9 +2,7 @@ use std::{sync::Arc, time::Duration};
use easytier_proto::{
api::{
config::{
ConfigRpc, GetConfigRequest, GetConfigResponse, PatchConfigRequest, PatchConfigResponse,
},
config::ConfigRpc,
instance::{
AclManageRpc, ConnectorManageRpc, CredentialInfo, CredentialManageRpc,
GenerateCredentialRequest, GenerateCredentialResponse, GetAclStatsRequest,
@@ -27,7 +25,7 @@ use easytier_proto::{
};
use crate::{
config::{api::network_config_from_toml, toml::ConfigLoader as _},
config::toml::ConfigLoader as _,
instance::{
CoreInstance, CoreInstanceHost,
manager::{InstanceFactory, InstanceManager},
@@ -35,11 +33,8 @@ use crate::{
peers::credential_manager::{CredentialCreateOptions, CredentialInfo as CoreCredentialInfo},
};
use super::{InstanceManagementRpc, ReadOnlyInstanceResolver, ResolvedInstanceManagementRpc};
use crate::management::{
full::{apply_config_patch, packet_proxy},
resolve_instance,
};
use super::InstanceManagementRpc;
use crate::management::{full::packet_proxy, resolve_instance};
/// Dispatches the JSON form of an Instance-targeted management RPC without
/// introducing a second, Host-owned set of service implementations.
@@ -369,38 +364,3 @@ where
Err(anyhow::anyhow!("not implemented for management API").into())
}
}
#[async_trait::async_trait]
impl<R> ConfigRpc for ResolvedInstanceManagementRpc<R>
where
R: ReadOnlyInstanceResolver,
{
type Controller = BaseController;
async fn patch_config(
&self,
_: BaseController,
request: PatchConfigRequest,
) -> rpc_types::error::Result<PatchConfigResponse> {
let instance = self.instance(request.instance.as_ref())?;
if let Some(patch) = request.patch {
apply_config_patch(&instance, patch).await?;
}
Ok(PatchConfigResponse::default())
}
async fn get_config(
&self,
_: BaseController,
request: GetConfigRequest,
) -> rpc_types::error::Result<GetConfigResponse> {
let config = self
.instance(request.instance.as_ref())?
.toml_config()
.ok_or_else(|| anyhow::anyhow!("shared TOML configuration is not available"))?;
Ok(GetConfigResponse {
config: Some(network_config_from_toml(&config)),
toml_config: config.dump(),
})
}
}
@@ -27,6 +27,8 @@ use crate::{
use super::resolve_instance;
#[cfg(feature = "web-client")]
mod config;
#[cfg(feature = "management")]
pub(super) mod full;
#[cfg(all(feature = "management", feature = "proxy-packet"))]
+13 -7
View File
@@ -2,7 +2,7 @@
#[cfg(all(feature = "management", any(test, target_os = "wasi")))]
mod forwarded_rpc;
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
mod full;
mod instance_rpc;
mod rpc_server_hook;
@@ -28,16 +28,22 @@ pub(crate) use forwarded_rpc::{
};
#[cfg(all(feature = "management", target_os = "wasi"))]
pub(crate) use full::WebClientBackend;
#[cfg(all(feature = "web-client", test))]
pub(crate) use full::register_web_client_rpc;
#[cfg(feature = "management")]
pub use full::remote_client;
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub use full::{
ConfigFileStorage, ConfigServerEndpoint, InstanceMutationHooks, InstanceMutationResult,
LoggerControl, LoggerManagementRpc, ProcessManagement, ProcessManagementRpc,
UnsupportedConfigFileStorage, UnsupportedLoggerControl, WebClient, WebClientConfig,
apply_config_patch, call_instance_json_rpc, call_management_json_rpc, config_source_from_rpc,
config_source_to_rpc, log_level_name, network_instance_running_info, parse_log_level,
register_instance_management_rpc, register_management_rpc,
ProcessManagement, ProcessManagementRpc, UnsupportedConfigFileStorage, WebClient,
WebClientConfig, apply_config_patch, config_source_from_rpc, config_source_to_rpc,
network_instance_running_info,
};
#[cfg(feature = "management")]
pub use full::{
LoggerControl, LoggerManagementRpc, UnsupportedLoggerControl, call_instance_json_rpc,
call_management_json_rpc, log_level_name, parse_log_level, register_instance_management_rpc,
register_management_rpc,
};
pub use instance_rpc::InstanceManagementRpc;
pub use rpc_server_hook::ManagementRpcServerHook;
+14 -2
View File
@@ -277,12 +277,24 @@ pub fn build_rpc_packet(args: BuildRpcPacketArgs<'_>) -> Vec<ZCPacket> {
ret
}
#[cfg(all(test, not(feature = "zstd")))]
#[cfg(test)]
mod tests {
use crate::proto::common::CompressionAlgoPb;
use super::{accepted_compression_algo, compress_packet};
use super::accepted_compression_algo;
#[cfg(not(feature = "zstd"))]
use super::compress_packet;
#[test]
fn accepted_compression_matches_build_capabilities() {
#[cfg(feature = "zstd")]
assert_eq!(accepted_compression_algo(), CompressionAlgoPb::Zstd);
#[cfg(not(feature = "zstd"))]
assert_eq!(accepted_compression_algo(), CompressionAlgoPb::None);
}
#[cfg(not(feature = "zstd"))]
#[tokio::test]
async fn compression_negotiation_falls_back_when_zstd_is_unavailable() {
assert_eq!(accepted_compression_algo(), CompressionAlgoPb::None);