mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-02 17:15:43 +00:00
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:
@@ -21,3 +21,10 @@ operation transition.
|
||||
|
||||
Host capability operations use a separate seam. They turn Host readiness into
|
||||
Rust task wakeups and do not share the caller-to-core broker state machine.
|
||||
|
||||
## Compact compatibility Host
|
||||
|
||||
A compact compatibility Host retains accepted values in the authoritative TOML
|
||||
model for management readback, while the shared host-aware normalization path
|
||||
omits capabilities that the compact runtime cannot execute. Omitted settings
|
||||
are silent no-ops and must not be advertised as live network capabilities.
|
||||
|
||||
Generated
+9
@@ -2528,6 +2528,15 @@ dependencies = [
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-mini"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"easytier",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-proto"
|
||||
version = "2.6.4"
|
||||
|
||||
@@ -6,6 +6,7 @@ members = [
|
||||
"easytier",
|
||||
"easytier-gui/src-tauri",
|
||||
"easytier-web",
|
||||
"easytier-contrib/easytier-mini",
|
||||
"easytier-contrib/easytier-ffi",
|
||||
"easytier-contrib/easytier-uptime",
|
||||
"easytier-contrib/easytier-android-jni",
|
||||
@@ -29,3 +30,8 @@ lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = true
|
||||
|
||||
[profile.mini]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
strip = "symbols"
|
||||
|
||||
@@ -217,7 +217,10 @@ are not independent owners of EasyTier peer state.
|
||||
|
||||
Each protocol registration must provide a coherent client/server Adapter.
|
||||
Unavailable configured transports must be rejected during validation or
|
||||
protocol selection, rather than silently falling back to another transport.
|
||||
protocol selection in the standard runtime, rather than silently falling back
|
||||
to another transport. A compact compatibility Host may instead retain the
|
||||
desired value for management readback and omit it from normalized runtime
|
||||
state; it must not advertise or partially activate the unavailable transport.
|
||||
|
||||
### Connectivity
|
||||
|
||||
@@ -283,8 +286,9 @@ Adapters.
|
||||
|
||||
Optional gateway capabilities are selected by cohesive Modules. Disabled
|
||||
implementations retain stable lifecycle calls and report unsupported
|
||||
configuration where a stable interface is required; they do not duplicate
|
||||
portable policy.
|
||||
configuration in the standard runtime. A compact compatibility Host may
|
||||
silently normalize those settings to no-ops while preserving the desired TOML
|
||||
model; disabled implementations do not duplicate portable policy.
|
||||
|
||||
The instance-scoped `DataPlaneSession` composes the foundation operation broker
|
||||
under the same session lock as its resource and quota state. The broker owns
|
||||
@@ -368,10 +372,12 @@ configuration, or connectivity state.
|
||||
|
||||
## Runtime configuration authority
|
||||
|
||||
`TomlConfig` is an owned construction input. After startup, it is not a second
|
||||
mutable source of truth.
|
||||
`TomlConfig` is the authoritative desired configuration used for management
|
||||
readback and patch transactions. Compact Hosts keep unsupported accepted values
|
||||
there so controllers observe the configuration they submitted.
|
||||
|
||||
The normalized core runtime store is authoritative for:
|
||||
The separately typed, normalized core runtime store is authoritative for live
|
||||
behavior:
|
||||
|
||||
- peer feature flags and routing policy;
|
||||
- listeners and initial peers;
|
||||
@@ -477,8 +483,9 @@ configuration, management, packet-plane, or test-support interfaces.
|
||||
14. Unknown protobuf fields in reflected route information survive forwarding
|
||||
and credential filtering.
|
||||
15. Feature selection is localized at cohesive Module/Adapter boundaries.
|
||||
16. Unsupported configured capabilities fail explicitly rather than changing
|
||||
wire protocol or silently falling back.
|
||||
16. The standard runtime rejects unsupported configured capabilities. Compact
|
||||
compatibility Hosts may preserve them as runtime no-ops, but never change
|
||||
wire protocol, advertise them, or silently fall back to an unsafe mode.
|
||||
|
||||
## Validation
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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.
|
||||
Executable
+61
@@ -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
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,8 @@ extended-services = [
|
||||
"wrapped-transport",
|
||||
"proxy-cidr-monitor",
|
||||
]
|
||||
management = ["management-rpc", "config-write", "extended-services", "rich-config-errors", "easytier-proto/json-rpc"]
|
||||
web-client = ["management-rpc", "config-write"]
|
||||
management = ["web-client", "extended-services", "rich-config-errors", "easytier-proto/json-rpc"]
|
||||
management-rpc = ["easytier-proto/api"]
|
||||
proxy-cidr-monitor = []
|
||||
rich-config-errors = ["dep:ariadne"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Static configuration schema plus the live runtime configuration store.
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub mod api;
|
||||
#[cfg(any(feature = "management", feature = "browser-config"))]
|
||||
#[cfg(any(feature = "web-client", feature = "browser-config"))]
|
||||
pub mod api_input;
|
||||
#[cfg(all(
|
||||
feature = "browser-config",
|
||||
|
||||
@@ -95,6 +95,23 @@ impl CoreRuntimeConfigStore {
|
||||
.send_modify(|version| *version += 1);
|
||||
}
|
||||
|
||||
pub(crate) fn replace_with_current(
|
||||
&self,
|
||||
mut config: CoreInstanceRuntimeConfig,
|
||||
merge: impl FnOnce(&CoreInstanceRuntimeConfig, &mut CoreInstanceRuntimeConfig),
|
||||
) -> Arc<CoreInstanceRuntimeConfig> {
|
||||
let _update = self.inner.update.lock();
|
||||
let current = self.inner.snapshot.load_full();
|
||||
merge(¤t, &mut config);
|
||||
let config = Arc::new(config);
|
||||
self.inner.snapshot.store(config.clone());
|
||||
self.inner.peer_changes.send_modify(|version| *version += 1);
|
||||
self.inner
|
||||
.service_changes
|
||||
.send_modify(|version| *version += 1);
|
||||
config
|
||||
}
|
||||
|
||||
pub fn update_services(&self, update: impl FnOnce(&mut CoreRuntimeConfig)) {
|
||||
let _update = self.inner.update.lock();
|
||||
let mut config = self.inner.snapshot.load_full().as_ref().clone();
|
||||
|
||||
@@ -624,7 +624,7 @@ impl TomlConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
mod snapshot;
|
||||
|
||||
impl ConfigLoader for TomlConfig {
|
||||
|
||||
@@ -4,11 +4,11 @@ use std::collections::BTreeSet;
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
IpPrefix, NodeConfig, ProxyNetworkConfig, RouteConfig,
|
||||
EncryptionAlgorithm, IpPrefix, NodeConfig, ProxyNetworkConfig, RouteConfig,
|
||||
gateway::{GatewayRuntimeConfig, ProxyRuntimeConfig},
|
||||
peers::{AclRuleConfig, HostRoutingPolicy, PublicIpv6ProviderConfig},
|
||||
runtime::CoreRuntimeConfig,
|
||||
toml::{ConfigLoader as _, TomlConfig},
|
||||
toml::{ConfigLoader as _, Flags, TomlConfig},
|
||||
},
|
||||
connectivity::{
|
||||
direct::DirectConnectorOptions,
|
||||
@@ -16,13 +16,17 @@ use crate::{
|
||||
stun::StunServerConfig,
|
||||
},
|
||||
listener::plan::ListenerRuntimeConfig,
|
||||
packet::CompressorAlgo,
|
||||
peers::{
|
||||
context::PeerRuntimeSnapshotInput,
|
||||
peer_manager::{PortablePeerManagerConfig, RouteAlgoType},
|
||||
},
|
||||
socket::{NetNamespace, SocketContext, tcp::TcpBindOptions, udp::UdpBindOptions},
|
||||
tunnel::encrypt::algorithm_is_available,
|
||||
};
|
||||
|
||||
use easytier_proto::common::CompressionAlgoPb;
|
||||
|
||||
use super::{CoreConnectivityConfig, CoreInstanceConfig};
|
||||
|
||||
const OSPF_UPDATE_MY_FOREIGN_NETWORK_INTERVAL_SEC: u64 = 10;
|
||||
@@ -44,6 +48,15 @@ pub struct CoreInstanceHostConfig {
|
||||
pub icmp_failure_is_fatal: bool,
|
||||
pub public_ipv6_provider_supported: bool,
|
||||
pub gateway_enabled: bool,
|
||||
pub proxy_enabled: bool,
|
||||
pub vpn_portal_enabled: bool,
|
||||
pub magic_dns_enabled: bool,
|
||||
pub kcp_enabled: bool,
|
||||
pub quic_enabled: bool,
|
||||
pub udp_broadcast_enabled: bool,
|
||||
pub upnp_enabled: bool,
|
||||
pub tcp_hole_punching_enabled: bool,
|
||||
pub ignore_unsupported_config: bool,
|
||||
pub easytier_version: String,
|
||||
pub endpoint_protocols: Vec<String>,
|
||||
}
|
||||
@@ -60,12 +73,87 @@ impl Default for CoreInstanceHostConfig {
|
||||
icmp_failure_is_fatal: false,
|
||||
public_ipv6_provider_supported: false,
|
||||
gateway_enabled: true,
|
||||
proxy_enabled: true,
|
||||
vpn_portal_enabled: true,
|
||||
magic_dns_enabled: true,
|
||||
kcp_enabled: true,
|
||||
quic_enabled: true,
|
||||
udp_broadcast_enabled: true,
|
||||
upnp_enabled: true,
|
||||
tcp_hole_punching_enabled: true,
|
||||
ignore_unsupported_config: false,
|
||||
easytier_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
endpoint_protocols: ManualEndpointDiscoveryConfig::default().srv_protocols,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreInstanceHostConfig {
|
||||
pub(crate) fn accepts_runtime_url(&self, url: &url::Url) -> bool {
|
||||
!self.ignore_unsupported_config
|
||||
|| self
|
||||
.endpoint_protocols
|
||||
.iter()
|
||||
.any(|scheme| scheme.eq_ignore_ascii_case(url.scheme()))
|
||||
}
|
||||
|
||||
fn runtime_flags(&self, mut flags: Flags) -> Flags {
|
||||
if !self.ignore_unsupported_config {
|
||||
return flags;
|
||||
}
|
||||
|
||||
if !self.smoltcp_available {
|
||||
flags.no_tun = false;
|
||||
flags.use_smoltcp = false;
|
||||
}
|
||||
if !self.proxy_enabled {
|
||||
flags.enable_exit_node = false;
|
||||
}
|
||||
if !self.magic_dns_enabled {
|
||||
flags.accept_dns = false;
|
||||
}
|
||||
if !self.kcp_enabled {
|
||||
flags.enable_kcp_proxy = false;
|
||||
flags.disable_kcp_input = true;
|
||||
flags.disable_relay_kcp = true;
|
||||
flags.enable_relay_foreign_network_kcp = false;
|
||||
}
|
||||
if !self.quic_enabled {
|
||||
flags.enable_quic_proxy = false;
|
||||
flags.disable_quic_input = true;
|
||||
flags.disable_relay_quic = true;
|
||||
flags.enable_relay_foreign_network_quic = false;
|
||||
}
|
||||
if !self.udp_broadcast_enabled {
|
||||
flags.enable_udp_broadcast_relay = false;
|
||||
}
|
||||
if !self.upnp_enabled {
|
||||
flags.disable_upnp = true;
|
||||
}
|
||||
if !self.tcp_hole_punching_enabled {
|
||||
flags.disable_tcp_hole_punching = true;
|
||||
}
|
||||
if CompressionAlgoPb::try_from(flags.data_compress_algo)
|
||||
.ok()
|
||||
.and_then(|algorithm| CompressorAlgo::try_from(algorithm).ok())
|
||||
.is_some_and(|algorithm| !algorithm.is_available())
|
||||
{
|
||||
flags.data_compress_algo = CompressionAlgoPb::None as i32;
|
||||
}
|
||||
|
||||
if flags
|
||||
.encryption_algorithm
|
||||
.parse::<EncryptionAlgorithm>()
|
||||
.is_ok_and(|algorithm| !algorithm_is_available(algorithm))
|
||||
&& algorithm_is_available(EncryptionAlgorithm::AesGcm)
|
||||
{
|
||||
flags.encryption_algorithm = EncryptionAlgorithm::AesGcm.to_string();
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreInstanceConfig {
|
||||
/// Normalizes the complete shared TOML model using OS-independent defaults.
|
||||
///
|
||||
@@ -81,7 +169,7 @@ impl CoreInstanceConfig {
|
||||
config: &TomlConfig,
|
||||
host: &CoreInstanceHostConfig,
|
||||
) -> anyhow::Result<Self> {
|
||||
let flags = config.get_flags();
|
||||
let flags = host.runtime_flags(config.get_flags());
|
||||
let instance_id = config.get_id();
|
||||
let identity: crate::config::NetworkIdentity = config.get_network_identity().into();
|
||||
let network_name = identity.network_name.clone();
|
||||
@@ -93,6 +181,16 @@ impl CoreInstanceConfig {
|
||||
_ => host.hostname_fallback.clone().unwrap_or_default(),
|
||||
};
|
||||
let acl = config.get_acl();
|
||||
let peers = config
|
||||
.get_peers()
|
||||
.into_iter()
|
||||
.filter(|peer| host.accepts_runtime_url(&peer.uri))
|
||||
.collect::<Vec<_>>();
|
||||
let proxy_networks = if host.ignore_unsupported_config && !host.proxy_enabled {
|
||||
Vec::new()
|
||||
} else {
|
||||
config.get_proxy_cidrs()
|
||||
};
|
||||
|
||||
let peer_snapshot =
|
||||
crate::config::peers::PeerRuntimeSnapshot::from_host_input(PeerRuntimeSnapshotInput {
|
||||
@@ -111,8 +209,7 @@ impl CoreInstanceConfig {
|
||||
address: value.address().into(),
|
||||
prefix_len: value.network_length(),
|
||||
}),
|
||||
proxy_networks: config
|
||||
.get_proxy_cidrs()
|
||||
proxy_networks: proxy_networks
|
||||
.into_iter()
|
||||
.map(|proxy| ProxyNetworkConfig {
|
||||
real: IpPrefix {
|
||||
@@ -134,12 +231,13 @@ impl CoreInstanceConfig {
|
||||
host_routing: host.host_routing,
|
||||
acl: acl.clone(),
|
||||
easytier_version: host.easytier_version.clone(),
|
||||
vpn_portal_cidr: config
|
||||
.get_vpn_portal_config()
|
||||
vpn_portal_cidr: (!host.ignore_unsupported_config || host.vpn_portal_enabled)
|
||||
.then(|| config.get_vpn_portal_config())
|
||||
.flatten()
|
||||
.map(|portal| portal.client_cidr),
|
||||
pinned_peers: config
|
||||
.get_peers()
|
||||
.into_iter()
|
||||
pinned_peers: peers
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|peer| (peer.uri, peer.peer_public_key))
|
||||
.collect(),
|
||||
ospf_update_my_foreign_network_interval_sec:
|
||||
@@ -151,19 +249,28 @@ impl CoreInstanceConfig {
|
||||
let peer = PortablePeerManagerConfig {
|
||||
snapshot: peer_snapshot,
|
||||
route_algo: RouteAlgoType::Ospf,
|
||||
exit_nodes: config.get_exit_nodes(),
|
||||
foreign_context_default_flags: TomlConfig::default().get_flags(),
|
||||
exit_nodes: if host.ignore_unsupported_config && !host.proxy_enabled {
|
||||
Vec::new()
|
||||
} else {
|
||||
config.get_exit_nodes()
|
||||
},
|
||||
foreign_context_default_flags: host.runtime_flags(TomlConfig::default().get_flags()),
|
||||
};
|
||||
|
||||
let tcp_bind = TcpBindOptions::default().with_context(socket_context.clone());
|
||||
let udp_bind = UdpBindOptions::direct_connect().with_context(socket_context.clone());
|
||||
let listeners = Some(ListenerRuntimeConfig::new(
|
||||
config.get_listener_uris(),
|
||||
config
|
||||
.get_listener_uris()
|
||||
.into_iter()
|
||||
.filter(|url| host.accepts_runtime_url(url))
|
||||
.collect(),
|
||||
flags.enable_ipv6,
|
||||
socket_context.clone(),
|
||||
));
|
||||
let socks5_bind = config
|
||||
.get_socks5_portal()
|
||||
let socks5_bind = (!host.ignore_unsupported_config || host.gateway_enabled)
|
||||
.then(|| config.get_socks5_portal())
|
||||
.flatten()
|
||||
.map(|url| {
|
||||
let host = url
|
||||
.host_str()
|
||||
@@ -186,7 +293,11 @@ impl CoreInstanceConfig {
|
||||
dhcp_ipv4: config.get_dhcp(),
|
||||
gateway: GatewayRuntimeConfig {
|
||||
socks5_bind,
|
||||
port_forwards: config.get_port_forwards(),
|
||||
port_forwards: if host.ignore_unsupported_config && !host.gateway_enabled {
|
||||
Vec::new()
|
||||
} else {
|
||||
config.get_port_forwards()
|
||||
},
|
||||
},
|
||||
manual_routes: config
|
||||
.get_routes()
|
||||
@@ -200,10 +311,15 @@ impl CoreInstanceConfig {
|
||||
icmp_failure_is_fatal: host.icmp_failure_is_fatal,
|
||||
udp_response_ipv4_mtu: 1280,
|
||||
},
|
||||
public_ipv6_auto: config.get_ipv6_public_addr_auto(),
|
||||
public_ipv6_auto: config.get_ipv6_public_addr_auto()
|
||||
&& (!host.ignore_unsupported_config || host.public_ipv6_provider_supported),
|
||||
public_ipv6_provider: PublicIpv6ProviderConfig {
|
||||
provider_enabled: config.get_ipv6_public_addr_provider(),
|
||||
configured_prefix: config.get_ipv6_public_addr_prefix(),
|
||||
provider_enabled: config.get_ipv6_public_addr_provider()
|
||||
&& (!host.ignore_unsupported_config || host.public_ipv6_provider_supported),
|
||||
configured_prefix: (!host.ignore_unsupported_config
|
||||
|| host.public_ipv6_provider_supported)
|
||||
.then(|| config.get_ipv6_public_addr_prefix())
|
||||
.flatten(),
|
||||
provider_supported: host.public_ipv6_provider_supported,
|
||||
},
|
||||
};
|
||||
@@ -212,11 +328,7 @@ impl CoreInstanceConfig {
|
||||
instance_name: config.get_inst_name(),
|
||||
peer,
|
||||
connectivity: CoreConnectivityConfig {
|
||||
initial_peers: config
|
||||
.get_peers()
|
||||
.into_iter()
|
||||
.map(|peer| peer.uri)
|
||||
.collect(),
|
||||
initial_peers: peers.into_iter().map(|peer| peer.uri).collect(),
|
||||
listeners,
|
||||
runtime,
|
||||
startup_plan: super::CoreInstanceStartupPlan {
|
||||
@@ -341,6 +453,7 @@ disable_p2p = true
|
||||
gateway_enabled: false,
|
||||
easytier_version: "host-version".to_owned(),
|
||||
endpoint_protocols: vec!["host-protocol".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let normalized = CoreInstanceConfig::from_toml_with_host(&config, &host).unwrap();
|
||||
@@ -383,4 +496,97 @@ disable_p2p = true
|
||||
["host-protocol"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_capabilities_stay_in_toml_but_not_runtime_config() {
|
||||
let config = TomlConfig::new_from_str(
|
||||
r#"
|
||||
listeners = ["tcp://127.0.0.1:11010", "quic://127.0.0.1:11011"]
|
||||
proxy_network = [{ cidr = "10.20.0.0/16" }]
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://127.0.0.1:11010"
|
||||
|
||||
[[peer]]
|
||||
uri = "quic://127.0.0.1:11011"
|
||||
|
||||
[flags]
|
||||
enable_exit_node = true
|
||||
enable_kcp_proxy = true
|
||||
accept_dns = true
|
||||
encryption_algorithm = "chacha20"
|
||||
data_compress_algo = "Zstd"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
config.set_exit_nodes(vec!["10.144.144.2".parse().unwrap()]);
|
||||
config.set_ipv6_public_addr_provider(true);
|
||||
config.get_id();
|
||||
let before = config.dump();
|
||||
|
||||
let host = CoreInstanceHostConfig {
|
||||
ignore_unsupported_config: true,
|
||||
smoltcp_available: true,
|
||||
proxy_enabled: false,
|
||||
gateway_enabled: false,
|
||||
public_ipv6_provider_supported: false,
|
||||
magic_dns_enabled: false,
|
||||
kcp_enabled: false,
|
||||
quic_enabled: false,
|
||||
endpoint_protocols: vec!["tcp".to_owned(), "udp".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let normalized = CoreInstanceConfig::from_toml_with_host(&config, &host).unwrap();
|
||||
|
||||
assert_eq!(config.dump(), before);
|
||||
assert_eq!(normalized.connectivity.initial_peers.len(), 1);
|
||||
assert_eq!(
|
||||
normalized
|
||||
.connectivity
|
||||
.listeners
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.urls
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
normalized
|
||||
.peer
|
||||
.snapshot
|
||||
.runtime
|
||||
.core
|
||||
.routes
|
||||
.proxy_networks
|
||||
.is_empty()
|
||||
);
|
||||
assert!(normalized.peer.exit_nodes.is_empty());
|
||||
assert!(!normalized.connectivity.runtime.proxy.enable_exit_node);
|
||||
assert!(
|
||||
!normalized
|
||||
.connectivity
|
||||
.runtime
|
||||
.public_ipv6_provider
|
||||
.provider_enabled
|
||||
);
|
||||
let flags = &normalized.peer.snapshot.flags;
|
||||
assert!(!flags.enable_kcp_proxy);
|
||||
assert!(flags.disable_kcp_input);
|
||||
assert!(!flags.accept_dns);
|
||||
let expected_encryption = if algorithm_is_available(EncryptionAlgorithm::ChaCha20) {
|
||||
EncryptionAlgorithm::ChaCha20
|
||||
} else {
|
||||
EncryptionAlgorithm::AesGcm
|
||||
};
|
||||
assert_eq!(flags.encryption_algorithm, expected_encryption.to_string());
|
||||
assert_eq!(
|
||||
flags.data_compress_algo,
|
||||
if CompressorAlgo::ZstdDefault.is_available() {
|
||||
CompressionAlgoPb::Zstd as i32
|
||||
} else {
|
||||
CompressionAlgoPb::None as i32
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::runtime::CoreInstanceRuntimeConfig;
|
||||
|
||||
use super::{CoreInstance, CoreInstanceHost, CoreInstanceHostConfig};
|
||||
|
||||
impl<H> CoreInstance<H>
|
||||
@@ -12,10 +8,6 @@ where
|
||||
self.management.toml_config()
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_config_snapshot(&self) -> Arc<CoreInstanceRuntimeConfig> {
|
||||
self.runtime_config.snapshot()
|
||||
}
|
||||
|
||||
pub(crate) fn host_config(&self) -> &CoreInstanceHostConfig {
|
||||
self.management.host_config()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{config::toml::TomlConfig, instance::CoreInstanceHostConfig};
|
||||
|
||||
pub(super) struct ManagementState {
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
toml_config: Option<TomlConfig>,
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
host_config: CoreInstanceHostConfig,
|
||||
}
|
||||
|
||||
@@ -12,22 +12,22 @@ impl ManagementState {
|
||||
toml_config: Option<TomlConfig>,
|
||||
host_config: CoreInstanceHostConfig,
|
||||
) -> Self {
|
||||
#[cfg(not(feature = "management"))]
|
||||
#[cfg(not(feature = "web-client"))]
|
||||
let _ = (toml_config, host_config);
|
||||
Self {
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
toml_config,
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
host_config,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub(super) fn toml_config(&self) -> Option<TomlConfig> {
|
||||
self.toml_config.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub(super) fn host_config(&self) -> &CoreInstanceHostConfig {
|
||||
&self.host_config
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@ use uuid::Uuid;
|
||||
use crate::config::toml::TomlConfig;
|
||||
use crate::instance::{CoreInstance, CoreInstanceHost};
|
||||
use crate::process_runtime::CoreProcessRuntime;
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
use crate::{
|
||||
config::toml::{ConfigLoader as _, ConfigSource},
|
||||
management::network_instance_running_info,
|
||||
};
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
use easytier_proto::api::manage::NetworkInstanceRunningInfo;
|
||||
|
||||
/// Stable identity required by the instance collection.
|
||||
@@ -439,25 +439,25 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub fn config(&self, instance_id: Uuid) -> Option<TomlConfig> {
|
||||
self.get(instance_id)
|
||||
.and_then(|instance| instance.toml_config())
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub fn config_source(&self, instance_id: Uuid) -> Option<ConfigSource> {
|
||||
self.config(instance_id)
|
||||
.map(|config| config.get_network_config_source())
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub async fn network_info(&self, instance_id: Uuid) -> Option<NetworkInstanceRunningInfo> {
|
||||
let instance = self.get(instance_id)?;
|
||||
network_instance_running_info(instance.as_ref()).await.ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub async fn collect_network_infos(
|
||||
&self,
|
||||
) -> anyhow::Result<std::collections::BTreeMap<Uuid, NetworkInstanceRunningInfo>> {
|
||||
@@ -471,7 +471,7 @@ where
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub fn collect_network_infos_sync(
|
||||
&self,
|
||||
) -> anyhow::Result<std::collections::BTreeMap<Uuid, NetworkInstanceRunningInfo>> {
|
||||
|
||||
@@ -6,7 +6,7 @@ mod config;
|
||||
mod data_plane_extension;
|
||||
mod lifecycle;
|
||||
mod management;
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
mod management_extension;
|
||||
mod management_state;
|
||||
pub mod manager;
|
||||
@@ -214,6 +214,23 @@ fn retain_core_peer_identity(
|
||||
peer.runtime.core.node.instance_id = instance_id;
|
||||
}
|
||||
|
||||
fn retain_runtime_owned_peer_state(
|
||||
current: &CoreInstanceRuntimeConfig,
|
||||
next: &mut CoreInstanceRuntimeConfig,
|
||||
peer_id: crate::config::PeerId,
|
||||
) {
|
||||
retain_core_peer_identity(
|
||||
&mut next.peer,
|
||||
peer_id,
|
||||
current.peer.runtime.core.node.instance_id,
|
||||
);
|
||||
let next_peer = Arc::make_mut(&mut next.peer);
|
||||
next_peer.runtime.stun_info = current.peer.runtime.stun_info.clone();
|
||||
if current.services.dhcp_ipv4 && next.services.dhcp_ipv4 {
|
||||
next_peer.runtime.core.routes.ipv4 = current.peer.runtime.core.routes.ipv4.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/// Host-owned resources that must be prepared for the complete Instance
|
||||
/// lifetime, such as a native packet interface.
|
||||
#[async_trait::async_trait]
|
||||
@@ -238,10 +255,15 @@ pub trait InstanceRuntimeHost: std::any::Any + Send + Sync + 'static {
|
||||
|
||||
/// Applies Host-side cached views of fields already committed to the
|
||||
/// shared TOML model.
|
||||
#[cfg(feature = "management")]
|
||||
fn synchronize_config(&self, _patch: &crate::proto::api::config::InstanceConfigPatch) {}
|
||||
#[cfg(feature = "web-client")]
|
||||
fn synchronize_config(
|
||||
&self,
|
||||
_patch: &crate::proto::api::config::InstanceConfigPatch,
|
||||
_config: &CoreInstanceRuntimeConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
fn publish_config_patch(&self, _patch: crate::proto::api::config::InstanceConfigPatch) {}
|
||||
|
||||
fn attach_tun_fd(&self, _fd: i32) -> anyhow::Result<()> {
|
||||
@@ -846,7 +868,7 @@ where
|
||||
|
||||
pub(crate) async fn update_runtime_config_under_operation(
|
||||
&self,
|
||||
mut config: CoreInstanceRuntimeConfig,
|
||||
config: CoreInstanceRuntimeConfig,
|
||||
) -> anyhow::Result<()> {
|
||||
if matches!(
|
||||
self.state(),
|
||||
@@ -856,21 +878,23 @@ where
|
||||
}
|
||||
self.validate_runtime_config_capabilities(&config)?;
|
||||
let current = self.runtime_config.snapshot();
|
||||
retain_core_peer_identity(
|
||||
&mut config.peer,
|
||||
self.peer_id(),
|
||||
current.peer.runtime.core.node.instance_id,
|
||||
);
|
||||
let refresh_acl_groups = current.peer.peer_group_memberships
|
||||
!= config.peer.peer_group_memberships
|
||||
|| current.peer.acl_group_declarations != config.peer.acl_group_declarations;
|
||||
if current.services.acl != config.services.acl {
|
||||
self.reload_acl_config_inner(&config.services.acl).await?;
|
||||
}
|
||||
// Foreign-network watchers read this state after the runtime-config
|
||||
// notification, so publish it before replacing the watched snapshot.
|
||||
self.sync_peer_runtime_state(&config.peer);
|
||||
self.runtime_config.replace(config);
|
||||
let peer_id = self.peer_id();
|
||||
let published = self
|
||||
.runtime_config
|
||||
.replace_with_current(config, |current, next| {
|
||||
retain_runtime_owned_peer_state(current, next, peer_id);
|
||||
});
|
||||
self.proxy_cidr_table
|
||||
.update_snapshot(proxy_cidr_snapshot(self.runtime_config.snapshot().as_ref()));
|
||||
.update_snapshot(proxy_cidr_snapshot(&published));
|
||||
if refresh_acl_groups {
|
||||
self.refresh_acl_groups().await;
|
||||
}
|
||||
|
||||
@@ -466,6 +466,57 @@ mod portable_runtime {
|
||||
build_with_engines(config, WrappedTransportEngines::default())
|
||||
}
|
||||
|
||||
#[cfg(feature = "dhcp-ipv4")]
|
||||
#[tokio::test]
|
||||
async fn runtime_update_preserves_dhcp_owned_ipv4() {
|
||||
let mut initial = test_config("dhcp-runtime-update");
|
||||
initial.connectivity.runtime.dhcp_ipv4 = true;
|
||||
let instance = build_instance(initial).unwrap();
|
||||
let lease = IpPrefix {
|
||||
address: "10.126.126.7".parse().unwrap(),
|
||||
prefix_len: 24,
|
||||
};
|
||||
instance.runtime_config.update_peer_with(|peer| {
|
||||
peer.runtime.core.routes.ipv4 = Some(lease.clone());
|
||||
});
|
||||
|
||||
let mut replacement = test_config("dhcp-runtime-update");
|
||||
replacement.connectivity.runtime.dhcp_ipv4 = true;
|
||||
instance
|
||||
.update_runtime_config(runtime_snapshot(&replacement))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
instance
|
||||
.runtime_config
|
||||
.snapshot()
|
||||
.peer
|
||||
.runtime
|
||||
.core
|
||||
.routes
|
||||
.ipv4,
|
||||
Some(lease)
|
||||
);
|
||||
|
||||
let static_replacement = test_config("dhcp-runtime-update");
|
||||
instance
|
||||
.update_runtime_config(runtime_snapshot(&static_replacement))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
instance
|
||||
.runtime_config
|
||||
.snapshot()
|
||||
.peer
|
||||
.runtime
|
||||
.core
|
||||
.routes
|
||||
.ipv4,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_instance_is_a_direct_managed_record() {
|
||||
let instance = build_instance(test_config("managed-directly")).unwrap();
|
||||
@@ -600,7 +651,7 @@ hostname = "core-owned-config"
|
||||
response.config.unwrap().hostname.as_deref(),
|
||||
Some("patched-in-core")
|
||||
);
|
||||
let runtime = instance.runtime_config_snapshot();
|
||||
let runtime = instance.runtime_config.snapshot();
|
||||
assert!(runtime.services.proxy.enable_exit_node);
|
||||
assert!(runtime.services.public_ipv6_provider.provider_supported);
|
||||
assert_eq!(runtime.peer.easytier_version, "host-version");
|
||||
@@ -685,7 +736,8 @@ hostname = "core-owned-config"
|
||||
);
|
||||
assert!(
|
||||
instance
|
||||
.runtime_config_snapshot()
|
||||
.runtime_config
|
||||
.snapshot()
|
||||
.services
|
||||
.gateway
|
||||
.port_forwards
|
||||
@@ -694,6 +746,71 @@ hostname = "core-owned-config"
|
||||
instance.stop().await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "web-client")]
|
||||
#[tokio::test]
|
||||
async fn ignored_gateway_patch_remains_in_toml_config() {
|
||||
use easytier_proto::{
|
||||
api::config::{ConfigPatchAction, InstanceConfigPatch, PortForwardPatch, UrlPatch},
|
||||
common::{PortForwardConfigPb, SocketType},
|
||||
};
|
||||
|
||||
let (packet_sink, _packet_receiver) = tokio::sync::mpsc::channel(16);
|
||||
let toml_config = crate::config::toml::TomlConfig::new_from_str(
|
||||
"instance_name = \"ignored-gateway-patch\"",
|
||||
)
|
||||
.unwrap();
|
||||
let mut host_adapters = adapters(None, Arc::new(packet_sink));
|
||||
host_adapters.config.ignore_unsupported_config = true;
|
||||
host_adapters.config.gateway_enabled = false;
|
||||
host_adapters.config.endpoint_protocols = vec!["tcp".to_owned(), "udp".to_owned()];
|
||||
let instance = CoreInstance::from_toml(toml_config, host_adapters).unwrap();
|
||||
instance.start().await.unwrap();
|
||||
|
||||
crate::management::apply_config_patch(
|
||||
&instance,
|
||||
InstanceConfigPatch {
|
||||
port_forwards: vec![PortForwardPatch {
|
||||
action: ConfigPatchAction::Add as i32,
|
||||
cfg: Some(PortForwardConfigPb {
|
||||
bind_addr: Some(
|
||||
"127.0.0.1:18080"
|
||||
.parse::<std::net::SocketAddr>()
|
||||
.unwrap()
|
||||
.into(),
|
||||
),
|
||||
dst_addr: Some(
|
||||
"10.144.144.2:8080"
|
||||
.parse::<std::net::SocketAddr>()
|
||||
.unwrap()
|
||||
.into(),
|
||||
),
|
||||
socket_type: SocketType::Tcp as i32,
|
||||
}),
|
||||
}],
|
||||
connectors: vec![UrlPatch {
|
||||
action: ConfigPatchAction::Add as i32,
|
||||
url: Some("quic://127.0.0.1:11010".parse::<url::Url>().unwrap().into()),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(instance.toml_config().unwrap().get_port_forwards().len(), 1);
|
||||
assert!(
|
||||
instance
|
||||
.runtime_config
|
||||
.snapshot()
|
||||
.services
|
||||
.gateway
|
||||
.port_forwards
|
||||
.is_empty()
|
||||
);
|
||||
assert!(instance.list_connectors().is_empty());
|
||||
instance.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dropping_core_instance_requests_host_shutdown() {
|
||||
struct DropAwareRuntimeHost(Arc<AtomicBool>);
|
||||
@@ -992,19 +1109,24 @@ hostname = "core-owned-config"
|
||||
assert!(instances.instances().is_empty());
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
#[tokio::test]
|
||||
async fn process_management_rpc_owns_instance_create_list_and_delete() {
|
||||
use crate::{
|
||||
config::toml::TomlConfig,
|
||||
instance::manager::InstanceFactory,
|
||||
management::{InstanceManager, ProcessManagementRpc, UnsupportedConfigFileStorage},
|
||||
management::{
|
||||
InstanceManager, ProcessManagementRpc, UnsupportedConfigFileStorage,
|
||||
register_web_client_rpc,
|
||||
},
|
||||
rpc::service_registry::ServiceRegistry,
|
||||
};
|
||||
use easytier_proto::{
|
||||
api::manage::{
|
||||
DeleteNetworkInstanceRequest, ListNetworkInstanceRequest, NetworkConfig,
|
||||
NetworkingMethod, RunNetworkInstanceRequest, WebClientService,
|
||||
},
|
||||
common::RpcDescriptor,
|
||||
rpc_types::controller::BaseController,
|
||||
};
|
||||
|
||||
@@ -1038,6 +1160,31 @@ hostname = "core-owned-config"
|
||||
ManagementTestFactory(CoreProcessRuntime::new()),
|
||||
Some(tokio::runtime::Handle::current()),
|
||||
));
|
||||
let registry = ServiceRegistry::new();
|
||||
register_web_client_rpc(
|
||||
instances.clone(),
|
||||
®istry,
|
||||
Arc::new(()),
|
||||
Arc::new(UnsupportedConfigFileStorage),
|
||||
);
|
||||
assert_eq!(
|
||||
registry.get_method_name(&RpcDescriptor {
|
||||
domain_name: String::new(),
|
||||
service_name: "ConfigRpc".to_owned(),
|
||||
proto_name: "ConfigRpc".to_owned(),
|
||||
method_index: 2,
|
||||
}),
|
||||
Some("get_config".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
registry.get_method_name(&RpcDescriptor {
|
||||
domain_name: String::new(),
|
||||
service_name: "WebClientService".to_owned(),
|
||||
proto_name: "WebClientService".to_owned(),
|
||||
method_index: 1,
|
||||
}),
|
||||
Some("validate_config".to_owned())
|
||||
);
|
||||
let rpc = ProcessManagementRpc::<ManagementTestFactory>::new(
|
||||
instances.clone(),
|
||||
Arc::new(()),
|
||||
|
||||
@@ -31,7 +31,8 @@ where
|
||||
.toml_config()
|
||||
.ok_or_else(|| anyhow::anyhow!("shared TOML configuration is not available"))?;
|
||||
let candidate = config.detached_snapshot();
|
||||
let parsed_prefix = validate_public_ipv6_patch(instance, &config, &patch)?;
|
||||
let parsed_prefix =
|
||||
parse_ipv6_public_addr_prefix_patch(patch.ipv6_public_addr_prefix.as_deref())?;
|
||||
let patch_for_host = patch.clone();
|
||||
|
||||
// Preserve the existing ordered partial-commit contract: earlier valid
|
||||
@@ -54,8 +55,11 @@ where
|
||||
result?;
|
||||
|
||||
let result = patch_exit_nodes_config(&candidate, patch.exit_nodes);
|
||||
validate_and_commit_candidate(instance, &config, &candidate)?;
|
||||
instance.update_exit_nodes(result?).await;
|
||||
let normalized = validate_and_commit_candidate(instance, &config, &candidate)?;
|
||||
result?;
|
||||
instance
|
||||
.update_exit_nodes(normalized.peer.exit_nodes.clone())
|
||||
.await;
|
||||
|
||||
let result = patch_mapped_listeners(&candidate, patch.mapped_listeners);
|
||||
validate_and_commit_candidate(instance, &config, &candidate)?;
|
||||
@@ -91,10 +95,11 @@ where
|
||||
candidate.set_ipv6_public_addr_prefix(prefix);
|
||||
provider_config_changed = true;
|
||||
}
|
||||
validate_and_commit_candidate(instance, &config, &candidate)?;
|
||||
let normalized = validate_and_commit_candidate(instance, &config, &candidate)?;
|
||||
let runtime = runtime_config_from_normalized(&normalized);
|
||||
instance
|
||||
.instance_runtime
|
||||
.synchronize_config(&patch_for_host);
|
||||
.synchronize_config(&patch_for_host, &runtime);
|
||||
Ok(provider_config_changed)
|
||||
}
|
||||
.await;
|
||||
@@ -106,9 +111,12 @@ where
|
||||
instance
|
||||
.instance_runtime
|
||||
.publish_config_patch(patch_for_host);
|
||||
#[cfg(feature = "public-ipv6-provider")]
|
||||
if provider_config_changed && instance.state() == CoreInstanceState::Running {
|
||||
instance.reconcile_public_ipv6_provider().await;
|
||||
}
|
||||
#[cfg(not(feature = "public-ipv6-provider"))]
|
||||
let _ = provider_config_changed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -116,14 +124,16 @@ fn validate_and_commit_candidate<H>(
|
||||
instance: &CoreInstance<H>,
|
||||
shared: &TomlConfig,
|
||||
candidate: &TomlConfig,
|
||||
) -> anyhow::Result<()>
|
||||
) -> anyhow::Result<CoreInstanceConfig>
|
||||
where
|
||||
H: CoreInstanceHost,
|
||||
{
|
||||
let runtime = runtime_config_from_toml(instance, candidate)?;
|
||||
let normalized = CoreInstanceConfig::from_toml_with_host(candidate, instance.host_config())?;
|
||||
let runtime = runtime_config_from_normalized(&normalized);
|
||||
runtime.services.public_ipv6_provider.validate()?;
|
||||
instance.validate_runtime_config_capabilities(&runtime)?;
|
||||
shared.replace_from_snapshot(candidate);
|
||||
Ok(())
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn runtime_config_from_toml<H>(
|
||||
@@ -134,15 +144,14 @@ where
|
||||
H: CoreInstanceHost,
|
||||
{
|
||||
let normalized = CoreInstanceConfig::from_toml_with_host(config, instance.host_config())?;
|
||||
let current = instance.runtime_config_snapshot();
|
||||
let services = normalized.connectivity.runtime;
|
||||
let mut peer = normalized.peer.snapshot;
|
||||
peer.runtime.stun_info = current.peer.runtime.stun_info.clone();
|
||||
Ok(runtime_config_from_normalized(&normalized))
|
||||
}
|
||||
|
||||
Ok(CoreInstanceRuntimeConfig {
|
||||
services,
|
||||
peer: Arc::new(peer),
|
||||
})
|
||||
fn runtime_config_from_normalized(config: &CoreInstanceConfig) -> CoreInstanceRuntimeConfig {
|
||||
CoreInstanceRuntimeConfig {
|
||||
services: config.connectivity.runtime.clone(),
|
||||
peer: Arc::new(config.peer.snapshot.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ipv6_public_addr_prefix_patch(
|
||||
@@ -160,34 +169,6 @@ fn parse_ipv6_public_addr_prefix_patch(
|
||||
})?)))
|
||||
}
|
||||
|
||||
fn validate_public_ipv6_patch<H>(
|
||||
instance: &CoreInstance<H>,
|
||||
config: &TomlConfig,
|
||||
patch: &InstanceConfigPatch,
|
||||
) -> anyhow::Result<Option<Option<cidr::Ipv6Cidr>>>
|
||||
where
|
||||
H: CoreInstanceHost,
|
||||
{
|
||||
let parsed_prefix =
|
||||
parse_ipv6_public_addr_prefix_patch(patch.ipv6_public_addr_prefix.as_deref())?;
|
||||
let provider_enabled = patch
|
||||
.ipv6_public_addr_provider
|
||||
.unwrap_or(config.get_ipv6_public_addr_provider());
|
||||
let configured_prefix = parsed_prefix.unwrap_or_else(|| config.get_ipv6_public_addr_prefix());
|
||||
let provider_supported = instance
|
||||
.runtime_config_snapshot()
|
||||
.services
|
||||
.public_ipv6_provider
|
||||
.provider_supported;
|
||||
crate::config::peers::PublicIpv6ProviderConfig {
|
||||
provider_enabled,
|
||||
configured_prefix,
|
||||
provider_supported,
|
||||
}
|
||||
.validate()?;
|
||||
Ok(parsed_prefix)
|
||||
}
|
||||
|
||||
fn trace_patchables<T: Debug>(patches: &[Patchable<T>]) {
|
||||
for patch in patches {
|
||||
match patch.action {
|
||||
@@ -336,13 +317,25 @@ where
|
||||
H: CoreInstanceHost,
|
||||
{
|
||||
for patch in patches {
|
||||
let Some(url) = patch.url.map(Into::<url::Url>::into) else {
|
||||
tracing::warn!("ignored connector patch without URL");
|
||||
return Ok(());
|
||||
};
|
||||
match ConfigPatchAction::try_from(patch.action) {
|
||||
Ok(ConfigPatchAction::Add) => instance.add_connector(url)?,
|
||||
Ok(ConfigPatchAction::Add) => {
|
||||
let Some(url) = patch.url.map(Into::<url::Url>::into) else {
|
||||
tracing::warn!("ignored connector add without URL");
|
||||
continue;
|
||||
};
|
||||
if !instance.host_config().accepts_runtime_url(&url) {
|
||||
continue;
|
||||
}
|
||||
instance.add_connector(url)?;
|
||||
}
|
||||
Ok(ConfigPatchAction::Remove) => {
|
||||
let Some(url) = patch.url.map(Into::<url::Url>::into) else {
|
||||
tracing::warn!("ignored connector remove without URL");
|
||||
continue;
|
||||
};
|
||||
if !instance.host_config().accepts_runtime_url(&url) {
|
||||
continue;
|
||||
}
|
||||
if !instance.remove_connector(&url) {
|
||||
anyhow::bail!("connector not found: {url}");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ where
|
||||
.map(Into::into)
|
||||
.collect::<Vec<_>>();
|
||||
let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone());
|
||||
#[cfg(feature = "vpn-portal")]
|
||||
let vpn_portal_cfg = Some(instance.vpn_portal_info().await.client_config);
|
||||
#[cfg(not(feature = "vpn-portal"))]
|
||||
let vpn_portal_cfg = Some(String::new());
|
||||
let dev_name = instance
|
||||
.toml_config()
|
||||
.map(|config| config.get_flags().dev_name)
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
#[cfg(feature = "management")]
|
||||
mod compiled;
|
||||
mod config_patch;
|
||||
mod instance_info;
|
||||
#[cfg(feature = "management")]
|
||||
mod logger_rpc;
|
||||
#[cfg(feature = "management")]
|
||||
pub(super) mod packet_proxy;
|
||||
mod process_rpc;
|
||||
#[cfg(feature = "management")]
|
||||
pub mod remote_client;
|
||||
mod web_client;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(any(not(feature = "management"), test))]
|
||||
use easytier_proto::api::config::ConfigRpcServer;
|
||||
use easytier_proto::api::manage::WebClientServiceServer;
|
||||
#[cfg(feature = "management")]
|
||||
use easytier_proto::{
|
||||
api::{
|
||||
logger::{LoggerRpc, LoggerRpcServer},
|
||||
manage::WebClientServiceServer,
|
||||
},
|
||||
api::logger::{LoggerRpc, LoggerRpcServer},
|
||||
rpc_types::controller::BaseController,
|
||||
};
|
||||
|
||||
@@ -30,9 +35,11 @@ use super::{
|
||||
ConfigFileControl, ConfigFilePermission, DaemonGuard, resolve_optional_instance_by_name,
|
||||
};
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
pub use compiled::register_instance_management_rpc;
|
||||
pub use config_patch::apply_config_patch;
|
||||
pub use instance_info::network_instance_running_info;
|
||||
#[cfg(feature = "management")]
|
||||
pub use logger_rpc::{
|
||||
LoggerControl, LoggerManagementRpc, UnsupportedLoggerControl, log_level_name, parse_log_level,
|
||||
};
|
||||
@@ -44,6 +51,7 @@ pub use process_rpc::{
|
||||
pub(crate) use web_client::WebClientBackend;
|
||||
pub use web_client::{ConfigServerEndpoint, WebClient, WebClientConfig};
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
pub use super::instance_rpc::full::call_instance_json_rpc;
|
||||
|
||||
pub fn config_source_from_rpc(source: i32) -> Option<ConfigSource> {
|
||||
@@ -62,6 +70,7 @@ pub fn config_source_to_rpc(source: ConfigSource) -> i32 {
|
||||
}
|
||||
|
||||
/// Registers the complete process-level management surface once.
|
||||
#[cfg(feature = "management")]
|
||||
pub fn register_management_rpc<F, H>(
|
||||
instances: Arc<InstanceManager<F>>,
|
||||
registry: &ServiceRegistry,
|
||||
@@ -81,6 +90,27 @@ pub fn register_management_rpc<F, H>(
|
||||
);
|
||||
}
|
||||
|
||||
/// Registers the compact reverse-RPC surface required by easytier-web.
|
||||
#[cfg(any(not(feature = "management"), test))]
|
||||
pub(crate) fn register_web_client_rpc<F, H>(
|
||||
instances: Arc<InstanceManager<F>>,
|
||||
registry: &ServiceRegistry,
|
||||
hooks: Arc<dyn InstanceMutationHooks>,
|
||||
storage: Arc<dyn ConfigFileStorage>,
|
||||
) where
|
||||
F: InstanceFactory<Instance = CoreInstance<H>, CreateContext = ()>,
|
||||
F::Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static,
|
||||
H: CoreInstanceHost,
|
||||
{
|
||||
let config_rpc = super::instance_rpc::InstanceManagementRpc::<F>::new(instances.clone());
|
||||
registry.register(ConfigRpcServer::new(config_rpc), "");
|
||||
registry.register(
|
||||
WebClientServiceServer::new(ProcessManagementRpc::<F>::new(instances, hooks, storage)),
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
pub async fn call_management_json_rpc<F, H>(
|
||||
manager: &Arc<InstanceManager<F>>,
|
||||
logger: Arc<dyn LoggerControl>,
|
||||
|
||||
@@ -23,10 +23,11 @@ use crate::{
|
||||
tunnel::{Tunnel, web_security},
|
||||
};
|
||||
|
||||
use super::{
|
||||
ConfigFileStorage, DaemonGuard, InstanceManager, InstanceMutationHooks, LoggerControl,
|
||||
register_management_rpc,
|
||||
};
|
||||
#[cfg(not(feature = "management"))]
|
||||
use super::register_web_client_rpc;
|
||||
use super::{ConfigFileStorage, DaemonGuard, InstanceManager, InstanceMutationHooks};
|
||||
#[cfg(feature = "management")]
|
||||
use super::{LoggerControl, register_management_rpc};
|
||||
|
||||
const RETRY_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
|
||||
// Keep retry ownership in this loop when transport or protocol handshakes stall.
|
||||
@@ -108,6 +109,7 @@ where
|
||||
instances: Arc<InstanceManager<F>>,
|
||||
hooks: Arc<dyn InstanceMutationHooks>,
|
||||
storage: Arc<dyn ConfigFileStorage>,
|
||||
#[cfg(feature = "management")]
|
||||
logger: Arc<dyn LoggerControl>,
|
||||
}
|
||||
|
||||
@@ -119,6 +121,7 @@ where
|
||||
H: CoreInstanceHost,
|
||||
{
|
||||
fn register(&self, registry: &ServiceRegistry) {
|
||||
#[cfg(feature = "management")]
|
||||
register_management_rpc(
|
||||
self.instances.clone(),
|
||||
registry,
|
||||
@@ -126,6 +129,13 @@ where
|
||||
self.storage.clone(),
|
||||
self.logger.clone(),
|
||||
);
|
||||
#[cfg(not(feature = "management"))]
|
||||
register_web_client_rpc(
|
||||
self.instances.clone(),
|
||||
registry,
|
||||
self.hooks.clone(),
|
||||
self.storage.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn instance_ids(&self) -> anyhow::Result<Vec<uuid::Uuid>> {
|
||||
@@ -159,13 +169,14 @@ where
|
||||
instances: Arc<InstanceManager<F>>,
|
||||
hooks: Arc<dyn InstanceMutationHooks>,
|
||||
storage: Arc<dyn ConfigFileStorage>,
|
||||
logger: Arc<dyn LoggerControl>,
|
||||
#[cfg(feature = "management")] logger: Arc<dyn LoggerControl>,
|
||||
) -> Self {
|
||||
let manager_guard = instances.register_daemon();
|
||||
let backend = Arc::new(NativeWebClientBackend {
|
||||
instances,
|
||||
hooks,
|
||||
storage,
|
||||
#[cfg(feature = "management")]
|
||||
logger,
|
||||
});
|
||||
Self::start(connector, config, backend, Some(manager_guard))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use easytier_proto::{
|
||||
api::config::{
|
||||
ConfigRpc, GetConfigRequest, GetConfigResponse, PatchConfigRequest, PatchConfigResponse,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{api::network_config_from_toml, toml::ConfigLoader as _},
|
||||
management::apply_config_patch,
|
||||
};
|
||||
|
||||
use super::{ReadOnlyInstanceResolver, ResolvedInstanceManagementRpc};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<R> ConfigRpc for ResolvedInstanceManagementRpc<R>
|
||||
where
|
||||
R: ReadOnlyInstanceResolver,
|
||||
{
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn patch_config(
|
||||
&self,
|
||||
_: BaseController,
|
||||
request: PatchConfigRequest,
|
||||
) -> rpc_types::error::Result<PatchConfigResponse> {
|
||||
let instance = self.instance(request.instance.as_ref())?;
|
||||
if let Some(patch) = request.patch {
|
||||
apply_config_patch(&instance, patch).await?;
|
||||
}
|
||||
Ok(PatchConfigResponse::default())
|
||||
}
|
||||
|
||||
async fn get_config(
|
||||
&self,
|
||||
_: BaseController,
|
||||
request: GetConfigRequest,
|
||||
) -> rpc_types::error::Result<GetConfigResponse> {
|
||||
let config = self
|
||||
.instance(request.instance.as_ref())?
|
||||
.toml_config()
|
||||
.ok_or_else(|| anyhow::anyhow!("shared TOML configuration is not available"))?;
|
||||
Ok(GetConfigResponse {
|
||||
config: Some(network_config_from_toml(&config)),
|
||||
toml_config: config.dump(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,7 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use easytier_proto::{
|
||||
api::{
|
||||
config::{
|
||||
ConfigRpc, GetConfigRequest, GetConfigResponse, PatchConfigRequest, PatchConfigResponse,
|
||||
},
|
||||
config::ConfigRpc,
|
||||
instance::{
|
||||
AclManageRpc, ConnectorManageRpc, CredentialInfo, CredentialManageRpc,
|
||||
GenerateCredentialRequest, GenerateCredentialResponse, GetAclStatsRequest,
|
||||
@@ -27,7 +25,7 @@ use easytier_proto::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{api::network_config_from_toml, toml::ConfigLoader as _},
|
||||
config::toml::ConfigLoader as _,
|
||||
instance::{
|
||||
CoreInstance, CoreInstanceHost,
|
||||
manager::{InstanceFactory, InstanceManager},
|
||||
@@ -35,11 +33,8 @@ use crate::{
|
||||
peers::credential_manager::{CredentialCreateOptions, CredentialInfo as CoreCredentialInfo},
|
||||
};
|
||||
|
||||
use super::{InstanceManagementRpc, ReadOnlyInstanceResolver, ResolvedInstanceManagementRpc};
|
||||
use crate::management::{
|
||||
full::{apply_config_patch, packet_proxy},
|
||||
resolve_instance,
|
||||
};
|
||||
use super::InstanceManagementRpc;
|
||||
use crate::management::{full::packet_proxy, resolve_instance};
|
||||
|
||||
/// Dispatches the JSON form of an Instance-targeted management RPC without
|
||||
/// introducing a second, Host-owned set of service implementations.
|
||||
@@ -369,38 +364,3 @@ where
|
||||
Err(anyhow::anyhow!("not implemented for management API").into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<R> ConfigRpc for ResolvedInstanceManagementRpc<R>
|
||||
where
|
||||
R: ReadOnlyInstanceResolver,
|
||||
{
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn patch_config(
|
||||
&self,
|
||||
_: BaseController,
|
||||
request: PatchConfigRequest,
|
||||
) -> rpc_types::error::Result<PatchConfigResponse> {
|
||||
let instance = self.instance(request.instance.as_ref())?;
|
||||
if let Some(patch) = request.patch {
|
||||
apply_config_patch(&instance, patch).await?;
|
||||
}
|
||||
Ok(PatchConfigResponse::default())
|
||||
}
|
||||
|
||||
async fn get_config(
|
||||
&self,
|
||||
_: BaseController,
|
||||
request: GetConfigRequest,
|
||||
) -> rpc_types::error::Result<GetConfigResponse> {
|
||||
let config = self
|
||||
.instance(request.instance.as_ref())?
|
||||
.toml_config()
|
||||
.ok_or_else(|| anyhow::anyhow!("shared TOML configuration is not available"))?;
|
||||
Ok(GetConfigResponse {
|
||||
config: Some(network_config_from_toml(&config)),
|
||||
toml_config: config.dump(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ use crate::{
|
||||
|
||||
use super::resolve_instance;
|
||||
|
||||
#[cfg(feature = "web-client")]
|
||||
mod config;
|
||||
#[cfg(feature = "management")]
|
||||
pub(super) mod full;
|
||||
#[cfg(all(feature = "management", feature = "proxy-packet"))]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#[cfg(all(feature = "management", any(test, target_os = "wasi")))]
|
||||
mod forwarded_rpc;
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
mod full;
|
||||
mod instance_rpc;
|
||||
mod rpc_server_hook;
|
||||
@@ -28,16 +28,22 @@ pub(crate) use forwarded_rpc::{
|
||||
};
|
||||
#[cfg(all(feature = "management", target_os = "wasi"))]
|
||||
pub(crate) use full::WebClientBackend;
|
||||
#[cfg(all(feature = "web-client", test))]
|
||||
pub(crate) use full::register_web_client_rpc;
|
||||
#[cfg(feature = "management")]
|
||||
pub use full::remote_client;
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub use full::{
|
||||
ConfigFileStorage, ConfigServerEndpoint, InstanceMutationHooks, InstanceMutationResult,
|
||||
LoggerControl, LoggerManagementRpc, ProcessManagement, ProcessManagementRpc,
|
||||
UnsupportedConfigFileStorage, UnsupportedLoggerControl, WebClient, WebClientConfig,
|
||||
apply_config_patch, call_instance_json_rpc, call_management_json_rpc, config_source_from_rpc,
|
||||
config_source_to_rpc, log_level_name, network_instance_running_info, parse_log_level,
|
||||
register_instance_management_rpc, register_management_rpc,
|
||||
ProcessManagement, ProcessManagementRpc, UnsupportedConfigFileStorage, WebClient,
|
||||
WebClientConfig, apply_config_patch, config_source_from_rpc, config_source_to_rpc,
|
||||
network_instance_running_info,
|
||||
};
|
||||
#[cfg(feature = "management")]
|
||||
pub use full::{
|
||||
LoggerControl, LoggerManagementRpc, UnsupportedLoggerControl, call_instance_json_rpc,
|
||||
call_management_json_rpc, log_level_name, parse_log_level, register_instance_management_rpc,
|
||||
register_management_rpc,
|
||||
};
|
||||
pub use instance_rpc::InstanceManagementRpc;
|
||||
pub use rpc_server_hook::ManagementRpcServerHook;
|
||||
|
||||
@@ -277,12 +277,24 @@ pub fn build_rpc_packet(args: BuildRpcPacketArgs<'_>) -> Vec<ZCPacket> {
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(feature = "zstd")))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::proto::common::CompressionAlgoPb;
|
||||
|
||||
use super::{accepted_compression_algo, compress_packet};
|
||||
use super::accepted_compression_algo;
|
||||
#[cfg(not(feature = "zstd"))]
|
||||
use super::compress_packet;
|
||||
|
||||
#[test]
|
||||
fn accepted_compression_matches_build_capabilities() {
|
||||
#[cfg(feature = "zstd")]
|
||||
assert_eq!(accepted_compression_algo(), CompressionAlgoPb::Zstd);
|
||||
|
||||
#[cfg(not(feature = "zstd"))]
|
||||
assert_eq!(accepted_compression_algo(), CompressionAlgoPb::None);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "zstd"))]
|
||||
#[tokio::test]
|
||||
async fn compression_negotiation_falls_back_when_zstd_is_unavailable() {
|
||||
assert_eq!(accepted_compression_algo(), CompressionAlgoPb::None);
|
||||
|
||||
@@ -385,7 +385,10 @@ mod tests {
|
||||
use axum::{Json, Router, extract::State, routing::post};
|
||||
use easytier::{
|
||||
common::{MachineIdOptions, config::NetworkConfigExt},
|
||||
instance::factory::{NativeInstanceManager, native_instance_manager},
|
||||
instance::factory::{
|
||||
NativeInstanceManager, native_compact_instance_manager_with_runtime,
|
||||
native_instance_manager,
|
||||
},
|
||||
proto::{
|
||||
api::manage::{NetworkConfig, NetworkingMethod, PortForwardConfig},
|
||||
common::CompressionAlgoPb,
|
||||
@@ -901,6 +904,72 @@ mod tests {
|
||||
println!("{:?}", mgr);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compact_runtime_preserves_unsupported_web_config_during_hot_patch() {
|
||||
let (webhook_config, webhook_server, _) = test_webhook_config().await;
|
||||
let mut mgr = ClientManager::new(
|
||||
Db::memory_db().await,
|
||||
None,
|
||||
Duration::ZERO,
|
||||
Arc::new(FeatureFlags::default()),
|
||||
webhook_config,
|
||||
);
|
||||
let config_server_addr = add_random_udp_listener(&mut mgr).await;
|
||||
|
||||
let machine_id = uuid::Uuid::new_v4();
|
||||
let instance_id = uuid::Uuid::new_v4();
|
||||
let core_manager = Arc::new(native_compact_instance_manager_with_runtime(
|
||||
tokio::runtime::Handle::current(),
|
||||
));
|
||||
let _client =
|
||||
start_web_client_for_test(config_server_addr, machine_id, core_manager.clone()).await;
|
||||
let user_id = wait_for_validated_user(&mgr, machine_id).await;
|
||||
|
||||
let desired = updated_managed_network_config(instance_id);
|
||||
let mut initial = desired.clone();
|
||||
initial["port_forwards"] = json!([]);
|
||||
mgr.reconcile_managed_network_configs(
|
||||
user_id,
|
||||
machine_id,
|
||||
vec![managed_config(instance_id, initial)],
|
||||
Some("compact-initial".to_string()),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
wait_for_runtime_config(&core_manager, instance_id, |config| {
|
||||
config.network_name.as_deref() == Some("managed-updated")
|
||||
&& config.port_forwards.is_empty()
|
||||
})
|
||||
.await;
|
||||
|
||||
mgr.reconcile_managed_network_configs(
|
||||
user_id,
|
||||
machine_id,
|
||||
vec![managed_config(instance_id, desired)],
|
||||
Some("compact-patched".to_string()),
|
||||
Some("compact-initial".to_string()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let patched = wait_for_runtime_config(&core_manager, instance_id, |config| {
|
||||
config.network_name.as_deref() == Some("managed-updated")
|
||||
&& config.port_forwards.len() == 1
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_updated_runtime_config(&patched, instance_id);
|
||||
assert_eq!(
|
||||
mgr.db()
|
||||
.get_managed_config_revision((user_id, machine_id))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("compact-patched")
|
||||
);
|
||||
webhook_server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_web_config_revision_updates_running_core_config() {
|
||||
let (webhook_config, webhook_server, _) = test_webhook_config().await;
|
||||
|
||||
+2
-1
@@ -413,14 +413,15 @@ extended-services = [
|
||||
"proxy-cidr-monitor",
|
||||
]
|
||||
management = [
|
||||
"web-client",
|
||||
"logging",
|
||||
"management-rpc",
|
||||
"easytier-core/management",
|
||||
"easytier-proto/json-rpc",
|
||||
"easytier-proto/utils",
|
||||
"tokio/full",
|
||||
]
|
||||
management-rpc = ["easytier-core/management-rpc"]
|
||||
web-client = ["management-rpc", "easytier-core/web-client"]
|
||||
tcp-hole-punch = ["easytier-core/tcp-hole-punch"]
|
||||
# Deprecated: hotpath profiling has been removed. These feature aliases are
|
||||
# retained as no-ops so existing build scripts using `--features hotpath*`
|
||||
|
||||
@@ -6,11 +6,14 @@ use std::{
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use async_trait::async_trait;
|
||||
use easytier_core::config::PeerId;
|
||||
use easytier_core::connectivity::composite::ConnectorRuntime as _;
|
||||
use easytier_core::peers::public_ipv6::PublicIpv6Host;
|
||||
use easytier_core::socket::{NetNamespace, SocketContext};
|
||||
use easytier_core::tunnel::effective_encryption_uses_xor;
|
||||
use easytier_core::{
|
||||
config::{PeerId, peers::PeerRuntimeSnapshot, runtime::CoreInstanceRuntimeConfig},
|
||||
instance::{CoreInstanceConfig, CoreInstanceHostConfig},
|
||||
};
|
||||
|
||||
use super::{
|
||||
config::{ConfigLoader, Flags, NetworkIdentity},
|
||||
@@ -85,11 +88,13 @@ pub struct GlobalCtx {
|
||||
|
||||
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
||||
cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>,
|
||||
vpn_portal_cidr: AtomicCell<Option<cidr::Ipv4Cidr>>,
|
||||
hostname: Mutex<String>,
|
||||
|
||||
tun_device_name: Mutex<Option<String>>,
|
||||
|
||||
flags: ArcSwap<Flags>,
|
||||
runtime_endpoint_protocols: Option<HashSet<String>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GlobalCtx {
|
||||
@@ -113,7 +118,7 @@ impl PublicIpv6Host for GlobalCtx {
|
||||
prefix: cidr::Ipv6Cidr,
|
||||
) -> HashSet<Ipv6Addr> {
|
||||
let context = SocketContext::default()
|
||||
.with_socket_mark(self.config.get_flags().socket_mark)
|
||||
.with_socket_mark(self.get_flags().socket_mark)
|
||||
.with_netns(self.net_ns.name().map(NetNamespace::new));
|
||||
let ip_list = crate::host_runtime::native_host_runtime()
|
||||
.collect_ip_addrs(&context)
|
||||
@@ -139,14 +144,57 @@ impl PublicIpv6Host for GlobalCtx {
|
||||
|
||||
impl GlobalCtx {
|
||||
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
|
||||
Self::new_inner(config_fs, None, None)
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_runtime_config(
|
||||
config_fs: impl ConfigLoader + 'static,
|
||||
runtime: &CoreInstanceConfig,
|
||||
host: &CoreInstanceHostConfig,
|
||||
) -> Self {
|
||||
let runtime = CoreInstanceRuntimeConfig {
|
||||
services: runtime.connectivity.runtime.clone(),
|
||||
peer: Arc::new(runtime.peer.snapshot.clone()),
|
||||
};
|
||||
let protocols = host.ignore_unsupported_config.then(|| {
|
||||
host.endpoint_protocols
|
||||
.iter()
|
||||
.map(|protocol| protocol.to_ascii_lowercase())
|
||||
.collect()
|
||||
});
|
||||
Self::new_inner(config_fs, Some(&runtime), protocols)
|
||||
}
|
||||
|
||||
fn new_inner(
|
||||
config_fs: impl ConfigLoader + 'static,
|
||||
runtime: Option<&CoreInstanceRuntimeConfig>,
|
||||
runtime_endpoint_protocols: Option<HashSet<String>>,
|
||||
) -> Self {
|
||||
let id = config_fs.get_id();
|
||||
let network = config_fs.get_network_identity();
|
||||
let net_ns = NetNS::new(config_fs.get_netns());
|
||||
let hostname = match config_fs.get_hostname() {
|
||||
hostname if !hostname.is_empty() => hostname,
|
||||
_ => gethostname::gethostname().to_string_lossy().to_string(),
|
||||
};
|
||||
let flags = config_fs.get_flags();
|
||||
let hostname = runtime
|
||||
.and_then(|runtime| runtime.peer.runtime.core.node.hostname.clone())
|
||||
.unwrap_or_else(|| match config_fs.get_hostname() {
|
||||
hostname if !hostname.is_empty() => hostname,
|
||||
_ => gethostname::gethostname().to_string_lossy().to_string(),
|
||||
});
|
||||
let flags = runtime
|
||||
.map(|runtime| runtime.peer.flags.clone())
|
||||
.unwrap_or_else(|| config_fs.get_flags());
|
||||
let ipv4 = runtime
|
||||
.map(|runtime| Self::runtime_ipv4(&runtime.peer))
|
||||
.unwrap_or_else(|| config_fs.get_ipv4());
|
||||
let ipv6 = runtime
|
||||
.map(|runtime| Self::runtime_ipv6(&runtime.peer))
|
||||
.unwrap_or_else(|| config_fs.get_ipv6());
|
||||
let vpn_portal_cidr = runtime
|
||||
.map(|runtime| runtime.peer.vpn_portal_cidr)
|
||||
.unwrap_or_else(|| {
|
||||
config_fs
|
||||
.get_vpn_portal_config()
|
||||
.map(|config| config.client_cidr)
|
||||
});
|
||||
if flags.enable_encryption && effective_encryption_uses_xor(&flags.encryption_algorithm) {
|
||||
tracing::warn!("using insecure XOR because no AEAD encryption is configured");
|
||||
}
|
||||
@@ -160,16 +208,34 @@ impl GlobalCtx {
|
||||
network,
|
||||
|
||||
event_bus,
|
||||
cached_ipv4: AtomicCell::new(None),
|
||||
cached_ipv6: AtomicCell::new(None),
|
||||
cached_ipv4: AtomicCell::new(ipv4),
|
||||
cached_ipv6: AtomicCell::new(ipv6),
|
||||
vpn_portal_cidr: AtomicCell::new(vpn_portal_cidr),
|
||||
hostname: Mutex::new(hostname),
|
||||
|
||||
tun_device_name: Mutex::new(None),
|
||||
|
||||
flags: ArcSwap::new(Arc::new(flags)),
|
||||
runtime_endpoint_protocols,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_ipv4(peer: &PeerRuntimeSnapshot) -> Option<cidr::Ipv4Inet> {
|
||||
let prefix = peer.runtime.core.routes.ipv4.as_ref()?;
|
||||
let IpAddr::V4(address) = prefix.address else {
|
||||
return None;
|
||||
};
|
||||
cidr::Ipv4Inet::new(address, prefix.prefix_len).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_ipv6(peer: &PeerRuntimeSnapshot) -> Option<cidr::Ipv6Inet> {
|
||||
let prefix = peer.runtime.core.routes.ipv6.as_ref()?;
|
||||
let IpAddr::V6(address) = prefix.address else {
|
||||
return None;
|
||||
};
|
||||
cidr::Ipv6Inet::new(address, prefix.prefix_len).ok()
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> EventBusSubscriber {
|
||||
self.event_bus.subscribe()
|
||||
}
|
||||
@@ -207,31 +273,19 @@ impl GlobalCtx {
|
||||
}
|
||||
|
||||
pub fn get_ipv4(&self) -> Option<cidr::Ipv4Inet> {
|
||||
if let Some(ret) = self.cached_ipv4.load() {
|
||||
return Some(ret);
|
||||
}
|
||||
let addr = self.config.get_ipv4();
|
||||
self.cached_ipv4.store(addr);
|
||||
addr
|
||||
self.cached_ipv4.load()
|
||||
}
|
||||
|
||||
pub fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>) {
|
||||
self.config.set_ipv4(addr);
|
||||
self.cached_ipv4.store(None);
|
||||
self.cached_ipv4.store(addr);
|
||||
}
|
||||
|
||||
pub fn get_ipv6(&self) -> Option<cidr::Ipv6Inet> {
|
||||
if let Some(ret) = self.cached_ipv6.load() {
|
||||
return Some(ret);
|
||||
}
|
||||
let addr = self.config.get_ipv6();
|
||||
self.cached_ipv6.store(addr);
|
||||
addr
|
||||
self.cached_ipv6.load()
|
||||
}
|
||||
|
||||
pub fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>) {
|
||||
self.config.set_ipv6(addr);
|
||||
self.cached_ipv6.store(None);
|
||||
self.cached_ipv6.store(addr);
|
||||
}
|
||||
|
||||
pub fn is_ip_local_ipv6(&self, ip: &std::net::Ipv6Addr) -> bool {
|
||||
@@ -273,7 +327,7 @@ impl GlobalCtx {
|
||||
}
|
||||
|
||||
pub fn get_vpn_portal_cidr(&self) -> Option<cidr::Ipv4Cidr> {
|
||||
self.config.get_vpn_portal_config().map(|x| x.client_cidr)
|
||||
self.vpn_portal_cidr.load()
|
||||
}
|
||||
|
||||
pub fn get_flags(&self) -> Flags {
|
||||
@@ -281,7 +335,6 @@ impl GlobalCtx {
|
||||
}
|
||||
|
||||
pub fn set_flags(&self, flags: Flags) {
|
||||
self.config.set_flags(flags.clone());
|
||||
self.flags.store(Arc::new(flags));
|
||||
}
|
||||
|
||||
@@ -300,6 +353,17 @@ impl GlobalCtx {
|
||||
pub fn no_tun(&self) -> bool {
|
||||
self.flags.load().no_tun
|
||||
}
|
||||
|
||||
pub fn runtime_mapped_listeners(&self) -> Vec<url::Url> {
|
||||
let listeners = self.config.get_mapped_listeners();
|
||||
let Some(protocols) = &self.runtime_endpoint_protocols else {
|
||||
return listeners;
|
||||
};
|
||||
listeners
|
||||
.into_iter()
|
||||
.filter(|listener| protocols.contains(&listener.scheme().to_ascii_lowercase()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -377,6 +441,60 @@ pub mod tests {
|
||||
assert!(!config.dump().contains("hostname"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_dhcp_ipv4_survives_declarative_config_replacement() {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_dhcp(true);
|
||||
let global_ctx = GlobalCtx::new(config.clone());
|
||||
let lease = "10.144.144.7/24".parse().unwrap();
|
||||
|
||||
global_ctx.set_ipv4(Some(lease));
|
||||
config.set_ipv4(None);
|
||||
|
||||
assert_eq!(global_ctx.get_ipv4(), Some(lease));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_state_does_not_rewrite_toml_config() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let global_ctx = GlobalCtx::new(config.clone());
|
||||
let mut runtime_flags = global_ctx.get_flags();
|
||||
runtime_flags.enable_exit_node = true;
|
||||
|
||||
global_ctx.set_ipv4(Some("10.144.144.7/24".parse().unwrap()));
|
||||
global_ctx.set_ipv6(Some("fd00::7/64".parse().unwrap()));
|
||||
global_ctx.set_flags(runtime_flags);
|
||||
|
||||
assert_eq!(config.get_ipv4(), None);
|
||||
assert_eq!(config.get_ipv6(), None);
|
||||
assert!(!config.get_flags().enable_exit_node);
|
||||
assert_eq!(
|
||||
global_ctx.get_ipv4(),
|
||||
Some("10.144.144.7/24".parse().unwrap())
|
||||
);
|
||||
assert_eq!(global_ctx.get_ipv6(), Some("fd00::7/64".parse().unwrap()));
|
||||
assert!(global_ctx.get_flags().enable_exit_node);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_runtime_does_not_advertise_unsupported_mapped_listeners() {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_mapped_listeners(Some(vec![
|
||||
"tcp://127.0.0.1:11010".parse().unwrap(),
|
||||
"quic://127.0.0.1:11011".parse().unwrap(),
|
||||
]));
|
||||
let host = crate::instance::config::compact_runtime_core_host_config();
|
||||
let normalized =
|
||||
easytier_core::instance::CoreInstanceConfig::from_toml_with_host(&config, &host)
|
||||
.unwrap();
|
||||
|
||||
let global_ctx = GlobalCtx::new_with_runtime_config(config.clone(), &normalized, &host);
|
||||
|
||||
assert_eq!(config.get_mapped_listeners().len(), 2);
|
||||
assert_eq!(global_ctx.runtime_mapped_listeners().len(), 1);
|
||||
assert_eq!(global_ctx.runtime_mapped_listeners()[0].scheme(), "tcp");
|
||||
}
|
||||
|
||||
pub fn get_mock_global_ctx_with_network(
|
||||
network_identy: Option<NetworkIdentity>,
|
||||
) -> ArcGlobalCtx {
|
||||
|
||||
@@ -6,15 +6,14 @@ use easytier_core::gateway::proxy::wrapped_transport::WrappedTransportEngines;
|
||||
use easytier_core::gateway::vpn_portal::VpnPortalHost;
|
||||
#[cfg(test)]
|
||||
use easytier_core::host::packet::{HostPacket, PacketSink};
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
use easytier_core::{
|
||||
connectivity::manual::ManualTunnelConnector,
|
||||
host::dns::{DnsRecordResolver, DnsResolver},
|
||||
instance::CoreInstanceConfig,
|
||||
};
|
||||
use easytier_core::{
|
||||
events::{CoreEvent, CoreEventSink},
|
||||
instance::{CoreHostAdapters, CoreInstance, PacketEgressHost},
|
||||
instance::{CoreHostAdapters, CoreInstance, CoreInstanceConfig, PacketEgressHost},
|
||||
process_runtime::CoreProcessRuntime,
|
||||
};
|
||||
|
||||
@@ -25,7 +24,9 @@ use crate::{
|
||||
common::global_ctx::ArcGlobalCtx,
|
||||
common::{config::TomlConfig, global_ctx::GlobalCtx},
|
||||
host_runtime::native_host_runtime,
|
||||
instance::config::{runtime_core_host_config, runtime_peer_credential_storage},
|
||||
instance::config::{
|
||||
compact_runtime_core_host_config, runtime_core_host_config, runtime_peer_credential_storage,
|
||||
},
|
||||
instance::listeners::RuntimeExternalListenerFactory,
|
||||
instance::runtime_host::NativeInstanceRuntimeHost,
|
||||
};
|
||||
@@ -42,18 +43,30 @@ use easytier_core::gateway::proxy::wrapped_transport::WrappedTransportEngine;
|
||||
pub(crate) type NativeCoreInstance = CoreInstance<NativeInstanceHost>;
|
||||
|
||||
pub(crate) fn compose_native_core_instance(
|
||||
config: TomlConfig,
|
||||
toml_config: TomlConfig,
|
||||
process_runtime: Arc<CoreProcessRuntime>,
|
||||
compact_runtime: bool,
|
||||
) -> anyhow::Result<Arc<NativeCoreInstance>> {
|
||||
let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
|
||||
let host_config = if compact_runtime {
|
||||
compact_runtime_core_host_config()
|
||||
} else {
|
||||
runtime_core_host_config()
|
||||
};
|
||||
let normalized = CoreInstanceConfig::from_toml_with_host(&toml_config, &host_config)?;
|
||||
let global_ctx = Arc::new(GlobalCtx::new_with_runtime_config(
|
||||
toml_config.clone(),
|
||||
&normalized,
|
||||
&host_config,
|
||||
));
|
||||
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone());
|
||||
let mut adapters = runtime_core_host_adapters_with_packet_egress(
|
||||
let mut adapters = runtime_core_host_adapters_with_packet_egress_and_config(
|
||||
global_ctx.clone(),
|
||||
process_runtime,
|
||||
runtime_host.clone(),
|
||||
host_config,
|
||||
);
|
||||
adapters.instance_runtime = runtime_host;
|
||||
NativeCoreInstance::from_toml(config, adapters)
|
||||
NativeCoreInstance::from_toml(toml_config, adapters)
|
||||
}
|
||||
|
||||
impl CoreEventSink for GlobalCtx {
|
||||
@@ -139,13 +152,19 @@ impl CoreEventSink for GlobalCtx {
|
||||
}
|
||||
|
||||
#[cfg(feature = "wrapped-transport")]
|
||||
fn runtime_wrapped_transport_engines() -> WrappedTransportEngines {
|
||||
fn runtime_wrapped_transport_engines(
|
||||
config: &easytier_core::instance::CoreInstanceHostConfig,
|
||||
) -> WrappedTransportEngines {
|
||||
#[cfg(feature = "kcp")]
|
||||
let kcp = Some(Arc::new(KcpProxyService::new()) as Arc<dyn WrappedTransportEngine>);
|
||||
let kcp = config
|
||||
.kcp_enabled
|
||||
.then(|| Arc::new(KcpProxyService::new()) as Arc<dyn WrappedTransportEngine>);
|
||||
#[cfg(not(feature = "kcp"))]
|
||||
let kcp = None;
|
||||
#[cfg(feature = "quic")]
|
||||
let quic = Some(Arc::new(QuicProxyService::new()) as Arc<dyn WrappedTransportEngine>);
|
||||
let quic = config
|
||||
.quic_enabled
|
||||
.then(|| Arc::new(QuicProxyService::new()) as Arc<dyn WrappedTransportEngine>);
|
||||
#[cfg(not(feature = "quic"))]
|
||||
let quic = None;
|
||||
|
||||
@@ -161,9 +180,10 @@ pub(crate) fn runtime_core_host_adapters(
|
||||
let host = native_instance_host(global_ctx.clone());
|
||||
let runtime_dns = native_host_runtime();
|
||||
let adapters = CoreHostAdapters::new(host, runtime_dns, packet_sink, process_runtime);
|
||||
configure_runtime_core_host_adapters(global_ctx, adapters)
|
||||
configure_runtime_core_host_adapters(global_ctx, adapters, runtime_core_host_config())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn runtime_core_host_adapters_with_packet_egress(
|
||||
global_ctx: ArcGlobalCtx,
|
||||
process_runtime: Arc<CoreProcessRuntime>,
|
||||
@@ -173,29 +193,45 @@ pub(crate) fn runtime_core_host_adapters_with_packet_egress(
|
||||
let runtime_dns = native_host_runtime();
|
||||
let adapters =
|
||||
CoreHostAdapters::new_with_packet_egress(host, runtime_dns, packet_egress, process_runtime);
|
||||
configure_runtime_core_host_adapters(global_ctx, adapters)
|
||||
configure_runtime_core_host_adapters(global_ctx, adapters, runtime_core_host_config())
|
||||
}
|
||||
|
||||
fn runtime_core_host_adapters_with_packet_egress_and_config(
|
||||
global_ctx: ArcGlobalCtx,
|
||||
process_runtime: Arc<CoreProcessRuntime>,
|
||||
packet_egress: Arc<dyn PacketEgressHost>,
|
||||
host_config: easytier_core::instance::CoreInstanceHostConfig,
|
||||
) -> CoreHostAdapters<NativeInstanceHost> {
|
||||
let host = native_instance_host(global_ctx.clone());
|
||||
let runtime_dns = native_host_runtime();
|
||||
let adapters =
|
||||
CoreHostAdapters::new_with_packet_egress(host, runtime_dns, packet_egress, process_runtime);
|
||||
configure_runtime_core_host_adapters(global_ctx, adapters, host_config)
|
||||
}
|
||||
|
||||
fn configure_runtime_core_host_adapters(
|
||||
global_ctx: ArcGlobalCtx,
|
||||
mut adapters: CoreHostAdapters<NativeInstanceHost>,
|
||||
host_config: easytier_core::instance::CoreInstanceHostConfig,
|
||||
) -> CoreHostAdapters<NativeInstanceHost> {
|
||||
#[cfg(test)]
|
||||
adapters.replace_stun_provider(Arc::new(crate::common::stun::MockStunInfoCollector {
|
||||
udp_nat_type: crate::proto::common::NatType::Unknown,
|
||||
}));
|
||||
adapters.config = runtime_core_host_config();
|
||||
adapters.credential_storage = runtime_peer_credential_storage(&global_ctx);
|
||||
adapters.config = host_config.clone();
|
||||
adapters.credential_storage = (!host_config.ignore_unsupported_config)
|
||||
.then(|| runtime_peer_credential_storage(&global_ctx))
|
||||
.flatten();
|
||||
adapters.events = global_ctx.clone();
|
||||
#[cfg(feature = "wrapped-transport")]
|
||||
{
|
||||
adapters.wrapped_transports = runtime_wrapped_transport_engines();
|
||||
adapters.wrapped_transports = runtime_wrapped_transport_engines(&host_config);
|
||||
}
|
||||
adapters.protocol = Some(runtime_client_protocol_upgrader(global_ctx.clone()));
|
||||
adapters.external_listener_factory = Some(Arc::new(RuntimeExternalListenerFactory));
|
||||
adapters.server_protocol = Some(runtime_server_protocol_upgrader(global_ctx.clone()));
|
||||
#[cfg(feature = "upnp")]
|
||||
{
|
||||
if host_config.upnp_enabled {
|
||||
adapters.udp_hole_punch_platform = Some(
|
||||
crate::instance::udp_hole_punch::runtime_udp_hole_punch_platform(
|
||||
global_ctx.net_ns.clone(),
|
||||
@@ -211,12 +247,12 @@ fn configure_runtime_core_host_adapters(
|
||||
adapters.proxy_cidr_monitor_enabled = true;
|
||||
}
|
||||
#[cfg(feature = "public-ipv6-provider")]
|
||||
{
|
||||
if host_config.public_ipv6_provider_supported {
|
||||
adapters.public_ipv6_host = Some(global_ctx.clone());
|
||||
adapters.public_ipv6_provider = Some(runtime_public_ipv6_provider_platform(&global_ctx));
|
||||
}
|
||||
#[cfg(feature = "wireguard")]
|
||||
{
|
||||
if host_config.vpn_portal_enabled {
|
||||
use crate::common::config::ConfigLoader as _;
|
||||
|
||||
adapters.vpn_portal = Some(crate::vpn_portal::wireguard::WireGuardPortalHost::new(
|
||||
@@ -230,7 +266,7 @@ fn configure_runtime_core_host_adapters(
|
||||
adapters
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub(crate) fn runtime_one_shot_manual_connector(
|
||||
global_ctx: ArcGlobalCtx,
|
||||
config: &TomlConfig,
|
||||
|
||||
@@ -44,13 +44,44 @@ pub(crate) fn runtime_core_host_config() -> CoreInstanceHostConfig {
|
||||
all(target_os = "macos", feature = "macos-ne"),
|
||||
target_env = "ohos"
|
||||
))),
|
||||
public_ipv6_provider_supported: cfg!(target_os = "linux"),
|
||||
public_ipv6_provider_supported: cfg!(all(
|
||||
target_os = "linux",
|
||||
feature = "public-ipv6-provider"
|
||||
)),
|
||||
gateway_enabled: cfg!(feature = "socks5"),
|
||||
proxy_enabled: cfg!(any(feature = "kcp", feature = "quic")),
|
||||
vpn_portal_enabled: cfg!(feature = "wireguard"),
|
||||
magic_dns_enabled: cfg!(feature = "magic-dns"),
|
||||
kcp_enabled: cfg!(feature = "kcp"),
|
||||
quic_enabled: cfg!(feature = "quic"),
|
||||
udp_broadcast_enabled: cfg!(all(target_os = "windows", feature = "tun")),
|
||||
upnp_enabled: cfg!(feature = "upnp"),
|
||||
tcp_hole_punching_enabled: cfg!(feature = "tcp-hole-punch"),
|
||||
ignore_unsupported_config: false,
|
||||
easytier_version: EASYTIER_VERSION.to_owned(),
|
||||
endpoint_protocols: IpScheme::VARIANTS.iter().map(ToString::to_string).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compact_runtime_core_host_config() -> CoreInstanceHostConfig {
|
||||
let mut config = runtime_core_host_config();
|
||||
config.force_exit_node = false;
|
||||
config.host_routing.local_exit_node_fallback = false;
|
||||
config.public_ipv6_provider_supported = false;
|
||||
config.gateway_enabled = false;
|
||||
config.proxy_enabled = false;
|
||||
config.vpn_portal_enabled = false;
|
||||
config.magic_dns_enabled = false;
|
||||
config.kcp_enabled = false;
|
||||
config.quic_enabled = false;
|
||||
config.udp_broadcast_enabled = false;
|
||||
config.upnp_enabled = false;
|
||||
config.tcp_hole_punching_enabled = false;
|
||||
config.ignore_unsupported_config = true;
|
||||
config.endpoint_protocols = vec!["tcp".to_owned(), "udp".to_owned()];
|
||||
config
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_peer_credential_storage(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
) -> Option<Arc<dyn CredentialStorage>> {
|
||||
@@ -71,6 +102,9 @@ pub(crate) fn test_core_instance_config(
|
||||
|
||||
let config = TomlConfig::new_from_str(&global_ctx.config.dump())
|
||||
.expect("test configuration should round-trip through TOML");
|
||||
config.set_ipv4(global_ctx.get_ipv4());
|
||||
config.set_ipv6(global_ctx.get_ipv6());
|
||||
config.set_flags(global_ctx.get_flags());
|
||||
let mut host = runtime_core_host_config();
|
||||
let hostname = global_ctx.get_hostname();
|
||||
host.hostname_fallback = (!hostname.is_empty()).then_some(hostname);
|
||||
@@ -113,7 +147,7 @@ mod tests {
|
||||
assert_eq!(config.smoltcp_available, cfg!(feature = "smoltcp"));
|
||||
assert_eq!(
|
||||
config.public_ipv6_provider_supported,
|
||||
cfg!(target_os = "linux")
|
||||
cfg!(all(target_os = "linux", feature = "public-ipv6-provider"))
|
||||
);
|
||||
assert_eq!(config.easytier_version, EASYTIER_VERSION);
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ impl MagicDnsServerInstance {
|
||||
rpc_server.set_hook(data.clone());
|
||||
|
||||
// Use configured tld_dns_zone or fall back to DEFAULT_ET_DNS_ZONE if empty
|
||||
let flags = global_ctx.config.get_flags();
|
||||
let flags = global_ctx.get_flags();
|
||||
let tld_dns_zone_clone = flags.tld_dns_zone.clone();
|
||||
|
||||
data.update_dns_records(std::iter::empty(), &tld_dns_zone_clone)
|
||||
|
||||
@@ -58,6 +58,19 @@ pub fn native_instance_manager_with_runtime(
|
||||
native_instance_manager_with_optional_runtime(Some(runtime_handle))
|
||||
}
|
||||
|
||||
#[cfg(feature = "management-rpc")]
|
||||
pub fn native_compact_instance_manager_with_runtime(
|
||||
runtime_handle: tokio::runtime::Handle,
|
||||
) -> NativeInstanceManager {
|
||||
let process_runtime = CoreProcessRuntime::new();
|
||||
let factory = NativeInstanceFactory::new(process_runtime)
|
||||
.with_runtime_handle(Some(runtime_handle.clone()))
|
||||
.with_compact_runtime();
|
||||
#[cfg(feature = "logging")]
|
||||
let factory = factory.with_cli_event_logging();
|
||||
InstanceManager::new(factory, Some(runtime_handle))
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
pub fn native_process_management(
|
||||
instances: Arc<NativeInstanceManager>,
|
||||
@@ -85,7 +98,8 @@ fn native_instance_manager_with_optional_runtime(
|
||||
pub struct NativeInstanceFactory {
|
||||
process_runtime: Arc<CoreProcessRuntime>,
|
||||
runtime_handle: Option<tokio::runtime::Handle>,
|
||||
#[cfg(feature = "management")]
|
||||
compact_runtime: bool,
|
||||
#[cfg(feature = "logging")]
|
||||
log_cli_events: bool,
|
||||
}
|
||||
|
||||
@@ -94,12 +108,13 @@ impl NativeInstanceFactory {
|
||||
Self {
|
||||
process_runtime,
|
||||
runtime_handle: None,
|
||||
#[cfg(feature = "management")]
|
||||
compact_runtime: false,
|
||||
#[cfg(feature = "logging")]
|
||||
log_cli_events: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "logging")]
|
||||
fn with_cli_event_logging(mut self) -> Self {
|
||||
self.log_cli_events = true;
|
||||
self
|
||||
@@ -110,6 +125,11 @@ impl NativeInstanceFactory {
|
||||
self.runtime_handle = runtime_handle;
|
||||
self
|
||||
}
|
||||
|
||||
fn with_compact_runtime(mut self) -> Self {
|
||||
self.compact_runtime = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InstanceFactory for NativeInstanceFactory {
|
||||
@@ -126,8 +146,12 @@ impl InstanceFactory for NativeInstanceFactory {
|
||||
.runtime_handle
|
||||
.as_ref()
|
||||
.map(tokio::runtime::Handle::enter);
|
||||
let instance = compose_native_core_instance(config, self.process_runtime.clone())?;
|
||||
#[cfg(feature = "management")]
|
||||
let instance = compose_native_core_instance(
|
||||
config,
|
||||
self.process_runtime.clone(),
|
||||
self.compact_runtime,
|
||||
)?;
|
||||
#[cfg(feature = "logging")]
|
||||
if self.log_cli_events {
|
||||
let events = subscribe_native_instance_event(&instance)
|
||||
.ok_or_else(|| anyhow::anyhow!("native instance runtime host is unavailable"))?;
|
||||
|
||||
@@ -25,7 +25,7 @@ pub struct NativeInstanceEnvironment {
|
||||
impl NativeInstanceEnvironment {
|
||||
fn new(global_ctx: ArcGlobalCtx, runtime: Arc<NativeHostRuntime>) -> Self {
|
||||
let socket_context = SocketContext::default()
|
||||
.with_socket_mark(global_ctx.config.get_flags().socket_mark)
|
||||
.with_socket_mark(global_ctx.get_flags().socket_mark)
|
||||
.with_netns(global_ctx.net_ns.name().map(NetNamespace::new));
|
||||
Self {
|
||||
global_ctx,
|
||||
@@ -49,7 +49,7 @@ impl ConnectorEnvironment for NativeInstanceEnvironment {
|
||||
}
|
||||
|
||||
fn mapped_listeners(&self) -> Vec<url::Url> {
|
||||
self.global_ctx.config.get_mapped_listeners()
|
||||
self.global_ctx.runtime_mapped_listeners()
|
||||
}
|
||||
|
||||
fn is_local_ip(&self, ip: &IpAddr) -> bool {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "logging")]
|
||||
pub(crate) mod cli_event_logger;
|
||||
pub(crate) mod composition;
|
||||
pub(crate) mod config;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use easytier_core::{
|
||||
gateway::dhcp::DhcpIpv4Host, host::packet::HostPacketReceiver, instance::CorePacketPlane,
|
||||
config::runtime::CoreInstanceRuntimeConfig, gateway::dhcp::DhcpIpv4Host,
|
||||
host::packet::HostPacketReceiver, instance::CorePacketPlane,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -77,6 +78,41 @@ impl NativeInstanceRuntimeHost {
|
||||
self.event_journal.events()
|
||||
}
|
||||
|
||||
#[cfg(feature = "web-client")]
|
||||
fn synchronize_global_ctx_config(
|
||||
&self,
|
||||
patch: &crate::proto::api::config::InstanceConfigPatch,
|
||||
config: &CoreInstanceRuntimeConfig,
|
||||
) {
|
||||
if patch.hostname.is_some() {
|
||||
self.global_ctx.set_hostname(
|
||||
config
|
||||
.peer
|
||||
.runtime
|
||||
.core
|
||||
.node
|
||||
.hostname
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
if patch.ipv4.is_some() && !config.services.dhcp_ipv4 {
|
||||
self.global_ctx
|
||||
.set_ipv4(crate::common::global_ctx::GlobalCtx::runtime_ipv4(
|
||||
&config.peer,
|
||||
));
|
||||
}
|
||||
if patch.ipv6.is_some() {
|
||||
self.global_ctx
|
||||
.set_ipv6(crate::common::global_ctx::GlobalCtx::runtime_ipv6(
|
||||
&config.peer,
|
||||
));
|
||||
}
|
||||
if patch.disable_relay_data.is_some() {
|
||||
self.global_ctx.set_flags(config.peer.flags.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn subscribe_event(&self) -> crate::common::global_ctx::EventBusSubscriber {
|
||||
self.global_ctx.subscribe()
|
||||
}
|
||||
@@ -98,6 +134,15 @@ mod tests {
|
||||
global_ctx::{GlobalCtx, GlobalCtxEvent},
|
||||
};
|
||||
|
||||
#[cfg(feature = "web-client")]
|
||||
fn runtime_config(config: &TomlConfig) -> CoreInstanceRuntimeConfig {
|
||||
let normalized = easytier_core::instance::CoreInstanceConfig::from_toml(config).unwrap();
|
||||
CoreInstanceRuntimeConfig {
|
||||
services: normalized.connectivity.runtime,
|
||||
peer: Arc::new(normalized.peer.snapshot),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_host_owns_event_subscription_context() {
|
||||
let global_ctx = Arc::new(GlobalCtx::new(TomlConfig::default()));
|
||||
@@ -111,4 +156,68 @@ mod tests {
|
||||
GlobalCtxEvent::CredentialChanged
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "web-client")]
|
||||
#[test]
|
||||
fn runtime_host_synchronizes_normalized_config_without_management() {
|
||||
use easytier_core::{config::toml::ConfigLoader as _, instance::InstanceRuntimeHost as _};
|
||||
|
||||
let config = TomlConfig::default();
|
||||
config.set_hostname(Some("before".to_owned()));
|
||||
config.set_ipv4(Some("10.20.0.1/24".parse().unwrap()));
|
||||
config.set_ipv6(Some("fd00::1/64".parse().unwrap()));
|
||||
let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
|
||||
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone());
|
||||
|
||||
assert_eq!(global_ctx.get_hostname(), "before");
|
||||
assert_eq!(global_ctx.get_ipv4(), Some("10.20.0.1/24".parse().unwrap()));
|
||||
assert_eq!(global_ctx.get_ipv6(), Some("fd00::1/64".parse().unwrap()));
|
||||
assert!(!global_ctx.get_flags().disable_relay_data);
|
||||
|
||||
config.set_hostname(Some("after".to_owned()));
|
||||
config.set_ipv4(Some("10.20.0.2/24".parse().unwrap()));
|
||||
config.set_ipv6(Some("fd00::2/64".parse().unwrap()));
|
||||
let mut flags = config.get_flags();
|
||||
flags.disable_relay_data = true;
|
||||
config.set_flags(flags);
|
||||
runtime_host.synchronize_config(
|
||||
&crate::proto::api::config::InstanceConfigPatch {
|
||||
hostname: Some("ignored-raw-hostname".to_owned()),
|
||||
ipv4: Some("10.99.0.1/24".parse::<cidr::Ipv4Inet>().unwrap().into()),
|
||||
ipv6: Some("fd99::1/64".parse::<cidr::Ipv6Inet>().unwrap().into()),
|
||||
disable_relay_data: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
&runtime_config(&config),
|
||||
);
|
||||
|
||||
assert_eq!(global_ctx.get_hostname(), "after");
|
||||
assert_eq!(global_ctx.get_ipv4(), Some("10.20.0.2/24".parse().unwrap()));
|
||||
assert_eq!(global_ctx.get_ipv6(), Some("fd00::2/64".parse().unwrap()));
|
||||
assert!(global_ctx.get_flags().disable_relay_data);
|
||||
}
|
||||
|
||||
#[cfg(feature = "web-client")]
|
||||
#[test]
|
||||
fn runtime_host_preserves_dhcp_ipv4_during_config_synchronization() {
|
||||
use easytier_core::{config::toml::ConfigLoader as _, instance::InstanceRuntimeHost as _};
|
||||
|
||||
let config = TomlConfig::default();
|
||||
config.set_dhcp(true);
|
||||
let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
|
||||
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone());
|
||||
let lease = "10.20.0.7/24".parse().unwrap();
|
||||
global_ctx.set_ipv4(Some(lease));
|
||||
|
||||
config.set_ipv4(None);
|
||||
runtime_host.synchronize_config(
|
||||
&crate::proto::api::config::InstanceConfigPatch {
|
||||
ipv4: Some("10.99.0.1/24".parse::<cidr::Ipv4Inet>().unwrap().into()),
|
||||
..Default::default()
|
||||
},
|
||||
&runtime_config(&config),
|
||||
);
|
||||
|
||||
assert_eq!(global_ctx.get_ipv4(), Some(lease));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,28 +85,6 @@ impl EventJournal {
|
||||
self.events.read().unwrap().iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub(super) fn synchronize_config(
|
||||
&self,
|
||||
patch: &crate::proto::api::config::InstanceConfigPatch,
|
||||
) {
|
||||
if let Some(hostname) = &patch.hostname {
|
||||
self.global_ctx.set_hostname(hostname.clone());
|
||||
}
|
||||
if let Some(ipv4) = patch.ipv4.as_ref()
|
||||
&& !self.global_ctx.config.get_dhcp()
|
||||
{
|
||||
self.global_ctx.set_ipv4(Some((*ipv4).into()));
|
||||
}
|
||||
if let Some(ipv6) = patch.ipv6.as_ref() {
|
||||
self.global_ctx.set_ipv6(Some((*ipv6).into()));
|
||||
}
|
||||
if let Some(disable_relay_data) = patch.disable_relay_data {
|
||||
let mut flags = self.global_ctx.get_flags();
|
||||
flags.disable_relay_data = disable_relay_data;
|
||||
self.global_ctx.set_flags(flags);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn publish_config_patch(
|
||||
&self,
|
||||
patch: crate::proto::api::config::InstanceConfigPatch,
|
||||
|
||||
@@ -29,14 +29,21 @@ impl InstanceRuntimeHost for NativeInstanceRuntimeHost {
|
||||
self.management_events_snapshot()
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
fn synchronize_config(&self, patch: &crate::proto::api::config::InstanceConfigPatch) {
|
||||
self.event_journal.synchronize_config(patch);
|
||||
#[cfg(feature = "web-client")]
|
||||
fn synchronize_config(
|
||||
&self,
|
||||
patch: &crate::proto::api::config::InstanceConfigPatch,
|
||||
config: &easytier_core::config::runtime::CoreInstanceRuntimeConfig,
|
||||
) {
|
||||
self.synchronize_global_ctx_config(patch, config);
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
fn publish_config_patch(&self, patch: crate::proto::api::config::InstanceConfigPatch) {
|
||||
#[cfg(feature = "management")]
|
||||
self.event_journal.publish_config_patch(patch);
|
||||
#[cfg(not(feature = "management"))]
|
||||
let _ = patch;
|
||||
}
|
||||
|
||||
fn attach_tun_fd(&self, fd: i32) -> anyhow::Result<()> {
|
||||
|
||||
@@ -5,10 +5,7 @@ use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle};
|
||||
|
||||
use crate::common::global_ctx::ArcGlobalCtx;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
use crate::{
|
||||
common::config::ConfigLoader as _,
|
||||
instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner},
|
||||
};
|
||||
use crate::instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct MagicDnsRuntime {
|
||||
@@ -30,7 +27,7 @@ impl MagicDnsRuntime {
|
||||
tun_dev: Option<String>,
|
||||
tun_ip: Ipv4Inet,
|
||||
) -> Self {
|
||||
let active = global_ctx.config.get_flags().accept_dns.then(|| {
|
||||
let active = global_ctx.get_flags().accept_dns.then(|| {
|
||||
let mut runner = DnsRunner::new(
|
||||
packet_plane,
|
||||
global_ctx,
|
||||
|
||||
@@ -17,7 +17,6 @@ use tokio_util::sync::CancellationToken;
|
||||
use super::{MagicDnsRuntime, tun_common::TunNicState};
|
||||
use crate::{
|
||||
common::{
|
||||
config::ConfigLoader as _,
|
||||
error::Error,
|
||||
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
|
||||
},
|
||||
@@ -133,7 +132,7 @@ impl NativeTunRuntime {
|
||||
|
||||
pub(super) async fn prepare(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
|
||||
self.nic.drain().await;
|
||||
if !self.global_ctx.config.get_flags().no_tun {
|
||||
if !self.global_ctx.get_flags().no_tun {
|
||||
self.start_static_ip(packet_plane).await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -663,7 +663,7 @@ impl VirtualNic {
|
||||
|
||||
let dev = AsyncDevice::new(dev)?;
|
||||
|
||||
let flags = self.global_ctx.config.get_flags();
|
||||
let flags = self.global_ctx.get_flags();
|
||||
let mut mtu_in_config = flags.mtu;
|
||||
if flags.enable_encryption {
|
||||
mtu_in_config -= 20;
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ pub mod service_manager;
|
||||
pub(crate) mod socket;
|
||||
pub mod tunnel;
|
||||
pub mod utils;
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub mod web_client;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub use easytier_proto::api;
|
||||
#[cfg(feature = "management")]
|
||||
#[cfg(feature = "web-client")]
|
||||
pub use easytier_proto::web;
|
||||
pub use easytier_proto::{
|
||||
ALL_DESCRIPTOR_BYTES, acl, common, core_config, error, peer_rpc, rpc_types,
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use easytier_core::{
|
||||
config::toml::ConfigLoader as _,
|
||||
connectivity::{manual::ManualTunnelConnector, protocol::raw::TunnelDialer},
|
||||
management::{ConfigServerEndpoint, WebClientConfig},
|
||||
socket::IpVersion,
|
||||
@@ -10,18 +11,21 @@ use easytier_core::{
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
use crate::{
|
||||
common::os_info::collect_device_os_info, instance::config_storage::NativeConfigFileStorage,
|
||||
rpc_service::logger::NativeLoggerControl,
|
||||
};
|
||||
use crate::{
|
||||
common::{
|
||||
MachineIdOptions, config::TomlConfigLoader, constants::EASYTIER_VERSION,
|
||||
global_ctx::GlobalCtx, os_info::collect_device_os_info, resolve_machine_id,
|
||||
global_ctx::GlobalCtx, resolve_machine_id,
|
||||
},
|
||||
instance::{
|
||||
composition::runtime_one_shot_manual_connector,
|
||||
config_storage::NativeConfigFileStorage,
|
||||
factory::{NativeInstanceFactory, NativeInstanceManager},
|
||||
host::NativeInstanceHost,
|
||||
},
|
||||
rpc_service::logger::NativeLoggerControl,
|
||||
tunnel::TunnelScheme,
|
||||
};
|
||||
|
||||
@@ -46,23 +50,32 @@ impl WebClient {
|
||||
S: ToString,
|
||||
H: ToString,
|
||||
{
|
||||
Self {
|
||||
inner: easytier_core::management::WebClient::new(
|
||||
connector,
|
||||
WebClientConfig {
|
||||
token: token.to_string(),
|
||||
machine_id,
|
||||
hostname: hostname.to_string(),
|
||||
device_os: collect_device_os_info(),
|
||||
easytier_version: EASYTIER_VERSION.to_owned(),
|
||||
secure_mode,
|
||||
},
|
||||
manager,
|
||||
hooks.unwrap_or_else(|| Arc::new(DefaultHooks)),
|
||||
Arc::new(NativeConfigFileStorage),
|
||||
Arc::new(NativeLoggerControl),
|
||||
),
|
||||
}
|
||||
let config = WebClientConfig {
|
||||
token: token.to_string(),
|
||||
machine_id,
|
||||
hostname: hostname.to_string(),
|
||||
device_os: web_client_device_os_info(),
|
||||
easytier_version: EASYTIER_VERSION.to_owned(),
|
||||
secure_mode,
|
||||
};
|
||||
#[cfg(feature = "management")]
|
||||
let inner = easytier_core::management::WebClient::new(
|
||||
connector,
|
||||
config,
|
||||
manager,
|
||||
hooks.unwrap_or_else(|| Arc::new(DefaultHooks)),
|
||||
Arc::new(NativeConfigFileStorage),
|
||||
Arc::new(NativeLoggerControl),
|
||||
);
|
||||
#[cfg(not(feature = "management"))]
|
||||
let inner = easytier_core::management::WebClient::new(
|
||||
connector,
|
||||
config,
|
||||
manager,
|
||||
hooks.unwrap_or_else(|| Arc::new(DefaultHooks)),
|
||||
Arc::new(easytier_core::management::UnsupportedConfigFileStorage),
|
||||
);
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
@@ -70,6 +83,20 @@ impl WebClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "management")]
|
||||
fn web_client_device_os_info() -> easytier_proto::web::DeviceOsInfo {
|
||||
collect_device_os_info()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "management"))]
|
||||
fn web_client_device_os_info() -> easytier_proto::web::DeviceOsInfo {
|
||||
easytier_proto::web::DeviceOsInfo {
|
||||
os_type: std::env::consts::OS.to_owned(),
|
||||
version: String::new(),
|
||||
distribution: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefaultHooks;
|
||||
|
||||
#[async_trait]
|
||||
@@ -113,6 +140,7 @@ pub async fn run_web_client(
|
||||
let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.bind_device = false;
|
||||
config.set_flags(flags.clone());
|
||||
global_ctx.set_flags(flags);
|
||||
let hostname =
|
||||
hostname.unwrap_or_else(|| gethostname::gethostname().to_string_lossy().to_string());
|
||||
|
||||
Reference in New Issue
Block a user