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
+7
View File
@@ -21,3 +21,10 @@ operation transition.
Host capability operations use a separate seam. They turn Host readiness into
Rust task wakeups and do not share the caller-to-core broker state machine.
## Compact compatibility Host
A compact compatibility Host retains accepted values in the authoritative TOML
model for management readback, while the shared host-aware normalization path
omits capabilities that the compact runtime cannot execute. Omitted settings
are silent no-ops and must not be advertised as live network capabilities.
Generated
+9
View File
@@ -2528,6 +2528,15 @@ dependencies = [
"windows 0.52.0",
]
[[package]]
name = "easytier-mini"
version = "2.6.4"
dependencies = [
"anyhow",
"easytier",
"tokio",
]
[[package]]
name = "easytier-proto"
version = "2.6.4"
+6
View File
@@ -6,6 +6,7 @@ members = [
"easytier",
"easytier-gui/src-tauri",
"easytier-web",
"easytier-contrib/easytier-mini",
"easytier-contrib/easytier-ffi",
"easytier-contrib/easytier-uptime",
"easytier-contrib/easytier-android-jni",
@@ -29,3 +30,8 @@ lto = true
codegen-units = 1
opt-level = 3
strip = true
[profile.mini]
inherits = "release"
opt-level = "z"
strip = "symbols"
+15 -8
View File
@@ -217,7 +217,10 @@ are not independent owners of EasyTier peer state.
Each protocol registration must provide a coherent client/server Adapter.
Unavailable configured transports must be rejected during validation or
protocol selection, rather than silently falling back to another transport.
protocol selection in the standard runtime, rather than silently falling back
to another transport. A compact compatibility Host may instead retain the
desired value for management readback and omit it from normalized runtime
state; it must not advertise or partially activate the unavailable transport.
### Connectivity
@@ -283,8 +286,9 @@ Adapters.
Optional gateway capabilities are selected by cohesive Modules. Disabled
implementations retain stable lifecycle calls and report unsupported
configuration where a stable interface is required; they do not duplicate
portable policy.
configuration in the standard runtime. A compact compatibility Host may
silently normalize those settings to no-ops while preserving the desired TOML
model; disabled implementations do not duplicate portable policy.
The instance-scoped `DataPlaneSession` composes the foundation operation broker
under the same session lock as its resource and quota state. The broker owns
@@ -368,10 +372,12 @@ configuration, or connectivity state.
## Runtime configuration authority
`TomlConfig` is an owned construction input. After startup, it is not a second
mutable source of truth.
`TomlConfig` is the authoritative desired configuration used for management
readback and patch transactions. Compact Hosts keep unsupported accepted values
there so controllers observe the configuration they submitted.
The normalized core runtime store is authoritative for:
The separately typed, normalized core runtime store is authoritative for live
behavior:
- peer feature flags and routing policy;
- listeners and initial peers;
@@ -477,8 +483,9 @@ configuration, management, packet-plane, or test-support interfaces.
14. Unknown protobuf fields in reflected route information survive forwarding
and credential filtering.
15. Feature selection is localized at cohesive Module/Adapter boundaries.
16. Unsupported configured capabilities fail explicitly rather than changing
wire protocol or silently falling back.
16. The standard runtime rejects unsupported configured capabilities. Compact
compatibility Hosts may preserve them as runtime no-ops, but never change
wire protocol, advertise them, or silently fall back to an unsafe mode.
## Validation
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "easytier-mini"
description = "Minimal native EasyTier node with TCP/UDP tunnels, TUN and UDP hole punching."
version = "2.6.4"
edition.workspace = true
rust-version.workspace = true
license-file = "../../LICENSE"
build = "build.rs"
[dependencies]
anyhow = "1.0"
easytier = { path = "../../easytier", version = "2.6.4", default-features = false, features = [
"aes-gcm",
"dhcp-ipv4",
"logging",
"proxy-cidr-monitor",
"smoltcp",
"tun",
"web-client",
] }
tokio = { version = "1", default-features = false, features = ["macros", "rt", "signal"] }
+113
View File
@@ -0,0 +1,113 @@
# easytier-mini
`easytier-mini` is a native EasyTier POC binary. It shares EasyTier's TOML
configuration model, peer protocol, TCP/UDP tunnel implementations, TUN,
dynamic IPv4 allocation, the smoltcp userspace path and STUN/UDP hole-punching
core with the full binary. It includes AES-GCM so its default encryption
setting interoperates with the full binary's default configuration.
Build it with:
```sh
cargo build --release -p easytier-mini
```
For the static size target used by this POC:
```sh
cargo build --profile mini --target x86_64-unknown-linux-musl -p easytier-mini
```
MIPS targets use the repository's existing musl-cross toolchains. The helper
builds the standard library for size, applies immediate-abort only to the mini
MIPS target graph, and can build either or both byte orders:
```sh
./easytier-contrib/easytier-mini/build-mips.sh all
./easytier-contrib/easytier-mini/build-mips.sh mips
./easytier-contrib/easytier-mini/build-mips.sh mipsel
```
The `mini` profile derives from `release` and applies `opt-level=z` to the
entire compact binary dependency graph. Full EasyTier release builds retain
their normal `opt-level=3` profile. The musl builds use a mini-only static
linker policy to stay below 5,000,000 bytes on x86-64 and 5,500,000 bytes on
MIPS without UPX or another executable compressor. The compact x86-64 linker
policy retains static PIE, packs relative relocations and folds identical code.
MIPS builds omit standard-library backtrace support and use immediate abort;
normal workspace MIPS builds are not affected. Compact linker policies omit
unwind tables.
Start it with a normal EasyTier TOML file:
```sh
easytier-mini --config mini.toml
```
`-c` is accepted as the short form of `--config`.
Start it as an EasyTier Web managed node with a complete config-server URL:
```sh
easytier-mini --config-server udp://config-server.easytier.cn:22020/TOKEN
```
`--machine-id`, `--hostname`, and `--secure-mode` match the full client's Web
identity and transport options. `--config` and `--config-server` may be used
together: the local instance remains static while Web-owned instances are
created, updated, retained, and deleted independently.
The node also exposes the native EasyTier management RPC protocol on
`127.0.0.1:15888`, so the full `easytier-cli` can inspect it:
```sh
easytier-cli node info
easytier-cli peer
easytier-cli route
easytier-cli connector list
```
For example:
```toml
instance_name = "mini"
ipv4 = "10.147.0.2"
listeners = ["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"]
[network_identity]
network_name = "mini-poc"
network_secret = "change-me"
[[peer]]
uri = "tcp://example.net:11010"
```
Local TOML and Web configuration both retain the complete authoritative model.
The compact runtime silently omits unsupported capabilities while normalizing
that model into live runtime state. EasyTier Web therefore sees every accepted
configuration value unchanged and its consistency checks converge. This also
applies to hot patches: for example, a port-forward patch remains visible to
the controller while no port-forward service starts in mini. ChaCha20 falls
back to AES-GCM rather than plaintext.
The compact runtime supports `tcp://` and `udp://` listener, mapped-listener
and peer URLs. `no_tun = true` runs through smoltcp without an OS TUN device,
and `dhcp = true` allocates the virtual IPv4 address dynamically.
The mini feature set keeps STUN collection, UDP hole punching, Web heartbeats,
Web instance lifecycle management and the config hot-patch RPC. It omits TCP
hole punching, endpoint discovery (`http://`, `https://`, `txt://` and
`srv://` peers), protobuf reflection, logger control and the rest of the full
management surface. Unsupported connector URLs are accepted as no-ops. Its
local RPC surface remains read-only for node, peer, route and connector
queries. OSPF route messages keep their original protobuf wire data, so fields
added by future EasyTier versions are forwarded without requiring
`prost-reflect`.
For size, this POC reads one file directly and does not support configuration
from stdin or `${VAR}` expansion. It omits the process-management event journal,
while the console logger still reports runtime events such as peer, connection,
listener, TUN and DHCP changes. The RPC address is currently fixed, so only one
mini process can use the default portal on a host. The x86-64 musl POC cannot
provide reliable stack backtraces because its release binary has no unwind
tables.
+61
View File
@@ -0,0 +1,61 @@
#!/bin/sh
set -eu
# Cargo invokes this same file as a rustc wrapper during compact MIPS builds.
# Applying immediate-abort here keeps the size policy scoped to easytier-mini;
# normal MIPS builds elsewhere in the workspace retain their panic behavior.
if [ "${EASYTIER_MINI_MIPS_RUSTC_WRAPPER:-}" = "1" ]; then
mini_rustc=$1
shift
for mini_rustc_arg in "$@"; do
case "$mini_rustc_arg" in
mips-unknown-linux-musl|mipsel-unknown-linux-musl)
exec "$mini_rustc" "$@" \
-Zunstable-options \
-Cpanic=immediate-abort
;;
esac
done
exec "$mini_rustc" "$@"
fi
mini_script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
mini_repo_dir=$(CDPATH= cd -- "$mini_script_dir/../.." && pwd)
mini_requested_target=${1:-all}
cd "$mini_repo_dir"
build_mips_target() {
mini_target=$1
mini_toolchain=$2
PATH="$mini_repo_dir/musl_gcc/$mini_toolchain/bin:$PATH" \
EASYTIER_MINI_MIPS_RUSTC_WRAPPER=1 \
RUSTC_BOOTSTRAP=1 \
RUSTC_WRAPPER="$mini_script_dir/build-mips.sh" \
cargo build \
--manifest-path "$mini_repo_dir/Cargo.toml" \
--profile mini \
--target "$mini_target" \
-Z build-std=std \
-Z build-std-features=optimize_for_size \
-p easytier-mini
}
case "$mini_requested_target" in
all)
build_mips_target mips-unknown-linux-musl mips-unknown-linux-muslsf
build_mips_target mipsel-unknown-linux-musl mipsel-unknown-linux-muslsf
;;
mips|mips-unknown-linux-musl)
build_mips_target mips-unknown-linux-musl mips-unknown-linux-muslsf
;;
mipsel|mipsel-unknown-linux-musl)
build_mips_target mipsel-unknown-linux-musl mipsel-unknown-linux-muslsf
;;
-h|--help)
echo "usage: $0 [all|mips|mipsel]"
;;
*)
echo "unsupported MIPS target: $mini_requested_target" >&2
exit 2
;;
esac
+32
View File
@@ -0,0 +1,32 @@
use std::env;
use std::path::PathBuf;
fn main() {
let target = env::var("TARGET").unwrap_or_default();
let profile = env::var("PROFILE").unwrap_or_default();
if !matches!(profile.as_str(), "release" | "mini")
|| !matches!(
target.as_str(),
"x86_64-unknown-linux-musl" | "mips-unknown-linux-musl" | "mipsel-unknown-linux-musl"
)
{
return;
}
let script =
PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("easytier-mini-musl.ld");
println!("cargo:rerun-if-changed={}", script.display());
// The release-derived mini profile already aborts panics. Keep the compact
// binary's linker policy local so full EasyTier musl builds retain their
// normal PIE/unwind settings.
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--build-id=none");
if target == "x86_64-unknown-linux-musl" {
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--pack-dyn-relocs=relr");
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--icf=all");
}
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--no-eh-frame-hdr");
println!(
"cargo:rustc-link-arg-bin=easytier-mini=-Wl,-T,{}",
script.display()
);
}
@@ -0,0 +1,14 @@
SECTIONS
{
.eh_frame :
{
KEEP(*crtbegin.o(.eh_frame))
KEEP(*crtend.o(.eh_frame))
}
/DISCARD/ :
{
*(EXCLUDE_FILE (*crtbegin.o *crtend.o) .eh_frame)
*(.eh_frame_hdr)
}
}
INSERT AFTER .data;
+264
View File
@@ -0,0 +1,264 @@
use std::{ffi::OsString, path::PathBuf, sync::Arc};
use anyhow::Context as _;
use easytier::common::MachineIdOptions;
use easytier::{
common::config::{ConfigFileControl, load_toml_config_from_path},
instance::factory::native_compact_instance_manager_with_runtime,
rpc_service::ReadOnlyApiRpcServer,
web_client::{WebClientHooks, parse_config_server_endpoint, run_web_client},
};
enum Command {
Run(RunOptions),
Exit,
}
#[derive(Debug, Default, PartialEq, Eq)]
struct RunOptions {
config: Option<PathBuf>,
config_server: Option<String>,
machine_id: Option<String>,
hostname: Option<String>,
secure_mode: bool,
}
const USAGE: &str = "usage: easytier-mini [--config <FILE>] [--config-server <URL>] \
[--machine-id <ID>] [--hostname <NAME>] [--secure-mode]";
fn required_value(
args: &mut impl Iterator<Item = OsString>,
option: &str,
) -> anyhow::Result<OsString> {
args.next()
.with_context(|| format!("{option} requires a value"))
}
fn parse_args(mut args: impl Iterator<Item = OsString>) -> anyhow::Result<Command> {
let mut options = RunOptions::default();
while let Some(arg) = args.next() {
if arg == "-h" || arg == "--help" {
println!(
"easytier-mini {}\n\nUsage: {USAGE}",
env!("CARGO_PKG_VERSION")
);
return Ok(Command::Exit);
}
if arg == "-V" || arg == "--version" {
println!("easytier-mini {}", env!("CARGO_PKG_VERSION"));
return Ok(Command::Exit);
}
if arg == "-c" || arg == "--config" {
if options.config.is_some() {
anyhow::bail!("--config may only be specified once");
}
options.config = Some(PathBuf::from(required_value(&mut args, "--config")?));
continue;
}
if arg == "-w" || arg == "--config-server" {
if options.config_server.is_some() {
anyhow::bail!("--config-server may only be specified once");
}
options.config_server = Some(
required_value(&mut args, "--config-server")?
.into_string()
.map_err(|_| anyhow::anyhow!("--config-server must be valid UTF-8"))?,
);
continue;
}
if arg == "--machine-id" {
options.machine_id = Some(
required_value(&mut args, "--machine-id")?
.into_string()
.map_err(|_| anyhow::anyhow!("--machine-id must be valid UTF-8"))?,
);
continue;
}
if arg == "--hostname" {
options.hostname = Some(
required_value(&mut args, "--hostname")?
.into_string()
.map_err(|_| anyhow::anyhow!("--hostname must be valid UTF-8"))?,
);
continue;
}
if arg == "--secure-mode" {
options.secure_mode = true;
continue;
}
anyhow::bail!("unknown argument {arg:?}; {USAGE}");
}
if options.config.is_none() && options.config_server.is_none() {
anyhow::bail!("either --config or --config-server is required; {USAGE}");
}
Ok(Command::Run(options))
}
fn require_tcp_or_udp(scheme: &str, source: &str) -> anyhow::Result<()> {
match scheme {
"tcp" | "udp" => Ok(()),
scheme => anyhow::bail!(
"{source} uses unsupported tunnel scheme {scheme:?}; easytier-mini supports only tcp:// and udp://"
),
}
}
fn validate_config_server(config_server: &str) -> anyhow::Result<()> {
let endpoint = parse_config_server_endpoint(config_server)?;
require_tcp_or_udp(endpoint.connect_url().scheme(), "config server")
}
struct MiniWebClientHooks;
impl WebClientHooks for MiniWebClientHooks {
fn manages_remote_config_instances(&self) -> bool {
true
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let Command::Run(options) = parse_args(std::env::args_os().skip(1))? else {
return Ok(());
};
easytier::common::log::init_console()?;
let local_config = options
.config
.as_ref()
.map(|config_path| {
load_toml_config_from_path(config_path)
.with_context(|| format!("failed to load {}", config_path.display()))
})
.transpose()?;
if let Some(config_server) = options.config_server.as_deref() {
validate_config_server(config_server)?;
}
let instances = Arc::new(native_compact_instance_manager_with_runtime(
tokio::runtime::Handle::current(),
));
let local_instance_id = local_config
.map(|config| instances.run_network_instance(config, ConfigFileControl::STATIC_CONFIG))
.transpose()?;
let _web_client = if let Some(config_server) = options.config_server.as_deref() {
Some(
run_web_client(
config_server,
MachineIdOptions {
explicit_machine_id: options.machine_id,
state_dir: None,
},
options.hostname,
options.secure_mode,
instances.clone(),
Some(Arc::new(MiniWebClientHooks)),
)
.await?,
)
} else {
None
};
let _rpc_server =
ReadOnlyApiRpcServer::new(Some("127.0.0.1:15888".to_owned()), None, instances.clone())?
.serve()
.await?;
eprintln!(
"easytier-mini started: local={local_instance_id:?}, web={}; RPC: 127.0.0.1:15888",
options.config_server.is_some()
);
let stopped_unexpectedly = tokio::select! {
signal = tokio::signal::ctrl_c() => {
signal.context("failed to listen for Ctrl-C")?;
false
},
_ = instances.wait() => true,
};
for instance in instances.instances() {
instance.stop().await;
}
if stopped_unexpectedly {
anyhow::bail!("EasyTier instance stopped unexpectedly");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use easytier::common::config::{ConfigLoader as _, TomlConfigLoader};
#[test]
fn parses_minimal_config_argument() {
let Command::Run(options) =
parse_args([OsString::from("--config"), OsString::from("mini.toml")].into_iter())
.unwrap()
else {
panic!("expected run command");
};
assert_eq!(options.config, Some(PathBuf::from("mini.toml")));
}
#[test]
fn parses_web_client_arguments_without_a_local_config() {
let Command::Run(options) = parse_args(
[
OsString::from("--config-server"),
OsString::from("token"),
OsString::from("--machine-id"),
OsString::from("machine"),
OsString::from("--hostname"),
OsString::from("mini"),
OsString::from("--secure-mode"),
]
.into_iter(),
)
.unwrap() else {
panic!("expected run command");
};
assert_eq!(options.config_server.as_deref(), Some("token"));
assert_eq!(options.machine_id.as_deref(), Some("machine"));
assert_eq!(options.hostname.as_deref(), Some("mini"));
assert!(options.secure_mode);
}
#[test]
fn rejects_unknown_arguments() {
let result = parse_args([OsString::from("extra")].into_iter());
assert!(result.is_err());
}
#[test]
fn accepts_tcp_udp_config_server() {
assert!(validate_config_server("udp://127.0.0.1:22020/token").is_ok());
assert!(validate_config_server("quic://127.0.0.1:22020/token").is_err());
}
#[tokio::test]
async fn compact_factory_accepts_unsupported_config_without_changing_it() {
let config = TomlConfigLoader::new_from_str(
r#"
dhcp = true
listeners = ["quic://127.0.0.1:11010"]
proxy_network = [{ cidr = "10.20.0.0/16" }]
[flags]
encryption_algorithm = "chacha20"
data_compress_algo = "Zstd"
"#,
)
.unwrap();
config.get_id();
let before = config.dump();
let manager =
native_compact_instance_manager_with_runtime(tokio::runtime::Handle::current());
let instance = manager.create(config, ()).unwrap();
assert_eq!(instance.toml_config().unwrap().dump(), before);
}
}
+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);
+70 -1
View File
@@ -385,7 +385,10 @@ mod tests {
use axum::{Json, Router, extract::State, routing::post};
use easytier::{
common::{MachineIdOptions, config::NetworkConfigExt},
instance::factory::{NativeInstanceManager, native_instance_manager},
instance::factory::{
NativeInstanceManager, native_compact_instance_manager_with_runtime,
native_instance_manager,
},
proto::{
api::manage::{NetworkConfig, NetworkingMethod, PortForwardConfig},
common::CompressionAlgoPb,
@@ -901,6 +904,72 @@ mod tests {
println!("{:?}", mgr);
}
#[tokio::test]
async fn compact_runtime_preserves_unsupported_web_config_during_hot_patch() {
let (webhook_config, webhook_server, _) = test_webhook_config().await;
let mut mgr = ClientManager::new(
Db::memory_db().await,
None,
Duration::ZERO,
Arc::new(FeatureFlags::default()),
webhook_config,
);
let config_server_addr = add_random_udp_listener(&mut mgr).await;
let machine_id = uuid::Uuid::new_v4();
let instance_id = uuid::Uuid::new_v4();
let core_manager = Arc::new(native_compact_instance_manager_with_runtime(
tokio::runtime::Handle::current(),
));
let _client =
start_web_client_for_test(config_server_addr, machine_id, core_manager.clone()).await;
let user_id = wait_for_validated_user(&mgr, machine_id).await;
let desired = updated_managed_network_config(instance_id);
let mut initial = desired.clone();
initial["port_forwards"] = json!([]);
mgr.reconcile_managed_network_configs(
user_id,
machine_id,
vec![managed_config(instance_id, initial)],
Some("compact-initial".to_string()),
None,
)
.await
.unwrap();
wait_for_runtime_config(&core_manager, instance_id, |config| {
config.network_name.as_deref() == Some("managed-updated")
&& config.port_forwards.is_empty()
})
.await;
mgr.reconcile_managed_network_configs(
user_id,
machine_id,
vec![managed_config(instance_id, desired)],
Some("compact-patched".to_string()),
Some("compact-initial".to_string()),
)
.await
.unwrap();
let patched = wait_for_runtime_config(&core_manager, instance_id, |config| {
config.network_name.as_deref() == Some("managed-updated")
&& config.port_forwards.len() == 1
})
.await;
assert_updated_runtime_config(&patched, instance_id);
assert_eq!(
mgr.db()
.get_managed_config_revision((user_id, machine_id))
.await
.unwrap()
.as_deref(),
Some("compact-patched")
);
webhook_server.abort();
}
#[tokio::test]
async fn managed_web_config_revision_updates_running_core_config() {
let (webhook_config, webhook_server, _) = test_webhook_config().await;
+2 -1
View File
@@ -413,14 +413,15 @@ extended-services = [
"proxy-cidr-monitor",
]
management = [
"web-client",
"logging",
"management-rpc",
"easytier-core/management",
"easytier-proto/json-rpc",
"easytier-proto/utils",
"tokio/full",
]
management-rpc = ["easytier-core/management-rpc"]
web-client = ["management-rpc", "easytier-core/web-client"]
tcp-hole-punch = ["easytier-core/tcp-hole-punch"]
# Deprecated: hotpath profiling has been removed. These feature aliases are
# retained as no-ops so existing build scripts using `--features hotpath*`
+145 -27
View File
@@ -6,11 +6,14 @@ use std::{
use arc_swap::ArcSwap;
use async_trait::async_trait;
use easytier_core::config::PeerId;
use easytier_core::connectivity::composite::ConnectorRuntime as _;
use easytier_core::peers::public_ipv6::PublicIpv6Host;
use easytier_core::socket::{NetNamespace, SocketContext};
use easytier_core::tunnel::effective_encryption_uses_xor;
use easytier_core::{
config::{PeerId, peers::PeerRuntimeSnapshot, runtime::CoreInstanceRuntimeConfig},
instance::{CoreInstanceConfig, CoreInstanceHostConfig},
};
use super::{
config::{ConfigLoader, Flags, NetworkIdentity},
@@ -85,11 +88,13 @@ pub struct GlobalCtx {
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>,
vpn_portal_cidr: AtomicCell<Option<cidr::Ipv4Cidr>>,
hostname: Mutex<String>,
tun_device_name: Mutex<Option<String>>,
flags: ArcSwap<Flags>,
runtime_endpoint_protocols: Option<HashSet<String>>,
}
impl std::fmt::Debug for GlobalCtx {
@@ -113,7 +118,7 @@ impl PublicIpv6Host for GlobalCtx {
prefix: cidr::Ipv6Cidr,
) -> HashSet<Ipv6Addr> {
let context = SocketContext::default()
.with_socket_mark(self.config.get_flags().socket_mark)
.with_socket_mark(self.get_flags().socket_mark)
.with_netns(self.net_ns.name().map(NetNamespace::new));
let ip_list = crate::host_runtime::native_host_runtime()
.collect_ip_addrs(&context)
@@ -139,14 +144,57 @@ impl PublicIpv6Host for GlobalCtx {
impl GlobalCtx {
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
Self::new_inner(config_fs, None, None)
}
pub(crate) fn new_with_runtime_config(
config_fs: impl ConfigLoader + 'static,
runtime: &CoreInstanceConfig,
host: &CoreInstanceHostConfig,
) -> Self {
let runtime = CoreInstanceRuntimeConfig {
services: runtime.connectivity.runtime.clone(),
peer: Arc::new(runtime.peer.snapshot.clone()),
};
let protocols = host.ignore_unsupported_config.then(|| {
host.endpoint_protocols
.iter()
.map(|protocol| protocol.to_ascii_lowercase())
.collect()
});
Self::new_inner(config_fs, Some(&runtime), protocols)
}
fn new_inner(
config_fs: impl ConfigLoader + 'static,
runtime: Option<&CoreInstanceRuntimeConfig>,
runtime_endpoint_protocols: Option<HashSet<String>>,
) -> Self {
let id = config_fs.get_id();
let network = config_fs.get_network_identity();
let net_ns = NetNS::new(config_fs.get_netns());
let hostname = match config_fs.get_hostname() {
hostname if !hostname.is_empty() => hostname,
_ => gethostname::gethostname().to_string_lossy().to_string(),
};
let flags = config_fs.get_flags();
let hostname = runtime
.and_then(|runtime| runtime.peer.runtime.core.node.hostname.clone())
.unwrap_or_else(|| match config_fs.get_hostname() {
hostname if !hostname.is_empty() => hostname,
_ => gethostname::gethostname().to_string_lossy().to_string(),
});
let flags = runtime
.map(|runtime| runtime.peer.flags.clone())
.unwrap_or_else(|| config_fs.get_flags());
let ipv4 = runtime
.map(|runtime| Self::runtime_ipv4(&runtime.peer))
.unwrap_or_else(|| config_fs.get_ipv4());
let ipv6 = runtime
.map(|runtime| Self::runtime_ipv6(&runtime.peer))
.unwrap_or_else(|| config_fs.get_ipv6());
let vpn_portal_cidr = runtime
.map(|runtime| runtime.peer.vpn_portal_cidr)
.unwrap_or_else(|| {
config_fs
.get_vpn_portal_config()
.map(|config| config.client_cidr)
});
if flags.enable_encryption && effective_encryption_uses_xor(&flags.encryption_algorithm) {
tracing::warn!("using insecure XOR because no AEAD encryption is configured");
}
@@ -160,16 +208,34 @@ impl GlobalCtx {
network,
event_bus,
cached_ipv4: AtomicCell::new(None),
cached_ipv6: AtomicCell::new(None),
cached_ipv4: AtomicCell::new(ipv4),
cached_ipv6: AtomicCell::new(ipv6),
vpn_portal_cidr: AtomicCell::new(vpn_portal_cidr),
hostname: Mutex::new(hostname),
tun_device_name: Mutex::new(None),
flags: ArcSwap::new(Arc::new(flags)),
runtime_endpoint_protocols,
}
}
pub(crate) fn runtime_ipv4(peer: &PeerRuntimeSnapshot) -> Option<cidr::Ipv4Inet> {
let prefix = peer.runtime.core.routes.ipv4.as_ref()?;
let IpAddr::V4(address) = prefix.address else {
return None;
};
cidr::Ipv4Inet::new(address, prefix.prefix_len).ok()
}
pub(crate) fn runtime_ipv6(peer: &PeerRuntimeSnapshot) -> Option<cidr::Ipv6Inet> {
let prefix = peer.runtime.core.routes.ipv6.as_ref()?;
let IpAddr::V6(address) = prefix.address else {
return None;
};
cidr::Ipv6Inet::new(address, prefix.prefix_len).ok()
}
pub fn subscribe(&self) -> EventBusSubscriber {
self.event_bus.subscribe()
}
@@ -207,31 +273,19 @@ impl GlobalCtx {
}
pub fn get_ipv4(&self) -> Option<cidr::Ipv4Inet> {
if let Some(ret) = self.cached_ipv4.load() {
return Some(ret);
}
let addr = self.config.get_ipv4();
self.cached_ipv4.store(addr);
addr
self.cached_ipv4.load()
}
pub fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>) {
self.config.set_ipv4(addr);
self.cached_ipv4.store(None);
self.cached_ipv4.store(addr);
}
pub fn get_ipv6(&self) -> Option<cidr::Ipv6Inet> {
if let Some(ret) = self.cached_ipv6.load() {
return Some(ret);
}
let addr = self.config.get_ipv6();
self.cached_ipv6.store(addr);
addr
self.cached_ipv6.load()
}
pub fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>) {
self.config.set_ipv6(addr);
self.cached_ipv6.store(None);
self.cached_ipv6.store(addr);
}
pub fn is_ip_local_ipv6(&self, ip: &std::net::Ipv6Addr) -> bool {
@@ -273,7 +327,7 @@ impl GlobalCtx {
}
pub fn get_vpn_portal_cidr(&self) -> Option<cidr::Ipv4Cidr> {
self.config.get_vpn_portal_config().map(|x| x.client_cidr)
self.vpn_portal_cidr.load()
}
pub fn get_flags(&self) -> Flags {
@@ -281,7 +335,6 @@ impl GlobalCtx {
}
pub fn set_flags(&self, flags: Flags) {
self.config.set_flags(flags.clone());
self.flags.store(Arc::new(flags));
}
@@ -300,6 +353,17 @@ impl GlobalCtx {
pub fn no_tun(&self) -> bool {
self.flags.load().no_tun
}
pub fn runtime_mapped_listeners(&self) -> Vec<url::Url> {
let listeners = self.config.get_mapped_listeners();
let Some(protocols) = &self.runtime_endpoint_protocols else {
return listeners;
};
listeners
.into_iter()
.filter(|listener| protocols.contains(&listener.scheme().to_ascii_lowercase()))
.collect()
}
}
#[cfg(test)]
@@ -377,6 +441,60 @@ pub mod tests {
assert!(!config.dump().contains("hostname"));
}
#[test]
fn active_dhcp_ipv4_survives_declarative_config_replacement() {
let config = TomlConfigLoader::default();
config.set_dhcp(true);
let global_ctx = GlobalCtx::new(config.clone());
let lease = "10.144.144.7/24".parse().unwrap();
global_ctx.set_ipv4(Some(lease));
config.set_ipv4(None);
assert_eq!(global_ctx.get_ipv4(), Some(lease));
}
#[test]
fn runtime_state_does_not_rewrite_toml_config() {
let config = TomlConfigLoader::default();
let global_ctx = GlobalCtx::new(config.clone());
let mut runtime_flags = global_ctx.get_flags();
runtime_flags.enable_exit_node = true;
global_ctx.set_ipv4(Some("10.144.144.7/24".parse().unwrap()));
global_ctx.set_ipv6(Some("fd00::7/64".parse().unwrap()));
global_ctx.set_flags(runtime_flags);
assert_eq!(config.get_ipv4(), None);
assert_eq!(config.get_ipv6(), None);
assert!(!config.get_flags().enable_exit_node);
assert_eq!(
global_ctx.get_ipv4(),
Some("10.144.144.7/24".parse().unwrap())
);
assert_eq!(global_ctx.get_ipv6(), Some("fd00::7/64".parse().unwrap()));
assert!(global_ctx.get_flags().enable_exit_node);
}
#[test]
fn compact_runtime_does_not_advertise_unsupported_mapped_listeners() {
let config = TomlConfigLoader::default();
config.set_mapped_listeners(Some(vec![
"tcp://127.0.0.1:11010".parse().unwrap(),
"quic://127.0.0.1:11011".parse().unwrap(),
]));
let host = crate::instance::config::compact_runtime_core_host_config();
let normalized =
easytier_core::instance::CoreInstanceConfig::from_toml_with_host(&config, &host)
.unwrap();
let global_ctx = GlobalCtx::new_with_runtime_config(config.clone(), &normalized, &host);
assert_eq!(config.get_mapped_listeners().len(), 2);
assert_eq!(global_ctx.runtime_mapped_listeners().len(), 1);
assert_eq!(global_ctx.runtime_mapped_listeners()[0].scheme(), "tcp");
}
pub fn get_mock_global_ctx_with_network(
network_identy: Option<NetworkIdentity>,
) -> ArcGlobalCtx {
+56 -20
View File
@@ -6,15 +6,14 @@ use easytier_core::gateway::proxy::wrapped_transport::WrappedTransportEngines;
use easytier_core::gateway::vpn_portal::VpnPortalHost;
#[cfg(test)]
use easytier_core::host::packet::{HostPacket, PacketSink};
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
use easytier_core::{
connectivity::manual::ManualTunnelConnector,
host::dns::{DnsRecordResolver, DnsResolver},
instance::CoreInstanceConfig,
};
use easytier_core::{
events::{CoreEvent, CoreEventSink},
instance::{CoreHostAdapters, CoreInstance, PacketEgressHost},
instance::{CoreHostAdapters, CoreInstance, CoreInstanceConfig, PacketEgressHost},
process_runtime::CoreProcessRuntime,
};
@@ -25,7 +24,9 @@ use crate::{
common::global_ctx::ArcGlobalCtx,
common::{config::TomlConfig, global_ctx::GlobalCtx},
host_runtime::native_host_runtime,
instance::config::{runtime_core_host_config, runtime_peer_credential_storage},
instance::config::{
compact_runtime_core_host_config, runtime_core_host_config, runtime_peer_credential_storage,
},
instance::listeners::RuntimeExternalListenerFactory,
instance::runtime_host::NativeInstanceRuntimeHost,
};
@@ -42,18 +43,30 @@ use easytier_core::gateway::proxy::wrapped_transport::WrappedTransportEngine;
pub(crate) type NativeCoreInstance = CoreInstance<NativeInstanceHost>;
pub(crate) fn compose_native_core_instance(
config: TomlConfig,
toml_config: TomlConfig,
process_runtime: Arc<CoreProcessRuntime>,
compact_runtime: bool,
) -> anyhow::Result<Arc<NativeCoreInstance>> {
let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
let host_config = if compact_runtime {
compact_runtime_core_host_config()
} else {
runtime_core_host_config()
};
let normalized = CoreInstanceConfig::from_toml_with_host(&toml_config, &host_config)?;
let global_ctx = Arc::new(GlobalCtx::new_with_runtime_config(
toml_config.clone(),
&normalized,
&host_config,
));
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone());
let mut adapters = runtime_core_host_adapters_with_packet_egress(
let mut adapters = runtime_core_host_adapters_with_packet_egress_and_config(
global_ctx.clone(),
process_runtime,
runtime_host.clone(),
host_config,
);
adapters.instance_runtime = runtime_host;
NativeCoreInstance::from_toml(config, adapters)
NativeCoreInstance::from_toml(toml_config, adapters)
}
impl CoreEventSink for GlobalCtx {
@@ -139,13 +152,19 @@ impl CoreEventSink for GlobalCtx {
}
#[cfg(feature = "wrapped-transport")]
fn runtime_wrapped_transport_engines() -> WrappedTransportEngines {
fn runtime_wrapped_transport_engines(
config: &easytier_core::instance::CoreInstanceHostConfig,
) -> WrappedTransportEngines {
#[cfg(feature = "kcp")]
let kcp = Some(Arc::new(KcpProxyService::new()) as Arc<dyn WrappedTransportEngine>);
let kcp = config
.kcp_enabled
.then(|| Arc::new(KcpProxyService::new()) as Arc<dyn WrappedTransportEngine>);
#[cfg(not(feature = "kcp"))]
let kcp = None;
#[cfg(feature = "quic")]
let quic = Some(Arc::new(QuicProxyService::new()) as Arc<dyn WrappedTransportEngine>);
let quic = config
.quic_enabled
.then(|| Arc::new(QuicProxyService::new()) as Arc<dyn WrappedTransportEngine>);
#[cfg(not(feature = "quic"))]
let quic = None;
@@ -161,9 +180,10 @@ pub(crate) fn runtime_core_host_adapters(
let host = native_instance_host(global_ctx.clone());
let runtime_dns = native_host_runtime();
let adapters = CoreHostAdapters::new(host, runtime_dns, packet_sink, process_runtime);
configure_runtime_core_host_adapters(global_ctx, adapters)
configure_runtime_core_host_adapters(global_ctx, adapters, runtime_core_host_config())
}
#[cfg(test)]
pub(crate) fn runtime_core_host_adapters_with_packet_egress(
global_ctx: ArcGlobalCtx,
process_runtime: Arc<CoreProcessRuntime>,
@@ -173,29 +193,45 @@ pub(crate) fn runtime_core_host_adapters_with_packet_egress(
let runtime_dns = native_host_runtime();
let adapters =
CoreHostAdapters::new_with_packet_egress(host, runtime_dns, packet_egress, process_runtime);
configure_runtime_core_host_adapters(global_ctx, adapters)
configure_runtime_core_host_adapters(global_ctx, adapters, runtime_core_host_config())
}
fn runtime_core_host_adapters_with_packet_egress_and_config(
global_ctx: ArcGlobalCtx,
process_runtime: Arc<CoreProcessRuntime>,
packet_egress: Arc<dyn PacketEgressHost>,
host_config: easytier_core::instance::CoreInstanceHostConfig,
) -> CoreHostAdapters<NativeInstanceHost> {
let host = native_instance_host(global_ctx.clone());
let runtime_dns = native_host_runtime();
let adapters =
CoreHostAdapters::new_with_packet_egress(host, runtime_dns, packet_egress, process_runtime);
configure_runtime_core_host_adapters(global_ctx, adapters, host_config)
}
fn configure_runtime_core_host_adapters(
global_ctx: ArcGlobalCtx,
mut adapters: CoreHostAdapters<NativeInstanceHost>,
host_config: easytier_core::instance::CoreInstanceHostConfig,
) -> CoreHostAdapters<NativeInstanceHost> {
#[cfg(test)]
adapters.replace_stun_provider(Arc::new(crate::common::stun::MockStunInfoCollector {
udp_nat_type: crate::proto::common::NatType::Unknown,
}));
adapters.config = runtime_core_host_config();
adapters.credential_storage = runtime_peer_credential_storage(&global_ctx);
adapters.config = host_config.clone();
adapters.credential_storage = (!host_config.ignore_unsupported_config)
.then(|| runtime_peer_credential_storage(&global_ctx))
.flatten();
adapters.events = global_ctx.clone();
#[cfg(feature = "wrapped-transport")]
{
adapters.wrapped_transports = runtime_wrapped_transport_engines();
adapters.wrapped_transports = runtime_wrapped_transport_engines(&host_config);
}
adapters.protocol = Some(runtime_client_protocol_upgrader(global_ctx.clone()));
adapters.external_listener_factory = Some(Arc::new(RuntimeExternalListenerFactory));
adapters.server_protocol = Some(runtime_server_protocol_upgrader(global_ctx.clone()));
#[cfg(feature = "upnp")]
{
if host_config.upnp_enabled {
adapters.udp_hole_punch_platform = Some(
crate::instance::udp_hole_punch::runtime_udp_hole_punch_platform(
global_ctx.net_ns.clone(),
@@ -211,12 +247,12 @@ fn configure_runtime_core_host_adapters(
adapters.proxy_cidr_monitor_enabled = true;
}
#[cfg(feature = "public-ipv6-provider")]
{
if host_config.public_ipv6_provider_supported {
adapters.public_ipv6_host = Some(global_ctx.clone());
adapters.public_ipv6_provider = Some(runtime_public_ipv6_provider_platform(&global_ctx));
}
#[cfg(feature = "wireguard")]
{
if host_config.vpn_portal_enabled {
use crate::common::config::ConfigLoader as _;
adapters.vpn_portal = Some(crate::vpn_portal::wireguard::WireGuardPortalHost::new(
@@ -230,7 +266,7 @@ fn configure_runtime_core_host_adapters(
adapters
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub(crate) fn runtime_one_shot_manual_connector(
global_ctx: ArcGlobalCtx,
config: &TomlConfig,
+36 -2
View File
@@ -44,13 +44,44 @@ pub(crate) fn runtime_core_host_config() -> CoreInstanceHostConfig {
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))),
public_ipv6_provider_supported: cfg!(target_os = "linux"),
public_ipv6_provider_supported: cfg!(all(
target_os = "linux",
feature = "public-ipv6-provider"
)),
gateway_enabled: cfg!(feature = "socks5"),
proxy_enabled: cfg!(any(feature = "kcp", feature = "quic")),
vpn_portal_enabled: cfg!(feature = "wireguard"),
magic_dns_enabled: cfg!(feature = "magic-dns"),
kcp_enabled: cfg!(feature = "kcp"),
quic_enabled: cfg!(feature = "quic"),
udp_broadcast_enabled: cfg!(all(target_os = "windows", feature = "tun")),
upnp_enabled: cfg!(feature = "upnp"),
tcp_hole_punching_enabled: cfg!(feature = "tcp-hole-punch"),
ignore_unsupported_config: false,
easytier_version: EASYTIER_VERSION.to_owned(),
endpoint_protocols: IpScheme::VARIANTS.iter().map(ToString::to_string).collect(),
}
}
pub(crate) fn compact_runtime_core_host_config() -> CoreInstanceHostConfig {
let mut config = runtime_core_host_config();
config.force_exit_node = false;
config.host_routing.local_exit_node_fallback = false;
config.public_ipv6_provider_supported = false;
config.gateway_enabled = false;
config.proxy_enabled = false;
config.vpn_portal_enabled = false;
config.magic_dns_enabled = false;
config.kcp_enabled = false;
config.quic_enabled = false;
config.udp_broadcast_enabled = false;
config.upnp_enabled = false;
config.tcp_hole_punching_enabled = false;
config.ignore_unsupported_config = true;
config.endpoint_protocols = vec!["tcp".to_owned(), "udp".to_owned()];
config
}
pub(crate) fn runtime_peer_credential_storage(
global_ctx: &ArcGlobalCtx,
) -> Option<Arc<dyn CredentialStorage>> {
@@ -71,6 +102,9 @@ pub(crate) fn test_core_instance_config(
let config = TomlConfig::new_from_str(&global_ctx.config.dump())
.expect("test configuration should round-trip through TOML");
config.set_ipv4(global_ctx.get_ipv4());
config.set_ipv6(global_ctx.get_ipv6());
config.set_flags(global_ctx.get_flags());
let mut host = runtime_core_host_config();
let hostname = global_ctx.get_hostname();
host.hostname_fallback = (!hostname.is_empty()).then_some(hostname);
@@ -113,7 +147,7 @@ mod tests {
assert_eq!(config.smoltcp_available, cfg!(feature = "smoltcp"));
assert_eq!(
config.public_ipv6_provider_supported,
cfg!(target_os = "linux")
cfg!(all(target_os = "linux", feature = "public-ipv6-provider"))
);
assert_eq!(config.easytier_version, EASYTIER_VERSION);
}
@@ -403,7 +403,7 @@ impl MagicDnsServerInstance {
rpc_server.set_hook(data.clone());
// Use configured tld_dns_zone or fall back to DEFAULT_ET_DNS_ZONE if empty
let flags = global_ctx.config.get_flags();
let flags = global_ctx.get_flags();
let tld_dns_zone_clone = flags.tld_dns_zone.clone();
data.update_dns_records(std::iter::empty(), &tld_dns_zone_clone)
+29 -5
View File
@@ -58,6 +58,19 @@ pub fn native_instance_manager_with_runtime(
native_instance_manager_with_optional_runtime(Some(runtime_handle))
}
#[cfg(feature = "management-rpc")]
pub fn native_compact_instance_manager_with_runtime(
runtime_handle: tokio::runtime::Handle,
) -> NativeInstanceManager {
let process_runtime = CoreProcessRuntime::new();
let factory = NativeInstanceFactory::new(process_runtime)
.with_runtime_handle(Some(runtime_handle.clone()))
.with_compact_runtime();
#[cfg(feature = "logging")]
let factory = factory.with_cli_event_logging();
InstanceManager::new(factory, Some(runtime_handle))
}
#[cfg(feature = "management")]
pub fn native_process_management(
instances: Arc<NativeInstanceManager>,
@@ -85,7 +98,8 @@ fn native_instance_manager_with_optional_runtime(
pub struct NativeInstanceFactory {
process_runtime: Arc<CoreProcessRuntime>,
runtime_handle: Option<tokio::runtime::Handle>,
#[cfg(feature = "management")]
compact_runtime: bool,
#[cfg(feature = "logging")]
log_cli_events: bool,
}
@@ -94,12 +108,13 @@ impl NativeInstanceFactory {
Self {
process_runtime,
runtime_handle: None,
#[cfg(feature = "management")]
compact_runtime: false,
#[cfg(feature = "logging")]
log_cli_events: false,
}
}
#[cfg(feature = "management")]
#[cfg(feature = "logging")]
fn with_cli_event_logging(mut self) -> Self {
self.log_cli_events = true;
self
@@ -110,6 +125,11 @@ impl NativeInstanceFactory {
self.runtime_handle = runtime_handle;
self
}
fn with_compact_runtime(mut self) -> Self {
self.compact_runtime = true;
self
}
}
impl InstanceFactory for NativeInstanceFactory {
@@ -126,8 +146,12 @@ impl InstanceFactory for NativeInstanceFactory {
.runtime_handle
.as_ref()
.map(tokio::runtime::Handle::enter);
let instance = compose_native_core_instance(config, self.process_runtime.clone())?;
#[cfg(feature = "management")]
let instance = compose_native_core_instance(
config,
self.process_runtime.clone(),
self.compact_runtime,
)?;
#[cfg(feature = "logging")]
if self.log_cli_events {
let events = subscribe_native_instance_event(&instance)
.ok_or_else(|| anyhow::anyhow!("native instance runtime host is unavailable"))?;
+2 -2
View File
@@ -25,7 +25,7 @@ pub struct NativeInstanceEnvironment {
impl NativeInstanceEnvironment {
fn new(global_ctx: ArcGlobalCtx, runtime: Arc<NativeHostRuntime>) -> Self {
let socket_context = SocketContext::default()
.with_socket_mark(global_ctx.config.get_flags().socket_mark)
.with_socket_mark(global_ctx.get_flags().socket_mark)
.with_netns(global_ctx.net_ns.name().map(NetNamespace::new));
Self {
global_ctx,
@@ -49,7 +49,7 @@ impl ConnectorEnvironment for NativeInstanceEnvironment {
}
fn mapped_listeners(&self) -> Vec<url::Url> {
self.global_ctx.config.get_mapped_listeners()
self.global_ctx.runtime_mapped_listeners()
}
fn is_local_ip(&self, ip: &IpAddr) -> bool {
+1 -1
View File
@@ -1,4 +1,4 @@
#[cfg(feature = "management")]
#[cfg(feature = "logging")]
pub(crate) mod cli_event_logger;
pub(crate) mod composition;
pub(crate) mod config;
+110 -1
View File
@@ -1,7 +1,8 @@
use std::sync::Arc;
use easytier_core::{
gateway::dhcp::DhcpIpv4Host, host::packet::HostPacketReceiver, instance::CorePacketPlane,
config::runtime::CoreInstanceRuntimeConfig, gateway::dhcp::DhcpIpv4Host,
host::packet::HostPacketReceiver, instance::CorePacketPlane,
};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
@@ -77,6 +78,41 @@ impl NativeInstanceRuntimeHost {
self.event_journal.events()
}
#[cfg(feature = "web-client")]
fn synchronize_global_ctx_config(
&self,
patch: &crate::proto::api::config::InstanceConfigPatch,
config: &CoreInstanceRuntimeConfig,
) {
if patch.hostname.is_some() {
self.global_ctx.set_hostname(
config
.peer
.runtime
.core
.node
.hostname
.clone()
.unwrap_or_default(),
);
}
if patch.ipv4.is_some() && !config.services.dhcp_ipv4 {
self.global_ctx
.set_ipv4(crate::common::global_ctx::GlobalCtx::runtime_ipv4(
&config.peer,
));
}
if patch.ipv6.is_some() {
self.global_ctx
.set_ipv6(crate::common::global_ctx::GlobalCtx::runtime_ipv6(
&config.peer,
));
}
if patch.disable_relay_data.is_some() {
self.global_ctx.set_flags(config.peer.flags.clone());
}
}
pub(crate) fn subscribe_event(&self) -> crate::common::global_ctx::EventBusSubscriber {
self.global_ctx.subscribe()
}
@@ -98,6 +134,15 @@ mod tests {
global_ctx::{GlobalCtx, GlobalCtxEvent},
};
#[cfg(feature = "web-client")]
fn runtime_config(config: &TomlConfig) -> CoreInstanceRuntimeConfig {
let normalized = easytier_core::instance::CoreInstanceConfig::from_toml(config).unwrap();
CoreInstanceRuntimeConfig {
services: normalized.connectivity.runtime,
peer: Arc::new(normalized.peer.snapshot),
}
}
#[test]
fn runtime_host_owns_event_subscription_context() {
let global_ctx = Arc::new(GlobalCtx::new(TomlConfig::default()));
@@ -111,4 +156,68 @@ mod tests {
GlobalCtxEvent::CredentialChanged
);
}
#[cfg(feature = "web-client")]
#[test]
fn runtime_host_synchronizes_normalized_config_without_management() {
use easytier_core::{config::toml::ConfigLoader as _, instance::InstanceRuntimeHost as _};
let config = TomlConfig::default();
config.set_hostname(Some("before".to_owned()));
config.set_ipv4(Some("10.20.0.1/24".parse().unwrap()));
config.set_ipv6(Some("fd00::1/64".parse().unwrap()));
let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone());
assert_eq!(global_ctx.get_hostname(), "before");
assert_eq!(global_ctx.get_ipv4(), Some("10.20.0.1/24".parse().unwrap()));
assert_eq!(global_ctx.get_ipv6(), Some("fd00::1/64".parse().unwrap()));
assert!(!global_ctx.get_flags().disable_relay_data);
config.set_hostname(Some("after".to_owned()));
config.set_ipv4(Some("10.20.0.2/24".parse().unwrap()));
config.set_ipv6(Some("fd00::2/64".parse().unwrap()));
let mut flags = config.get_flags();
flags.disable_relay_data = true;
config.set_flags(flags);
runtime_host.synchronize_config(
&crate::proto::api::config::InstanceConfigPatch {
hostname: Some("ignored-raw-hostname".to_owned()),
ipv4: Some("10.99.0.1/24".parse::<cidr::Ipv4Inet>().unwrap().into()),
ipv6: Some("fd99::1/64".parse::<cidr::Ipv6Inet>().unwrap().into()),
disable_relay_data: Some(false),
..Default::default()
},
&runtime_config(&config),
);
assert_eq!(global_ctx.get_hostname(), "after");
assert_eq!(global_ctx.get_ipv4(), Some("10.20.0.2/24".parse().unwrap()));
assert_eq!(global_ctx.get_ipv6(), Some("fd00::2/64".parse().unwrap()));
assert!(global_ctx.get_flags().disable_relay_data);
}
#[cfg(feature = "web-client")]
#[test]
fn runtime_host_preserves_dhcp_ipv4_during_config_synchronization() {
use easytier_core::{config::toml::ConfigLoader as _, instance::InstanceRuntimeHost as _};
let config = TomlConfig::default();
config.set_dhcp(true);
let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone());
let lease = "10.20.0.7/24".parse().unwrap();
global_ctx.set_ipv4(Some(lease));
config.set_ipv4(None);
runtime_host.synchronize_config(
&crate::proto::api::config::InstanceConfigPatch {
ipv4: Some("10.99.0.1/24".parse::<cidr::Ipv4Inet>().unwrap().into()),
..Default::default()
},
&runtime_config(&config),
);
assert_eq!(global_ctx.get_ipv4(), Some(lease));
}
}
@@ -85,28 +85,6 @@ impl EventJournal {
self.events.read().unwrap().iter().cloned().collect()
}
pub(super) fn synchronize_config(
&self,
patch: &crate::proto::api::config::InstanceConfigPatch,
) {
if let Some(hostname) = &patch.hostname {
self.global_ctx.set_hostname(hostname.clone());
}
if let Some(ipv4) = patch.ipv4.as_ref()
&& !self.global_ctx.config.get_dhcp()
{
self.global_ctx.set_ipv4(Some((*ipv4).into()));
}
if let Some(ipv6) = patch.ipv6.as_ref() {
self.global_ctx.set_ipv6(Some((*ipv6).into()));
}
if let Some(disable_relay_data) = patch.disable_relay_data {
let mut flags = self.global_ctx.get_flags();
flags.disable_relay_data = disable_relay_data;
self.global_ctx.set_flags(flags);
}
}
pub(super) fn publish_config_patch(
&self,
patch: crate::proto::api::config::InstanceConfigPatch,
@@ -29,14 +29,21 @@ impl InstanceRuntimeHost for NativeInstanceRuntimeHost {
self.management_events_snapshot()
}
#[cfg(feature = "management")]
fn synchronize_config(&self, patch: &crate::proto::api::config::InstanceConfigPatch) {
self.event_journal.synchronize_config(patch);
#[cfg(feature = "web-client")]
fn synchronize_config(
&self,
patch: &crate::proto::api::config::InstanceConfigPatch,
config: &easytier_core::config::runtime::CoreInstanceRuntimeConfig,
) {
self.synchronize_global_ctx_config(patch, config);
}
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
fn publish_config_patch(&self, patch: crate::proto::api::config::InstanceConfigPatch) {
#[cfg(feature = "management")]
self.event_journal.publish_config_patch(patch);
#[cfg(not(feature = "management"))]
let _ = patch;
}
fn attach_tun_fd(&self, fd: i32) -> anyhow::Result<()> {
@@ -5,10 +5,7 @@ use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle};
use crate::common::global_ctx::ArcGlobalCtx;
#[cfg(feature = "magic-dns")]
use crate::{
common::config::ConfigLoader as _,
instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner},
};
use crate::instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
#[derive(Default)]
pub(super) struct MagicDnsRuntime {
@@ -30,7 +27,7 @@ impl MagicDnsRuntime {
tun_dev: Option<String>,
tun_ip: Ipv4Inet,
) -> Self {
let active = global_ctx.config.get_flags().accept_dns.then(|| {
let active = global_ctx.get_flags().accept_dns.then(|| {
let mut runner = DnsRunner::new(
packet_plane,
global_ctx,
@@ -17,7 +17,6 @@ use tokio_util::sync::CancellationToken;
use super::{MagicDnsRuntime, tun_common::TunNicState};
use crate::{
common::{
config::ConfigLoader as _,
error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
},
@@ -133,7 +132,7 @@ impl NativeTunRuntime {
pub(super) async fn prepare(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
self.nic.drain().await;
if !self.global_ctx.config.get_flags().no_tun {
if !self.global_ctx.get_flags().no_tun {
self.start_static_ip(packet_plane).await?;
}
Ok(())
+1 -1
View File
@@ -663,7 +663,7 @@ impl VirtualNic {
let dev = AsyncDevice::new(dev)?;
let flags = self.global_ctx.config.get_flags();
let flags = self.global_ctx.get_flags();
let mut mtu_in_config = flags.mtu;
if flags.enable_encryption {
mtu_in_config -= 20;
+1 -1
View File
@@ -20,7 +20,7 @@ pub mod service_manager;
pub(crate) mod socket;
pub mod tunnel;
pub mod utils;
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub mod web_client;
#[cfg(test)]
+1 -1
View File
@@ -1,5 +1,5 @@
pub use easytier_proto::api;
#[cfg(feature = "management")]
#[cfg(feature = "web-client")]
pub use easytier_proto::web;
pub use easytier_proto::{
ALL_DESCRIPTOR_BYTES, acl, common, core_config, error, peer_rpc, rpc_types,
+48 -20
View File
@@ -3,6 +3,7 @@ use std::sync::Arc;
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use easytier_core::{
config::toml::ConfigLoader as _,
connectivity::{manual::ManualTunnelConnector, protocol::raw::TunnelDialer},
management::{ConfigServerEndpoint, WebClientConfig},
socket::IpVersion,
@@ -10,18 +11,21 @@ use easytier_core::{
};
use url::Url;
#[cfg(feature = "management")]
use crate::{
common::os_info::collect_device_os_info, instance::config_storage::NativeConfigFileStorage,
rpc_service::logger::NativeLoggerControl,
};
use crate::{
common::{
MachineIdOptions, config::TomlConfigLoader, constants::EASYTIER_VERSION,
global_ctx::GlobalCtx, os_info::collect_device_os_info, resolve_machine_id,
global_ctx::GlobalCtx, resolve_machine_id,
},
instance::{
composition::runtime_one_shot_manual_connector,
config_storage::NativeConfigFileStorage,
factory::{NativeInstanceFactory, NativeInstanceManager},
host::NativeInstanceHost,
},
rpc_service::logger::NativeLoggerControl,
tunnel::TunnelScheme,
};
@@ -46,23 +50,32 @@ impl WebClient {
S: ToString,
H: ToString,
{
Self {
inner: easytier_core::management::WebClient::new(
connector,
WebClientConfig {
token: token.to_string(),
machine_id,
hostname: hostname.to_string(),
device_os: collect_device_os_info(),
easytier_version: EASYTIER_VERSION.to_owned(),
secure_mode,
},
manager,
hooks.unwrap_or_else(|| Arc::new(DefaultHooks)),
Arc::new(NativeConfigFileStorage),
Arc::new(NativeLoggerControl),
),
}
let config = WebClientConfig {
token: token.to_string(),
machine_id,
hostname: hostname.to_string(),
device_os: web_client_device_os_info(),
easytier_version: EASYTIER_VERSION.to_owned(),
secure_mode,
};
#[cfg(feature = "management")]
let inner = easytier_core::management::WebClient::new(
connector,
config,
manager,
hooks.unwrap_or_else(|| Arc::new(DefaultHooks)),
Arc::new(NativeConfigFileStorage),
Arc::new(NativeLoggerControl),
);
#[cfg(not(feature = "management"))]
let inner = easytier_core::management::WebClient::new(
connector,
config,
manager,
hooks.unwrap_or_else(|| Arc::new(DefaultHooks)),
Arc::new(easytier_core::management::UnsupportedConfigFileStorage),
);
Self { inner }
}
pub fn is_connected(&self) -> bool {
@@ -70,6 +83,20 @@ impl WebClient {
}
}
#[cfg(feature = "management")]
fn web_client_device_os_info() -> easytier_proto::web::DeviceOsInfo {
collect_device_os_info()
}
#[cfg(not(feature = "management"))]
fn web_client_device_os_info() -> easytier_proto::web::DeviceOsInfo {
easytier_proto::web::DeviceOsInfo {
os_type: std::env::consts::OS.to_owned(),
version: String::new(),
distribution: String::new(),
}
}
pub struct DefaultHooks;
#[async_trait]
@@ -113,6 +140,7 @@ pub async fn run_web_client(
let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
let mut flags = global_ctx.get_flags();
flags.bind_device = false;
config.set_flags(flags.clone());
global_ctx.set_flags(flags);
let hostname =
hostname.unwrap_or_else(|| gethostname::gethostname().to_string_lossy().to_string());