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 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. 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", "windows 0.52.0",
] ]
[[package]]
name = "easytier-mini"
version = "2.6.4"
dependencies = [
"anyhow",
"easytier",
"tokio",
]
[[package]] [[package]]
name = "easytier-proto" name = "easytier-proto"
version = "2.6.4" version = "2.6.4"
+6
View File
@@ -6,6 +6,7 @@ members = [
"easytier", "easytier",
"easytier-gui/src-tauri", "easytier-gui/src-tauri",
"easytier-web", "easytier-web",
"easytier-contrib/easytier-mini",
"easytier-contrib/easytier-ffi", "easytier-contrib/easytier-ffi",
"easytier-contrib/easytier-uptime", "easytier-contrib/easytier-uptime",
"easytier-contrib/easytier-android-jni", "easytier-contrib/easytier-android-jni",
@@ -29,3 +30,8 @@ lto = true
codegen-units = 1 codegen-units = 1
opt-level = 3 opt-level = 3
strip = true 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. Each protocol registration must provide a coherent client/server Adapter.
Unavailable configured transports must be rejected during validation or 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 ### Connectivity
@@ -283,8 +286,9 @@ Adapters.
Optional gateway capabilities are selected by cohesive Modules. Disabled Optional gateway capabilities are selected by cohesive Modules. Disabled
implementations retain stable lifecycle calls and report unsupported implementations retain stable lifecycle calls and report unsupported
configuration where a stable interface is required; they do not duplicate configuration in the standard runtime. A compact compatibility Host may
portable policy. 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 The instance-scoped `DataPlaneSession` composes the foundation operation broker
under the same session lock as its resource and quota state. The broker owns 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 ## Runtime configuration authority
`TomlConfig` is an owned construction input. After startup, it is not a second `TomlConfig` is the authoritative desired configuration used for management
mutable source of truth. 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; - peer feature flags and routing policy;
- listeners and initial peers; - 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 14. Unknown protobuf fields in reflected route information survive forwarding
and credential filtering. and credential filtering.
15. Feature selection is localized at cohesive Module/Adapter boundaries. 15. Feature selection is localized at cohesive Module/Adapter boundaries.
16. Unsupported configured capabilities fail explicitly rather than changing 16. The standard runtime rejects unsupported configured capabilities. Compact
wire protocol or silently falling back. 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 ## 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", "wrapped-transport",
"proxy-cidr-monitor", "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"] management-rpc = ["easytier-proto/api"]
proxy-cidr-monitor = [] proxy-cidr-monitor = []
rich-config-errors = ["dep:ariadne"] rich-config-errors = ["dep:ariadne"]
+2 -2
View File
@@ -1,8 +1,8 @@
//! Static configuration schema plus the live runtime configuration store. //! Static configuration schema plus the live runtime configuration store.
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub mod api; pub mod api;
#[cfg(any(feature = "management", feature = "browser-config"))] #[cfg(any(feature = "web-client", feature = "browser-config"))]
pub mod api_input; pub mod api_input;
#[cfg(all( #[cfg(all(
feature = "browser-config", feature = "browser-config",
+17
View File
@@ -95,6 +95,23 @@ impl CoreRuntimeConfigStore {
.send_modify(|version| *version += 1); .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)) { pub fn update_services(&self, update: impl FnOnce(&mut CoreRuntimeConfig)) {
let _update = self.inner.update.lock(); let _update = self.inner.update.lock();
let mut config = self.inner.snapshot.load_full().as_ref().clone(); 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; mod snapshot;
impl ConfigLoader for TomlConfig { impl ConfigLoader for TomlConfig {
+230 -24
View File
@@ -4,11 +4,11 @@ use std::collections::BTreeSet;
use crate::{ use crate::{
config::{ config::{
IpPrefix, NodeConfig, ProxyNetworkConfig, RouteConfig, EncryptionAlgorithm, IpPrefix, NodeConfig, ProxyNetworkConfig, RouteConfig,
gateway::{GatewayRuntimeConfig, ProxyRuntimeConfig}, gateway::{GatewayRuntimeConfig, ProxyRuntimeConfig},
peers::{AclRuleConfig, HostRoutingPolicy, PublicIpv6ProviderConfig}, peers::{AclRuleConfig, HostRoutingPolicy, PublicIpv6ProviderConfig},
runtime::CoreRuntimeConfig, runtime::CoreRuntimeConfig,
toml::{ConfigLoader as _, TomlConfig}, toml::{ConfigLoader as _, Flags, TomlConfig},
}, },
connectivity::{ connectivity::{
direct::DirectConnectorOptions, direct::DirectConnectorOptions,
@@ -16,13 +16,17 @@ use crate::{
stun::StunServerConfig, stun::StunServerConfig,
}, },
listener::plan::ListenerRuntimeConfig, listener::plan::ListenerRuntimeConfig,
packet::CompressorAlgo,
peers::{ peers::{
context::PeerRuntimeSnapshotInput, context::PeerRuntimeSnapshotInput,
peer_manager::{PortablePeerManagerConfig, RouteAlgoType}, peer_manager::{PortablePeerManagerConfig, RouteAlgoType},
}, },
socket::{NetNamespace, SocketContext, tcp::TcpBindOptions, udp::UdpBindOptions}, socket::{NetNamespace, SocketContext, tcp::TcpBindOptions, udp::UdpBindOptions},
tunnel::encrypt::algorithm_is_available,
}; };
use easytier_proto::common::CompressionAlgoPb;
use super::{CoreConnectivityConfig, CoreInstanceConfig}; use super::{CoreConnectivityConfig, CoreInstanceConfig};
const OSPF_UPDATE_MY_FOREIGN_NETWORK_INTERVAL_SEC: u64 = 10; const OSPF_UPDATE_MY_FOREIGN_NETWORK_INTERVAL_SEC: u64 = 10;
@@ -44,6 +48,15 @@ pub struct CoreInstanceHostConfig {
pub icmp_failure_is_fatal: bool, pub icmp_failure_is_fatal: bool,
pub public_ipv6_provider_supported: bool, pub public_ipv6_provider_supported: bool,
pub gateway_enabled: 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 easytier_version: String,
pub endpoint_protocols: Vec<String>, pub endpoint_protocols: Vec<String>,
} }
@@ -60,12 +73,87 @@ impl Default for CoreInstanceHostConfig {
icmp_failure_is_fatal: false, icmp_failure_is_fatal: false,
public_ipv6_provider_supported: false, public_ipv6_provider_supported: false,
gateway_enabled: true, 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(), easytier_version: env!("CARGO_PKG_VERSION").to_owned(),
endpoint_protocols: ManualEndpointDiscoveryConfig::default().srv_protocols, 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 { impl CoreInstanceConfig {
/// Normalizes the complete shared TOML model using OS-independent defaults. /// Normalizes the complete shared TOML model using OS-independent defaults.
/// ///
@@ -81,7 +169,7 @@ impl CoreInstanceConfig {
config: &TomlConfig, config: &TomlConfig,
host: &CoreInstanceHostConfig, host: &CoreInstanceHostConfig,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
let flags = config.get_flags(); let flags = host.runtime_flags(config.get_flags());
let instance_id = config.get_id(); let instance_id = config.get_id();
let identity: crate::config::NetworkIdentity = config.get_network_identity().into(); let identity: crate::config::NetworkIdentity = config.get_network_identity().into();
let network_name = identity.network_name.clone(); let network_name = identity.network_name.clone();
@@ -93,6 +181,16 @@ impl CoreInstanceConfig {
_ => host.hostname_fallback.clone().unwrap_or_default(), _ => host.hostname_fallback.clone().unwrap_or_default(),
}; };
let acl = config.get_acl(); 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 = let peer_snapshot =
crate::config::peers::PeerRuntimeSnapshot::from_host_input(PeerRuntimeSnapshotInput { crate::config::peers::PeerRuntimeSnapshot::from_host_input(PeerRuntimeSnapshotInput {
@@ -111,8 +209,7 @@ impl CoreInstanceConfig {
address: value.address().into(), address: value.address().into(),
prefix_len: value.network_length(), prefix_len: value.network_length(),
}), }),
proxy_networks: config proxy_networks: proxy_networks
.get_proxy_cidrs()
.into_iter() .into_iter()
.map(|proxy| ProxyNetworkConfig { .map(|proxy| ProxyNetworkConfig {
real: IpPrefix { real: IpPrefix {
@@ -134,12 +231,13 @@ impl CoreInstanceConfig {
host_routing: host.host_routing, host_routing: host.host_routing,
acl: acl.clone(), acl: acl.clone(),
easytier_version: host.easytier_version.clone(), easytier_version: host.easytier_version.clone(),
vpn_portal_cidr: config vpn_portal_cidr: (!host.ignore_unsupported_config || host.vpn_portal_enabled)
.get_vpn_portal_config() .then(|| config.get_vpn_portal_config())
.flatten()
.map(|portal| portal.client_cidr), .map(|portal| portal.client_cidr),
pinned_peers: config pinned_peers: peers
.get_peers() .iter()
.into_iter() .cloned()
.map(|peer| (peer.uri, peer.peer_public_key)) .map(|peer| (peer.uri, peer.peer_public_key))
.collect(), .collect(),
ospf_update_my_foreign_network_interval_sec: ospf_update_my_foreign_network_interval_sec:
@@ -151,19 +249,28 @@ impl CoreInstanceConfig {
let peer = PortablePeerManagerConfig { let peer = PortablePeerManagerConfig {
snapshot: peer_snapshot, snapshot: peer_snapshot,
route_algo: RouteAlgoType::Ospf, route_algo: RouteAlgoType::Ospf,
exit_nodes: config.get_exit_nodes(), exit_nodes: if host.ignore_unsupported_config && !host.proxy_enabled {
foreign_context_default_flags: TomlConfig::default().get_flags(), 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 tcp_bind = TcpBindOptions::default().with_context(socket_context.clone());
let udp_bind = UdpBindOptions::direct_connect().with_context(socket_context.clone()); let udp_bind = UdpBindOptions::direct_connect().with_context(socket_context.clone());
let listeners = Some(ListenerRuntimeConfig::new( 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, flags.enable_ipv6,
socket_context.clone(), socket_context.clone(),
)); ));
let socks5_bind = config let socks5_bind = (!host.ignore_unsupported_config || host.gateway_enabled)
.get_socks5_portal() .then(|| config.get_socks5_portal())
.flatten()
.map(|url| { .map(|url| {
let host = url let host = url
.host_str() .host_str()
@@ -186,7 +293,11 @@ impl CoreInstanceConfig {
dhcp_ipv4: config.get_dhcp(), dhcp_ipv4: config.get_dhcp(),
gateway: GatewayRuntimeConfig { gateway: GatewayRuntimeConfig {
socks5_bind, 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 manual_routes: config
.get_routes() .get_routes()
@@ -200,10 +311,15 @@ impl CoreInstanceConfig {
icmp_failure_is_fatal: host.icmp_failure_is_fatal, icmp_failure_is_fatal: host.icmp_failure_is_fatal,
udp_response_ipv4_mtu: 1280, 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 { public_ipv6_provider: PublicIpv6ProviderConfig {
provider_enabled: config.get_ipv6_public_addr_provider(), provider_enabled: config.get_ipv6_public_addr_provider()
configured_prefix: config.get_ipv6_public_addr_prefix(), && (!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, provider_supported: host.public_ipv6_provider_supported,
}, },
}; };
@@ -212,11 +328,7 @@ impl CoreInstanceConfig {
instance_name: config.get_inst_name(), instance_name: config.get_inst_name(),
peer, peer,
connectivity: CoreConnectivityConfig { connectivity: CoreConnectivityConfig {
initial_peers: config initial_peers: peers.into_iter().map(|peer| peer.uri).collect(),
.get_peers()
.into_iter()
.map(|peer| peer.uri)
.collect(),
listeners, listeners,
runtime, runtime,
startup_plan: super::CoreInstanceStartupPlan { startup_plan: super::CoreInstanceStartupPlan {
@@ -341,6 +453,7 @@ disable_p2p = true
gateway_enabled: false, gateway_enabled: false,
easytier_version: "host-version".to_owned(), easytier_version: "host-version".to_owned(),
endpoint_protocols: vec!["host-protocol".to_owned()], endpoint_protocols: vec!["host-protocol".to_owned()],
..Default::default()
}; };
let normalized = CoreInstanceConfig::from_toml_with_host(&config, &host).unwrap(); let normalized = CoreInstanceConfig::from_toml_with_host(&config, &host).unwrap();
@@ -383,4 +496,97 @@ disable_p2p = true
["host-protocol"] ["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}; use super::{CoreInstance, CoreInstanceHost, CoreInstanceHostConfig};
impl<H> CoreInstance<H> impl<H> CoreInstance<H>
@@ -12,10 +8,6 @@ where
self.management.toml_config() self.management.toml_config()
} }
pub(crate) fn runtime_config_snapshot(&self) -> Arc<CoreInstanceRuntimeConfig> {
self.runtime_config.snapshot()
}
pub(crate) fn host_config(&self) -> &CoreInstanceHostConfig { pub(crate) fn host_config(&self) -> &CoreInstanceHostConfig {
self.management.host_config() self.management.host_config()
} }
@@ -1,9 +1,9 @@
use crate::{config::toml::TomlConfig, instance::CoreInstanceHostConfig}; use crate::{config::toml::TomlConfig, instance::CoreInstanceHostConfig};
pub(super) struct ManagementState { pub(super) struct ManagementState {
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
toml_config: Option<TomlConfig>, toml_config: Option<TomlConfig>,
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
host_config: CoreInstanceHostConfig, host_config: CoreInstanceHostConfig,
} }
@@ -12,22 +12,22 @@ impl ManagementState {
toml_config: Option<TomlConfig>, toml_config: Option<TomlConfig>,
host_config: CoreInstanceHostConfig, host_config: CoreInstanceHostConfig,
) -> Self { ) -> Self {
#[cfg(not(feature = "management"))] #[cfg(not(feature = "web-client"))]
let _ = (toml_config, host_config); let _ = (toml_config, host_config);
Self { Self {
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
toml_config, toml_config,
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
host_config, host_config,
} }
} }
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub(super) fn toml_config(&self) -> Option<TomlConfig> { pub(super) fn toml_config(&self) -> Option<TomlConfig> {
self.toml_config.clone() self.toml_config.clone()
} }
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub(super) fn host_config(&self) -> &CoreInstanceHostConfig { pub(super) fn host_config(&self) -> &CoreInstanceHostConfig {
&self.host_config &self.host_config
} }
+7 -7
View File
@@ -17,12 +17,12 @@ use uuid::Uuid;
use crate::config::toml::TomlConfig; use crate::config::toml::TomlConfig;
use crate::instance::{CoreInstance, CoreInstanceHost}; use crate::instance::{CoreInstance, CoreInstanceHost};
use crate::process_runtime::CoreProcessRuntime; use crate::process_runtime::CoreProcessRuntime;
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
use crate::{ use crate::{
config::toml::{ConfigLoader as _, ConfigSource}, config::toml::{ConfigLoader as _, ConfigSource},
management::network_instance_running_info, management::network_instance_running_info,
}; };
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
use easytier_proto::api::manage::NetworkInstanceRunningInfo; use easytier_proto::api::manage::NetworkInstanceRunningInfo;
/// Stable identity required by the instance collection. /// 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> { pub fn config(&self, instance_id: Uuid) -> Option<TomlConfig> {
self.get(instance_id) self.get(instance_id)
.and_then(|instance| instance.toml_config()) .and_then(|instance| instance.toml_config())
} }
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub fn config_source(&self, instance_id: Uuid) -> Option<ConfigSource> { pub fn config_source(&self, instance_id: Uuid) -> Option<ConfigSource> {
self.config(instance_id) self.config(instance_id)
.map(|config| config.get_network_config_source()) .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> { pub async fn network_info(&self, instance_id: Uuid) -> Option<NetworkInstanceRunningInfo> {
let instance = self.get(instance_id)?; let instance = self.get(instance_id)?;
network_instance_running_info(instance.as_ref()).await.ok() network_instance_running_info(instance.as_ref()).await.ok()
} }
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub async fn collect_network_infos( pub async fn collect_network_infos(
&self, &self,
) -> anyhow::Result<std::collections::BTreeMap<Uuid, NetworkInstanceRunningInfo>> { ) -> anyhow::Result<std::collections::BTreeMap<Uuid, NetworkInstanceRunningInfo>> {
@@ -471,7 +471,7 @@ where
Ok(result) Ok(result)
} }
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub fn collect_network_infos_sync( pub fn collect_network_infos_sync(
&self, &self,
) -> anyhow::Result<std::collections::BTreeMap<Uuid, NetworkInstanceRunningInfo>> { ) -> anyhow::Result<std::collections::BTreeMap<Uuid, NetworkInstanceRunningInfo>> {
+36 -12
View File
@@ -6,7 +6,7 @@ mod config;
mod data_plane_extension; mod data_plane_extension;
mod lifecycle; mod lifecycle;
mod management; mod management;
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
mod management_extension; mod management_extension;
mod management_state; mod management_state;
pub mod manager; pub mod manager;
@@ -214,6 +214,23 @@ fn retain_core_peer_identity(
peer.runtime.core.node.instance_id = instance_id; 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 /// Host-owned resources that must be prepared for the complete Instance
/// lifetime, such as a native packet interface. /// lifetime, such as a native packet interface.
#[async_trait::async_trait] #[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 /// Applies Host-side cached views of fields already committed to the
/// shared TOML model. /// shared TOML model.
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
fn synchronize_config(&self, _patch: &crate::proto::api::config::InstanceConfigPatch) {} 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 publish_config_patch(&self, _patch: crate::proto::api::config::InstanceConfigPatch) {}
fn attach_tun_fd(&self, _fd: i32) -> anyhow::Result<()> { fn attach_tun_fd(&self, _fd: i32) -> anyhow::Result<()> {
@@ -846,7 +868,7 @@ where
pub(crate) async fn update_runtime_config_under_operation( pub(crate) async fn update_runtime_config_under_operation(
&self, &self,
mut config: CoreInstanceRuntimeConfig, config: CoreInstanceRuntimeConfig,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if matches!( if matches!(
self.state(), self.state(),
@@ -856,21 +878,23 @@ where
} }
self.validate_runtime_config_capabilities(&config)?; self.validate_runtime_config_capabilities(&config)?;
let current = self.runtime_config.snapshot(); 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 let refresh_acl_groups = current.peer.peer_group_memberships
!= config.peer.peer_group_memberships != config.peer.peer_group_memberships
|| current.peer.acl_group_declarations != config.peer.acl_group_declarations; || current.peer.acl_group_declarations != config.peer.acl_group_declarations;
if current.services.acl != config.services.acl { if current.services.acl != config.services.acl {
self.reload_acl_config_inner(&config.services.acl).await?; 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.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 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 { if refresh_acl_groups {
self.refresh_acl_groups().await; self.refresh_acl_groups().await;
} }
+151 -4
View File
@@ -466,6 +466,57 @@ mod portable_runtime {
build_with_engines(config, WrappedTransportEngines::default()) 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] #[tokio::test]
async fn core_instance_is_a_direct_managed_record() { async fn core_instance_is_a_direct_managed_record() {
let instance = build_instance(test_config("managed-directly")).unwrap(); let instance = build_instance(test_config("managed-directly")).unwrap();
@@ -600,7 +651,7 @@ hostname = "core-owned-config"
response.config.unwrap().hostname.as_deref(), response.config.unwrap().hostname.as_deref(),
Some("patched-in-core") 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.proxy.enable_exit_node);
assert!(runtime.services.public_ipv6_provider.provider_supported); assert!(runtime.services.public_ipv6_provider.provider_supported);
assert_eq!(runtime.peer.easytier_version, "host-version"); assert_eq!(runtime.peer.easytier_version, "host-version");
@@ -685,7 +736,8 @@ hostname = "core-owned-config"
); );
assert!( assert!(
instance instance
.runtime_config_snapshot() .runtime_config
.snapshot()
.services .services
.gateway .gateway
.port_forwards .port_forwards
@@ -694,6 +746,71 @@ hostname = "core-owned-config"
instance.stop().await; 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] #[tokio::test]
async fn dropping_core_instance_requests_host_shutdown() { async fn dropping_core_instance_requests_host_shutdown() {
struct DropAwareRuntimeHost(Arc<AtomicBool>); struct DropAwareRuntimeHost(Arc<AtomicBool>);
@@ -992,19 +1109,24 @@ hostname = "core-owned-config"
assert!(instances.instances().is_empty()); assert!(instances.instances().is_empty());
} }
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
#[tokio::test] #[tokio::test]
async fn process_management_rpc_owns_instance_create_list_and_delete() { async fn process_management_rpc_owns_instance_create_list_and_delete() {
use crate::{ use crate::{
config::toml::TomlConfig, config::toml::TomlConfig,
instance::manager::InstanceFactory, instance::manager::InstanceFactory,
management::{InstanceManager, ProcessManagementRpc, UnsupportedConfigFileStorage}, management::{
InstanceManager, ProcessManagementRpc, UnsupportedConfigFileStorage,
register_web_client_rpc,
},
rpc::service_registry::ServiceRegistry,
}; };
use easytier_proto::{ use easytier_proto::{
api::manage::{ api::manage::{
DeleteNetworkInstanceRequest, ListNetworkInstanceRequest, NetworkConfig, DeleteNetworkInstanceRequest, ListNetworkInstanceRequest, NetworkConfig,
NetworkingMethod, RunNetworkInstanceRequest, WebClientService, NetworkingMethod, RunNetworkInstanceRequest, WebClientService,
}, },
common::RpcDescriptor,
rpc_types::controller::BaseController, rpc_types::controller::BaseController,
}; };
@@ -1038,6 +1160,31 @@ hostname = "core-owned-config"
ManagementTestFactory(CoreProcessRuntime::new()), ManagementTestFactory(CoreProcessRuntime::new()),
Some(tokio::runtime::Handle::current()), 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( let rpc = ProcessManagementRpc::<ManagementTestFactory>::new(
instances.clone(), instances.clone(),
Arc::new(()), Arc::new(()),
@@ -31,7 +31,8 @@ where
.toml_config() .toml_config()
.ok_or_else(|| anyhow::anyhow!("shared TOML configuration is not available"))?; .ok_or_else(|| anyhow::anyhow!("shared TOML configuration is not available"))?;
let candidate = config.detached_snapshot(); 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(); let patch_for_host = patch.clone();
// Preserve the existing ordered partial-commit contract: earlier valid // Preserve the existing ordered partial-commit contract: earlier valid
@@ -54,8 +55,11 @@ where
result?; result?;
let result = patch_exit_nodes_config(&candidate, patch.exit_nodes); let result = patch_exit_nodes_config(&candidate, patch.exit_nodes);
validate_and_commit_candidate(instance, &config, &candidate)?; let normalized = validate_and_commit_candidate(instance, &config, &candidate)?;
instance.update_exit_nodes(result?).await; result?;
instance
.update_exit_nodes(normalized.peer.exit_nodes.clone())
.await;
let result = patch_mapped_listeners(&candidate, patch.mapped_listeners); let result = patch_mapped_listeners(&candidate, patch.mapped_listeners);
validate_and_commit_candidate(instance, &config, &candidate)?; validate_and_commit_candidate(instance, &config, &candidate)?;
@@ -91,10 +95,11 @@ where
candidate.set_ipv6_public_addr_prefix(prefix); candidate.set_ipv6_public_addr_prefix(prefix);
provider_config_changed = true; 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
.instance_runtime .instance_runtime
.synchronize_config(&patch_for_host); .synchronize_config(&patch_for_host, &runtime);
Ok(provider_config_changed) Ok(provider_config_changed)
} }
.await; .await;
@@ -106,9 +111,12 @@ where
instance instance
.instance_runtime .instance_runtime
.publish_config_patch(patch_for_host); .publish_config_patch(patch_for_host);
#[cfg(feature = "public-ipv6-provider")]
if provider_config_changed && instance.state() == CoreInstanceState::Running { if provider_config_changed && instance.state() == CoreInstanceState::Running {
instance.reconcile_public_ipv6_provider().await; instance.reconcile_public_ipv6_provider().await;
} }
#[cfg(not(feature = "public-ipv6-provider"))]
let _ = provider_config_changed;
Ok(()) Ok(())
} }
@@ -116,14 +124,16 @@ fn validate_and_commit_candidate<H>(
instance: &CoreInstance<H>, instance: &CoreInstance<H>,
shared: &TomlConfig, shared: &TomlConfig,
candidate: &TomlConfig, candidate: &TomlConfig,
) -> anyhow::Result<()> ) -> anyhow::Result<CoreInstanceConfig>
where where
H: CoreInstanceHost, 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)?; instance.validate_runtime_config_capabilities(&runtime)?;
shared.replace_from_snapshot(candidate); shared.replace_from_snapshot(candidate);
Ok(()) Ok(normalized)
} }
fn runtime_config_from_toml<H>( fn runtime_config_from_toml<H>(
@@ -134,15 +144,14 @@ where
H: CoreInstanceHost, H: CoreInstanceHost,
{ {
let normalized = CoreInstanceConfig::from_toml_with_host(config, instance.host_config())?; let normalized = CoreInstanceConfig::from_toml_with_host(config, instance.host_config())?;
let current = instance.runtime_config_snapshot(); Ok(runtime_config_from_normalized(&normalized))
let services = normalized.connectivity.runtime; }
let mut peer = normalized.peer.snapshot;
peer.runtime.stun_info = current.peer.runtime.stun_info.clone();
Ok(CoreInstanceRuntimeConfig { fn runtime_config_from_normalized(config: &CoreInstanceConfig) -> CoreInstanceRuntimeConfig {
services, CoreInstanceRuntimeConfig {
peer: Arc::new(peer), services: config.connectivity.runtime.clone(),
}) peer: Arc::new(config.peer.snapshot.clone()),
}
} }
fn parse_ipv6_public_addr_prefix_patch( 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>]) { fn trace_patchables<T: Debug>(patches: &[Patchable<T>]) {
for patch in patches { for patch in patches {
match patch.action { match patch.action {
@@ -336,13 +317,25 @@ where
H: CoreInstanceHost, H: CoreInstanceHost,
{ {
for patch in patches { 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) { 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) => { 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) { if !instance.remove_connector(&url) {
anyhow::bail!("connector not found: {url}"); anyhow::bail!("connector not found: {url}");
} }
@@ -50,7 +50,10 @@ where
.map(Into::into) .map(Into::into)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone()); 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); 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 let dev_name = instance
.toml_config() .toml_config()
.map(|config| config.get_flags().dev_name) .map(|config| config.get_flags().dev_name)
+34 -4
View File
@@ -1,19 +1,24 @@
#[cfg(feature = "management")]
mod compiled; mod compiled;
mod config_patch; mod config_patch;
mod instance_info; mod instance_info;
#[cfg(feature = "management")]
mod logger_rpc; mod logger_rpc;
#[cfg(feature = "management")]
pub(super) mod packet_proxy; pub(super) mod packet_proxy;
mod process_rpc; mod process_rpc;
#[cfg(feature = "management")]
pub mod remote_client; pub mod remote_client;
mod web_client; mod web_client;
use std::sync::Arc; 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::{ use easytier_proto::{
api::{ api::logger::{LoggerRpc, LoggerRpcServer},
logger::{LoggerRpc, LoggerRpcServer},
manage::WebClientServiceServer,
},
rpc_types::controller::BaseController, rpc_types::controller::BaseController,
}; };
@@ -30,9 +35,11 @@ use super::{
ConfigFileControl, ConfigFilePermission, DaemonGuard, resolve_optional_instance_by_name, ConfigFileControl, ConfigFilePermission, DaemonGuard, resolve_optional_instance_by_name,
}; };
#[cfg(feature = "management")]
pub use compiled::register_instance_management_rpc; pub use compiled::register_instance_management_rpc;
pub use config_patch::apply_config_patch; pub use config_patch::apply_config_patch;
pub use instance_info::network_instance_running_info; pub use instance_info::network_instance_running_info;
#[cfg(feature = "management")]
pub use logger_rpc::{ pub use logger_rpc::{
LoggerControl, LoggerManagementRpc, UnsupportedLoggerControl, log_level_name, parse_log_level, LoggerControl, LoggerManagementRpc, UnsupportedLoggerControl, log_level_name, parse_log_level,
}; };
@@ -44,6 +51,7 @@ pub use process_rpc::{
pub(crate) use web_client::WebClientBackend; pub(crate) use web_client::WebClientBackend;
pub use web_client::{ConfigServerEndpoint, WebClient, WebClientConfig}; pub use web_client::{ConfigServerEndpoint, WebClient, WebClientConfig};
#[cfg(feature = "management")]
pub use super::instance_rpc::full::call_instance_json_rpc; pub use super::instance_rpc::full::call_instance_json_rpc;
pub fn config_source_from_rpc(source: i32) -> Option<ConfigSource> { 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. /// Registers the complete process-level management surface once.
#[cfg(feature = "management")]
pub fn register_management_rpc<F, H>( pub fn register_management_rpc<F, H>(
instances: Arc<InstanceManager<F>>, instances: Arc<InstanceManager<F>>,
registry: &ServiceRegistry, 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>( pub async fn call_management_json_rpc<F, H>(
manager: &Arc<InstanceManager<F>>, manager: &Arc<InstanceManager<F>>,
logger: Arc<dyn LoggerControl>, logger: Arc<dyn LoggerControl>,
@@ -23,10 +23,11 @@ use crate::{
tunnel::{Tunnel, web_security}, tunnel::{Tunnel, web_security},
}; };
use super::{ #[cfg(not(feature = "management"))]
ConfigFileStorage, DaemonGuard, InstanceManager, InstanceMutationHooks, LoggerControl, use super::register_web_client_rpc;
register_management_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); const RETRY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
// Keep retry ownership in this loop when transport or protocol handshakes stall. // Keep retry ownership in this loop when transport or protocol handshakes stall.
@@ -108,6 +109,7 @@ where
instances: Arc<InstanceManager<F>>, instances: Arc<InstanceManager<F>>,
hooks: Arc<dyn InstanceMutationHooks>, hooks: Arc<dyn InstanceMutationHooks>,
storage: Arc<dyn ConfigFileStorage>, storage: Arc<dyn ConfigFileStorage>,
#[cfg(feature = "management")]
logger: Arc<dyn LoggerControl>, logger: Arc<dyn LoggerControl>,
} }
@@ -119,6 +121,7 @@ where
H: CoreInstanceHost, H: CoreInstanceHost,
{ {
fn register(&self, registry: &ServiceRegistry) { fn register(&self, registry: &ServiceRegistry) {
#[cfg(feature = "management")]
register_management_rpc( register_management_rpc(
self.instances.clone(), self.instances.clone(),
registry, registry,
@@ -126,6 +129,13 @@ where
self.storage.clone(), self.storage.clone(),
self.logger.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>> { async fn instance_ids(&self) -> anyhow::Result<Vec<uuid::Uuid>> {
@@ -159,13 +169,14 @@ where
instances: Arc<InstanceManager<F>>, instances: Arc<InstanceManager<F>>,
hooks: Arc<dyn InstanceMutationHooks>, hooks: Arc<dyn InstanceMutationHooks>,
storage: Arc<dyn ConfigFileStorage>, storage: Arc<dyn ConfigFileStorage>,
logger: Arc<dyn LoggerControl>, #[cfg(feature = "management")] logger: Arc<dyn LoggerControl>,
) -> Self { ) -> Self {
let manager_guard = instances.register_daemon(); let manager_guard = instances.register_daemon();
let backend = Arc::new(NativeWebClientBackend { let backend = Arc::new(NativeWebClientBackend {
instances, instances,
hooks, hooks,
storage, storage,
#[cfg(feature = "management")]
logger, logger,
}); });
Self::start(connector, config, backend, Some(manager_guard)) 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::{ use easytier_proto::{
api::{ api::{
config::{ config::ConfigRpc,
ConfigRpc, GetConfigRequest, GetConfigResponse, PatchConfigRequest, PatchConfigResponse,
},
instance::{ instance::{
AclManageRpc, ConnectorManageRpc, CredentialInfo, CredentialManageRpc, AclManageRpc, ConnectorManageRpc, CredentialInfo, CredentialManageRpc,
GenerateCredentialRequest, GenerateCredentialResponse, GetAclStatsRequest, GenerateCredentialRequest, GenerateCredentialResponse, GetAclStatsRequest,
@@ -27,7 +25,7 @@ use easytier_proto::{
}; };
use crate::{ use crate::{
config::{api::network_config_from_toml, toml::ConfigLoader as _}, config::toml::ConfigLoader as _,
instance::{ instance::{
CoreInstance, CoreInstanceHost, CoreInstance, CoreInstanceHost,
manager::{InstanceFactory, InstanceManager}, manager::{InstanceFactory, InstanceManager},
@@ -35,11 +33,8 @@ use crate::{
peers::credential_manager::{CredentialCreateOptions, CredentialInfo as CoreCredentialInfo}, peers::credential_manager::{CredentialCreateOptions, CredentialInfo as CoreCredentialInfo},
}; };
use super::{InstanceManagementRpc, ReadOnlyInstanceResolver, ResolvedInstanceManagementRpc}; use super::InstanceManagementRpc;
use crate::management::{ use crate::management::{full::packet_proxy, resolve_instance};
full::{apply_config_patch, packet_proxy},
resolve_instance,
};
/// Dispatches the JSON form of an Instance-targeted management RPC without /// Dispatches the JSON form of an Instance-targeted management RPC without
/// introducing a second, Host-owned set of service implementations. /// introducing a second, Host-owned set of service implementations.
@@ -369,38 +364,3 @@ where
Err(anyhow::anyhow!("not implemented for management API").into()) 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; use super::resolve_instance;
#[cfg(feature = "web-client")]
mod config;
#[cfg(feature = "management")] #[cfg(feature = "management")]
pub(super) mod full; pub(super) mod full;
#[cfg(all(feature = "management", feature = "proxy-packet"))] #[cfg(all(feature = "management", feature = "proxy-packet"))]
+13 -7
View File
@@ -2,7 +2,7 @@
#[cfg(all(feature = "management", any(test, target_os = "wasi")))] #[cfg(all(feature = "management", any(test, target_os = "wasi")))]
mod forwarded_rpc; mod forwarded_rpc;
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
mod full; mod full;
mod instance_rpc; mod instance_rpc;
mod rpc_server_hook; mod rpc_server_hook;
@@ -28,16 +28,22 @@ pub(crate) use forwarded_rpc::{
}; };
#[cfg(all(feature = "management", target_os = "wasi"))] #[cfg(all(feature = "management", target_os = "wasi"))]
pub(crate) use full::WebClientBackend; pub(crate) use full::WebClientBackend;
#[cfg(all(feature = "web-client", test))]
pub(crate) use full::register_web_client_rpc;
#[cfg(feature = "management")] #[cfg(feature = "management")]
pub use full::remote_client; pub use full::remote_client;
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub use full::{ pub use full::{
ConfigFileStorage, ConfigServerEndpoint, InstanceMutationHooks, InstanceMutationResult, ConfigFileStorage, ConfigServerEndpoint, InstanceMutationHooks, InstanceMutationResult,
LoggerControl, LoggerManagementRpc, ProcessManagement, ProcessManagementRpc, ProcessManagement, ProcessManagementRpc, UnsupportedConfigFileStorage, WebClient,
UnsupportedConfigFileStorage, UnsupportedLoggerControl, WebClient, WebClientConfig, WebClientConfig, apply_config_patch, config_source_from_rpc, config_source_to_rpc,
apply_config_patch, call_instance_json_rpc, call_management_json_rpc, config_source_from_rpc, network_instance_running_info,
config_source_to_rpc, log_level_name, network_instance_running_info, parse_log_level, };
register_instance_management_rpc, register_management_rpc, #[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 instance_rpc::InstanceManagementRpc;
pub use rpc_server_hook::ManagementRpcServerHook; pub use rpc_server_hook::ManagementRpcServerHook;
+14 -2
View File
@@ -277,12 +277,24 @@ pub fn build_rpc_packet(args: BuildRpcPacketArgs<'_>) -> Vec<ZCPacket> {
ret ret
} }
#[cfg(all(test, not(feature = "zstd")))] #[cfg(test)]
mod tests { mod tests {
use crate::proto::common::CompressionAlgoPb; 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] #[tokio::test]
async fn compression_negotiation_falls_back_when_zstd_is_unavailable() { async fn compression_negotiation_falls_back_when_zstd_is_unavailable() {
assert_eq!(accepted_compression_algo(), CompressionAlgoPb::None); 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 axum::{Json, Router, extract::State, routing::post};
use easytier::{ use easytier::{
common::{MachineIdOptions, config::NetworkConfigExt}, common::{MachineIdOptions, config::NetworkConfigExt},
instance::factory::{NativeInstanceManager, native_instance_manager}, instance::factory::{
NativeInstanceManager, native_compact_instance_manager_with_runtime,
native_instance_manager,
},
proto::{ proto::{
api::manage::{NetworkConfig, NetworkingMethod, PortForwardConfig}, api::manage::{NetworkConfig, NetworkingMethod, PortForwardConfig},
common::CompressionAlgoPb, common::CompressionAlgoPb,
@@ -901,6 +904,72 @@ mod tests {
println!("{:?}", mgr); 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] #[tokio::test]
async fn managed_web_config_revision_updates_running_core_config() { async fn managed_web_config_revision_updates_running_core_config() {
let (webhook_config, webhook_server, _) = test_webhook_config().await; let (webhook_config, webhook_server, _) = test_webhook_config().await;
+2 -1
View File
@@ -413,14 +413,15 @@ extended-services = [
"proxy-cidr-monitor", "proxy-cidr-monitor",
] ]
management = [ management = [
"web-client",
"logging", "logging",
"management-rpc",
"easytier-core/management", "easytier-core/management",
"easytier-proto/json-rpc", "easytier-proto/json-rpc",
"easytier-proto/utils", "easytier-proto/utils",
"tokio/full", "tokio/full",
] ]
management-rpc = ["easytier-core/management-rpc"] management-rpc = ["easytier-core/management-rpc"]
web-client = ["management-rpc", "easytier-core/web-client"]
tcp-hole-punch = ["easytier-core/tcp-hole-punch"] tcp-hole-punch = ["easytier-core/tcp-hole-punch"]
# Deprecated: hotpath profiling has been removed. These feature aliases are # Deprecated: hotpath profiling has been removed. These feature aliases are
# retained as no-ops so existing build scripts using `--features hotpath*` # 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 arc_swap::ArcSwap;
use async_trait::async_trait; use async_trait::async_trait;
use easytier_core::config::PeerId;
use easytier_core::connectivity::composite::ConnectorRuntime as _; use easytier_core::connectivity::composite::ConnectorRuntime as _;
use easytier_core::peers::public_ipv6::PublicIpv6Host; use easytier_core::peers::public_ipv6::PublicIpv6Host;
use easytier_core::socket::{NetNamespace, SocketContext}; use easytier_core::socket::{NetNamespace, SocketContext};
use easytier_core::tunnel::effective_encryption_uses_xor; use easytier_core::tunnel::effective_encryption_uses_xor;
use easytier_core::{
config::{PeerId, peers::PeerRuntimeSnapshot, runtime::CoreInstanceRuntimeConfig},
instance::{CoreInstanceConfig, CoreInstanceHostConfig},
};
use super::{ use super::{
config::{ConfigLoader, Flags, NetworkIdentity}, config::{ConfigLoader, Flags, NetworkIdentity},
@@ -85,11 +88,13 @@ pub struct GlobalCtx {
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>, cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>, cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>,
vpn_portal_cidr: AtomicCell<Option<cidr::Ipv4Cidr>>,
hostname: Mutex<String>, hostname: Mutex<String>,
tun_device_name: Mutex<Option<String>>, tun_device_name: Mutex<Option<String>>,
flags: ArcSwap<Flags>, flags: ArcSwap<Flags>,
runtime_endpoint_protocols: Option<HashSet<String>>,
} }
impl std::fmt::Debug for GlobalCtx { impl std::fmt::Debug for GlobalCtx {
@@ -113,7 +118,7 @@ impl PublicIpv6Host for GlobalCtx {
prefix: cidr::Ipv6Cidr, prefix: cidr::Ipv6Cidr,
) -> HashSet<Ipv6Addr> { ) -> HashSet<Ipv6Addr> {
let context = SocketContext::default() 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)); .with_netns(self.net_ns.name().map(NetNamespace::new));
let ip_list = crate::host_runtime::native_host_runtime() let ip_list = crate::host_runtime::native_host_runtime()
.collect_ip_addrs(&context) .collect_ip_addrs(&context)
@@ -139,14 +144,57 @@ impl PublicIpv6Host for GlobalCtx {
impl GlobalCtx { impl GlobalCtx {
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self { 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 id = config_fs.get_id();
let network = config_fs.get_network_identity(); let network = config_fs.get_network_identity();
let net_ns = NetNS::new(config_fs.get_netns()); let net_ns = NetNS::new(config_fs.get_netns());
let hostname = match config_fs.get_hostname() { let hostname = runtime
hostname if !hostname.is_empty() => hostname, .and_then(|runtime| runtime.peer.runtime.core.node.hostname.clone())
_ => gethostname::gethostname().to_string_lossy().to_string(), .unwrap_or_else(|| match config_fs.get_hostname() {
}; hostname if !hostname.is_empty() => hostname,
let flags = config_fs.get_flags(); _ => 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) { if flags.enable_encryption && effective_encryption_uses_xor(&flags.encryption_algorithm) {
tracing::warn!("using insecure XOR because no AEAD encryption is configured"); tracing::warn!("using insecure XOR because no AEAD encryption is configured");
} }
@@ -160,16 +208,34 @@ impl GlobalCtx {
network, network,
event_bus, event_bus,
cached_ipv4: AtomicCell::new(None), cached_ipv4: AtomicCell::new(ipv4),
cached_ipv6: AtomicCell::new(None), cached_ipv6: AtomicCell::new(ipv6),
vpn_portal_cidr: AtomicCell::new(vpn_portal_cidr),
hostname: Mutex::new(hostname), hostname: Mutex::new(hostname),
tun_device_name: Mutex::new(None), tun_device_name: Mutex::new(None),
flags: ArcSwap::new(Arc::new(flags)), 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 { pub fn subscribe(&self) -> EventBusSubscriber {
self.event_bus.subscribe() self.event_bus.subscribe()
} }
@@ -207,31 +273,19 @@ impl GlobalCtx {
} }
pub fn get_ipv4(&self) -> Option<cidr::Ipv4Inet> { pub fn get_ipv4(&self) -> Option<cidr::Ipv4Inet> {
if let Some(ret) = self.cached_ipv4.load() { self.cached_ipv4.load()
return Some(ret);
}
let addr = self.config.get_ipv4();
self.cached_ipv4.store(addr);
addr
} }
pub fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>) { pub fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>) {
self.config.set_ipv4(addr); self.cached_ipv4.store(addr);
self.cached_ipv4.store(None);
} }
pub fn get_ipv6(&self) -> Option<cidr::Ipv6Inet> { pub fn get_ipv6(&self) -> Option<cidr::Ipv6Inet> {
if let Some(ret) = self.cached_ipv6.load() { self.cached_ipv6.load()
return Some(ret);
}
let addr = self.config.get_ipv6();
self.cached_ipv6.store(addr);
addr
} }
pub fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>) { pub fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>) {
self.config.set_ipv6(addr); self.cached_ipv6.store(addr);
self.cached_ipv6.store(None);
} }
pub fn is_ip_local_ipv6(&self, ip: &std::net::Ipv6Addr) -> bool { 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> { 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 { pub fn get_flags(&self) -> Flags {
@@ -281,7 +335,6 @@ impl GlobalCtx {
} }
pub fn set_flags(&self, flags: Flags) { pub fn set_flags(&self, flags: Flags) {
self.config.set_flags(flags.clone());
self.flags.store(Arc::new(flags)); self.flags.store(Arc::new(flags));
} }
@@ -300,6 +353,17 @@ impl GlobalCtx {
pub fn no_tun(&self) -> bool { pub fn no_tun(&self) -> bool {
self.flags.load().no_tun 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)] #[cfg(test)]
@@ -377,6 +441,60 @@ pub mod tests {
assert!(!config.dump().contains("hostname")); 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( pub fn get_mock_global_ctx_with_network(
network_identy: Option<NetworkIdentity>, network_identy: Option<NetworkIdentity>,
) -> ArcGlobalCtx { ) -> ArcGlobalCtx {
+56 -20
View File
@@ -6,15 +6,14 @@ use easytier_core::gateway::proxy::wrapped_transport::WrappedTransportEngines;
use easytier_core::gateway::vpn_portal::VpnPortalHost; use easytier_core::gateway::vpn_portal::VpnPortalHost;
#[cfg(test)] #[cfg(test)]
use easytier_core::host::packet::{HostPacket, PacketSink}; use easytier_core::host::packet::{HostPacket, PacketSink};
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
use easytier_core::{ use easytier_core::{
connectivity::manual::ManualTunnelConnector, connectivity::manual::ManualTunnelConnector,
host::dns::{DnsRecordResolver, DnsResolver}, host::dns::{DnsRecordResolver, DnsResolver},
instance::CoreInstanceConfig,
}; };
use easytier_core::{ use easytier_core::{
events::{CoreEvent, CoreEventSink}, events::{CoreEvent, CoreEventSink},
instance::{CoreHostAdapters, CoreInstance, PacketEgressHost}, instance::{CoreHostAdapters, CoreInstance, CoreInstanceConfig, PacketEgressHost},
process_runtime::CoreProcessRuntime, process_runtime::CoreProcessRuntime,
}; };
@@ -25,7 +24,9 @@ use crate::{
common::global_ctx::ArcGlobalCtx, common::global_ctx::ArcGlobalCtx,
common::{config::TomlConfig, global_ctx::GlobalCtx}, common::{config::TomlConfig, global_ctx::GlobalCtx},
host_runtime::native_host_runtime, 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::listeners::RuntimeExternalListenerFactory,
instance::runtime_host::NativeInstanceRuntimeHost, instance::runtime_host::NativeInstanceRuntimeHost,
}; };
@@ -42,18 +43,30 @@ use easytier_core::gateway::proxy::wrapped_transport::WrappedTransportEngine;
pub(crate) type NativeCoreInstance = CoreInstance<NativeInstanceHost>; pub(crate) type NativeCoreInstance = CoreInstance<NativeInstanceHost>;
pub(crate) fn compose_native_core_instance( pub(crate) fn compose_native_core_instance(
config: TomlConfig, toml_config: TomlConfig,
process_runtime: Arc<CoreProcessRuntime>, process_runtime: Arc<CoreProcessRuntime>,
compact_runtime: bool,
) -> anyhow::Result<Arc<NativeCoreInstance>> { ) -> 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 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(), global_ctx.clone(),
process_runtime, process_runtime,
runtime_host.clone(), runtime_host.clone(),
host_config,
); );
adapters.instance_runtime = runtime_host; adapters.instance_runtime = runtime_host;
NativeCoreInstance::from_toml(config, adapters) NativeCoreInstance::from_toml(toml_config, adapters)
} }
impl CoreEventSink for GlobalCtx { impl CoreEventSink for GlobalCtx {
@@ -139,13 +152,19 @@ impl CoreEventSink for GlobalCtx {
} }
#[cfg(feature = "wrapped-transport")] #[cfg(feature = "wrapped-transport")]
fn runtime_wrapped_transport_engines() -> WrappedTransportEngines { fn runtime_wrapped_transport_engines(
config: &easytier_core::instance::CoreInstanceHostConfig,
) -> WrappedTransportEngines {
#[cfg(feature = "kcp")] #[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"))] #[cfg(not(feature = "kcp"))]
let kcp = None; let kcp = None;
#[cfg(feature = "quic")] #[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"))] #[cfg(not(feature = "quic"))]
let quic = None; let quic = None;
@@ -161,9 +180,10 @@ pub(crate) fn runtime_core_host_adapters(
let host = native_instance_host(global_ctx.clone()); let host = native_instance_host(global_ctx.clone());
let runtime_dns = native_host_runtime(); let runtime_dns = native_host_runtime();
let adapters = CoreHostAdapters::new(host, runtime_dns, packet_sink, process_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( pub(crate) fn runtime_core_host_adapters_with_packet_egress(
global_ctx: ArcGlobalCtx, global_ctx: ArcGlobalCtx,
process_runtime: Arc<CoreProcessRuntime>, 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 runtime_dns = native_host_runtime();
let adapters = let adapters =
CoreHostAdapters::new_with_packet_egress(host, runtime_dns, packet_egress, process_runtime); 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( fn configure_runtime_core_host_adapters(
global_ctx: ArcGlobalCtx, global_ctx: ArcGlobalCtx,
mut adapters: CoreHostAdapters<NativeInstanceHost>, mut adapters: CoreHostAdapters<NativeInstanceHost>,
host_config: easytier_core::instance::CoreInstanceHostConfig,
) -> CoreHostAdapters<NativeInstanceHost> { ) -> CoreHostAdapters<NativeInstanceHost> {
#[cfg(test)] #[cfg(test)]
adapters.replace_stun_provider(Arc::new(crate::common::stun::MockStunInfoCollector { adapters.replace_stun_provider(Arc::new(crate::common::stun::MockStunInfoCollector {
udp_nat_type: crate::proto::common::NatType::Unknown, udp_nat_type: crate::proto::common::NatType::Unknown,
})); }));
adapters.config = runtime_core_host_config(); adapters.config = host_config.clone();
adapters.credential_storage = runtime_peer_credential_storage(&global_ctx); adapters.credential_storage = (!host_config.ignore_unsupported_config)
.then(|| runtime_peer_credential_storage(&global_ctx))
.flatten();
adapters.events = global_ctx.clone(); adapters.events = global_ctx.clone();
#[cfg(feature = "wrapped-transport")] #[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.protocol = Some(runtime_client_protocol_upgrader(global_ctx.clone()));
adapters.external_listener_factory = Some(Arc::new(RuntimeExternalListenerFactory)); adapters.external_listener_factory = Some(Arc::new(RuntimeExternalListenerFactory));
adapters.server_protocol = Some(runtime_server_protocol_upgrader(global_ctx.clone())); adapters.server_protocol = Some(runtime_server_protocol_upgrader(global_ctx.clone()));
#[cfg(feature = "upnp")] #[cfg(feature = "upnp")]
{ if host_config.upnp_enabled {
adapters.udp_hole_punch_platform = Some( adapters.udp_hole_punch_platform = Some(
crate::instance::udp_hole_punch::runtime_udp_hole_punch_platform( crate::instance::udp_hole_punch::runtime_udp_hole_punch_platform(
global_ctx.net_ns.clone(), global_ctx.net_ns.clone(),
@@ -211,12 +247,12 @@ fn configure_runtime_core_host_adapters(
adapters.proxy_cidr_monitor_enabled = true; adapters.proxy_cidr_monitor_enabled = true;
} }
#[cfg(feature = "public-ipv6-provider")] #[cfg(feature = "public-ipv6-provider")]
{ if host_config.public_ipv6_provider_supported {
adapters.public_ipv6_host = Some(global_ctx.clone()); adapters.public_ipv6_host = Some(global_ctx.clone());
adapters.public_ipv6_provider = Some(runtime_public_ipv6_provider_platform(&global_ctx)); adapters.public_ipv6_provider = Some(runtime_public_ipv6_provider_platform(&global_ctx));
} }
#[cfg(feature = "wireguard")] #[cfg(feature = "wireguard")]
{ if host_config.vpn_portal_enabled {
use crate::common::config::ConfigLoader as _; use crate::common::config::ConfigLoader as _;
adapters.vpn_portal = Some(crate::vpn_portal::wireguard::WireGuardPortalHost::new( adapters.vpn_portal = Some(crate::vpn_portal::wireguard::WireGuardPortalHost::new(
@@ -230,7 +266,7 @@ fn configure_runtime_core_host_adapters(
adapters adapters
} }
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub(crate) fn runtime_one_shot_manual_connector( pub(crate) fn runtime_one_shot_manual_connector(
global_ctx: ArcGlobalCtx, global_ctx: ArcGlobalCtx,
config: &TomlConfig, 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"), all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos" 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"), 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(), easytier_version: EASYTIER_VERSION.to_owned(),
endpoint_protocols: IpScheme::VARIANTS.iter().map(ToString::to_string).collect(), 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( pub(crate) fn runtime_peer_credential_storage(
global_ctx: &ArcGlobalCtx, global_ctx: &ArcGlobalCtx,
) -> Option<Arc<dyn CredentialStorage>> { ) -> 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()) let config = TomlConfig::new_from_str(&global_ctx.config.dump())
.expect("test configuration should round-trip through TOML"); .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 mut host = runtime_core_host_config();
let hostname = global_ctx.get_hostname(); let hostname = global_ctx.get_hostname();
host.hostname_fallback = (!hostname.is_empty()).then_some(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.smoltcp_available, cfg!(feature = "smoltcp"));
assert_eq!( assert_eq!(
config.public_ipv6_provider_supported, 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); assert_eq!(config.easytier_version, EASYTIER_VERSION);
} }
@@ -403,7 +403,7 @@ impl MagicDnsServerInstance {
rpc_server.set_hook(data.clone()); rpc_server.set_hook(data.clone());
// Use configured tld_dns_zone or fall back to DEFAULT_ET_DNS_ZONE if empty // 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(); let tld_dns_zone_clone = flags.tld_dns_zone.clone();
data.update_dns_records(std::iter::empty(), &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)) 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")] #[cfg(feature = "management")]
pub fn native_process_management( pub fn native_process_management(
instances: Arc<NativeInstanceManager>, instances: Arc<NativeInstanceManager>,
@@ -85,7 +98,8 @@ fn native_instance_manager_with_optional_runtime(
pub struct NativeInstanceFactory { pub struct NativeInstanceFactory {
process_runtime: Arc<CoreProcessRuntime>, process_runtime: Arc<CoreProcessRuntime>,
runtime_handle: Option<tokio::runtime::Handle>, runtime_handle: Option<tokio::runtime::Handle>,
#[cfg(feature = "management")] compact_runtime: bool,
#[cfg(feature = "logging")]
log_cli_events: bool, log_cli_events: bool,
} }
@@ -94,12 +108,13 @@ impl NativeInstanceFactory {
Self { Self {
process_runtime, process_runtime,
runtime_handle: None, runtime_handle: None,
#[cfg(feature = "management")] compact_runtime: false,
#[cfg(feature = "logging")]
log_cli_events: false, log_cli_events: false,
} }
} }
#[cfg(feature = "management")] #[cfg(feature = "logging")]
fn with_cli_event_logging(mut self) -> Self { fn with_cli_event_logging(mut self) -> Self {
self.log_cli_events = true; self.log_cli_events = true;
self self
@@ -110,6 +125,11 @@ impl NativeInstanceFactory {
self.runtime_handle = runtime_handle; self.runtime_handle = runtime_handle;
self self
} }
fn with_compact_runtime(mut self) -> Self {
self.compact_runtime = true;
self
}
} }
impl InstanceFactory for NativeInstanceFactory { impl InstanceFactory for NativeInstanceFactory {
@@ -126,8 +146,12 @@ impl InstanceFactory for NativeInstanceFactory {
.runtime_handle .runtime_handle
.as_ref() .as_ref()
.map(tokio::runtime::Handle::enter); .map(tokio::runtime::Handle::enter);
let instance = compose_native_core_instance(config, self.process_runtime.clone())?; let instance = compose_native_core_instance(
#[cfg(feature = "management")] config,
self.process_runtime.clone(),
self.compact_runtime,
)?;
#[cfg(feature = "logging")]
if self.log_cli_events { if self.log_cli_events {
let events = subscribe_native_instance_event(&instance) let events = subscribe_native_instance_event(&instance)
.ok_or_else(|| anyhow::anyhow!("native instance runtime host is unavailable"))?; .ok_or_else(|| anyhow::anyhow!("native instance runtime host is unavailable"))?;
+2 -2
View File
@@ -25,7 +25,7 @@ pub struct NativeInstanceEnvironment {
impl NativeInstanceEnvironment { impl NativeInstanceEnvironment {
fn new(global_ctx: ArcGlobalCtx, runtime: Arc<NativeHostRuntime>) -> Self { fn new(global_ctx: ArcGlobalCtx, runtime: Arc<NativeHostRuntime>) -> Self {
let socket_context = SocketContext::default() 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)); .with_netns(global_ctx.net_ns.name().map(NetNamespace::new));
Self { Self {
global_ctx, global_ctx,
@@ -49,7 +49,7 @@ impl ConnectorEnvironment for NativeInstanceEnvironment {
} }
fn mapped_listeners(&self) -> Vec<url::Url> { 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 { 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 cli_event_logger;
pub(crate) mod composition; pub(crate) mod composition;
pub(crate) mod config; pub(crate) mod config;
+110 -1
View File
@@ -1,7 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use easytier_core::{ 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::sync::Mutex;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -77,6 +78,41 @@ impl NativeInstanceRuntimeHost {
self.event_journal.events() 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 { pub(crate) fn subscribe_event(&self) -> crate::common::global_ctx::EventBusSubscriber {
self.global_ctx.subscribe() self.global_ctx.subscribe()
} }
@@ -98,6 +134,15 @@ mod tests {
global_ctx::{GlobalCtx, GlobalCtxEvent}, 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] #[test]
fn runtime_host_owns_event_subscription_context() { fn runtime_host_owns_event_subscription_context() {
let global_ctx = Arc::new(GlobalCtx::new(TomlConfig::default())); let global_ctx = Arc::new(GlobalCtx::new(TomlConfig::default()));
@@ -111,4 +156,68 @@ mod tests {
GlobalCtxEvent::CredentialChanged 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() 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( pub(super) fn publish_config_patch(
&self, &self,
patch: crate::proto::api::config::InstanceConfigPatch, patch: crate::proto::api::config::InstanceConfigPatch,
@@ -29,14 +29,21 @@ impl InstanceRuntimeHost for NativeInstanceRuntimeHost {
self.management_events_snapshot() self.management_events_snapshot()
} }
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
fn synchronize_config(&self, patch: &crate::proto::api::config::InstanceConfigPatch) { fn synchronize_config(
self.event_journal.synchronize_config(patch); &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) { fn publish_config_patch(&self, patch: crate::proto::api::config::InstanceConfigPatch) {
#[cfg(feature = "management")]
self.event_journal.publish_config_patch(patch); self.event_journal.publish_config_patch(patch);
#[cfg(not(feature = "management"))]
let _ = patch;
} }
fn attach_tun_fd(&self, fd: i32) -> anyhow::Result<()> { 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; use crate::common::global_ctx::ArcGlobalCtx;
#[cfg(feature = "magic-dns")] #[cfg(feature = "magic-dns")]
use crate::{ use crate::instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
common::config::ConfigLoader as _,
instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner},
};
#[derive(Default)] #[derive(Default)]
pub(super) struct MagicDnsRuntime { pub(super) struct MagicDnsRuntime {
@@ -30,7 +27,7 @@ impl MagicDnsRuntime {
tun_dev: Option<String>, tun_dev: Option<String>,
tun_ip: Ipv4Inet, tun_ip: Ipv4Inet,
) -> Self { ) -> 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( let mut runner = DnsRunner::new(
packet_plane, packet_plane,
global_ctx, global_ctx,
@@ -17,7 +17,6 @@ use tokio_util::sync::CancellationToken;
use super::{MagicDnsRuntime, tun_common::TunNicState}; use super::{MagicDnsRuntime, tun_common::TunNicState};
use crate::{ use crate::{
common::{ common::{
config::ConfigLoader as _,
error::Error, error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
}, },
@@ -133,7 +132,7 @@ impl NativeTunRuntime {
pub(super) async fn prepare(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> { pub(super) async fn prepare(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
self.nic.drain().await; 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?; self.start_static_ip(packet_plane).await?;
} }
Ok(()) Ok(())
+1 -1
View File
@@ -663,7 +663,7 @@ impl VirtualNic {
let dev = AsyncDevice::new(dev)?; 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; let mut mtu_in_config = flags.mtu;
if flags.enable_encryption { if flags.enable_encryption {
mtu_in_config -= 20; mtu_in_config -= 20;
+1 -1
View File
@@ -20,7 +20,7 @@ pub mod service_manager;
pub(crate) mod socket; pub(crate) mod socket;
pub mod tunnel; pub mod tunnel;
pub mod utils; pub mod utils;
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub mod web_client; pub mod web_client;
#[cfg(test)] #[cfg(test)]
+1 -1
View File
@@ -1,5 +1,5 @@
pub use easytier_proto::api; pub use easytier_proto::api;
#[cfg(feature = "management")] #[cfg(feature = "web-client")]
pub use easytier_proto::web; pub use easytier_proto::web;
pub use easytier_proto::{ pub use easytier_proto::{
ALL_DESCRIPTOR_BYTES, acl, common, core_config, error, peer_rpc, rpc_types, 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 anyhow::{Context as _, Result};
use async_trait::async_trait; use async_trait::async_trait;
use easytier_core::{ use easytier_core::{
config::toml::ConfigLoader as _,
connectivity::{manual::ManualTunnelConnector, protocol::raw::TunnelDialer}, connectivity::{manual::ManualTunnelConnector, protocol::raw::TunnelDialer},
management::{ConfigServerEndpoint, WebClientConfig}, management::{ConfigServerEndpoint, WebClientConfig},
socket::IpVersion, socket::IpVersion,
@@ -10,18 +11,21 @@ use easytier_core::{
}; };
use url::Url; 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::{ use crate::{
common::{ common::{
MachineIdOptions, config::TomlConfigLoader, constants::EASYTIER_VERSION, 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::{ instance::{
composition::runtime_one_shot_manual_connector, composition::runtime_one_shot_manual_connector,
config_storage::NativeConfigFileStorage,
factory::{NativeInstanceFactory, NativeInstanceManager}, factory::{NativeInstanceFactory, NativeInstanceManager},
host::NativeInstanceHost, host::NativeInstanceHost,
}, },
rpc_service::logger::NativeLoggerControl,
tunnel::TunnelScheme, tunnel::TunnelScheme,
}; };
@@ -46,23 +50,32 @@ impl WebClient {
S: ToString, S: ToString,
H: ToString, H: ToString,
{ {
Self { let config = WebClientConfig {
inner: easytier_core::management::WebClient::new( token: token.to_string(),
connector, machine_id,
WebClientConfig { hostname: hostname.to_string(),
token: token.to_string(), device_os: web_client_device_os_info(),
machine_id, easytier_version: EASYTIER_VERSION.to_owned(),
hostname: hostname.to_string(), secure_mode,
device_os: collect_device_os_info(), };
easytier_version: EASYTIER_VERSION.to_owned(), #[cfg(feature = "management")]
secure_mode, let inner = easytier_core::management::WebClient::new(
}, connector,
manager, config,
hooks.unwrap_or_else(|| Arc::new(DefaultHooks)), manager,
Arc::new(NativeConfigFileStorage), hooks.unwrap_or_else(|| Arc::new(DefaultHooks)),
Arc::new(NativeLoggerControl), 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 { 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; pub struct DefaultHooks;
#[async_trait] #[async_trait]
@@ -113,6 +140,7 @@ pub async fn run_web_client(
let global_ctx = Arc::new(GlobalCtx::new(config.clone())); let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
let mut flags = global_ctx.get_flags(); let mut flags = global_ctx.get_flags();
flags.bind_device = false; flags.bind_device = false;
config.set_flags(flags.clone());
global_ctx.set_flags(flags); global_ctx.set_flags(flags);
let hostname = let hostname =
hostname.unwrap_or_else(|| gethostname::gethostname().to_string_lossy().to_string()); hostname.unwrap_or_else(|| gethostname::gethostname().to_string_lossy().to_string());