refactor(config): preserve explicit flags with FlagsPatch and derive CLI arguments from schema (#2585)

This PR refactors EasyTier's configuration flags system across protobuf definitions, TOML/API persistence, runtime reconciliation, and the CLI.

It introduces a synthesized FlagsPatch protobuf message with optionize and pbjson support to track field presence reliably, replaces manual CLI flag boilerplate with dynamic schema-driven clap argument generation, and
establishes Flags::resolve(patch) as the single source of truth for runtime defaults.

Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
Luna YaoandClaude Code authored and GitHub committed 2026-10-10 22:11:08 +08:00
1 parent 018750a785
commit 250c2ad8d6
52 files changed
+1909 -1405

No files matched your search

Generated
+42 -1
View File
@@ -2572,6 +2572,7 @@ dependencies = [
"pin-project-lite",
"pnet_datalink",
"prost 0.14.4",
"prost-types 0.14.4",
"quanta",
"quinn",
"quinn-proto",
@@ -2669,6 +2670,7 @@ dependencies = [
"hyper-util",
"idna",
"openssl",
"optionize",
"ordered_hash_map",
"parking_lot",
"percent-encoding",
@@ -2676,6 +2678,7 @@ dependencies = [
"pin-project-lite",
"prefix-trie 0.10.1",
"prost 0.14.4",
"prost-reflect 0.16.5",
"prost-types 0.14.4",
"quanta",
"rand 0.8.8",
@@ -2794,7 +2797,7 @@ dependencies = [
"flate2",
"gethostname",
"once_cell",
"prost-reflect",
"prost-reflect 0.14.7",
"rusqlite",
"serde",
"serde_json",
@@ -2842,11 +2845,13 @@ dependencies = [
"cidr",
"hmac 0.12.1",
"indoc",
"optionize",
"pbjson",
"pbjson-build",
"proc-macro2",
"prost 0.14.4",
"prost-build",
"prost-reflect 0.16.5",
"prost-types 0.14.4",
"prost-wkt-types",
"quote",
@@ -2854,6 +2859,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"strum 0.28.0",
"thiserror 2.0.20",
"tokio",
"url",
@@ -2917,6 +2923,7 @@ dependencies = [
"mimalloc",
"oauth2-reqwest",
"openidconnect",
"optionize",
"password-auth",
"prost 0.14.4",
"rand 0.8.8",
@@ -6384,6 +6391,30 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "optionize"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b6a0111dde86433acde952fdb2757eb65a79ae0e3e58acf378544280d9c3104"
dependencies = [
"derive_more",
"optionize-macros",
]
[[package]]
name = "optionize-macros"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92f30b98a08383cf48fd3d5cc3e3dd27b4a537d94e8488aadff7f3625288b360"
dependencies = [
"darling 0.24.1",
"derive_more",
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "ordered-float"
version = "2.10.1"
@@ -7248,6 +7279,16 @@ dependencies = [
"prost-types 0.13.5",
]
[[package]]
name = "prost-reflect"
version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b80ea363c31af2de2b92e3c07ed1156628f7838c4afb4df75ee78a37fedbd1"
dependencies = [
"prost 0.14.4",
"prost-types 0.14.4",
]
[[package]]
name = "prost-reflect-derive"
version = "0.14.0"
+1
View File
@@ -22,6 +22,7 @@ edition = "2024"
rust-version = "1.95"
[workspace.dependencies]
optionize = "0.5"
# Shared versions and sources; members select features and optional dependencies.
anyhow = "1.0"
arc-swap = "1.9"
@@ -1,6 +1,8 @@
use easytier::proto::ALL_DESCRIPTOR_BYTES;
use once_cell::sync::Lazy;
use prost_reflect::{Cardinality, DescriptorPool, FieldDescriptor, Kind, MessageDescriptor};
use prost_reflect::{
Cardinality, DescriptorPool, FieldDescriptor, FileDescriptor, Kind, MessageDescriptor,
};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -204,7 +206,15 @@ fn build_node(
}
}
fn build_map_entry_node(message_desc: &MessageDescriptor) -> NetworkConfigSchema {
/// The messages being expanded further up, by full name. The pool carries the
/// descriptor's own schema, whose messages refer back to each other, so a type
/// that reaches one it is already inside stops instead of recursing forever.
type ExpansionPath = Vec<String>;
fn build_map_entry_node(
message_desc: &MessageDescriptor,
path: &mut ExpansionPath,
) -> NetworkConfigSchema {
let key_field = message_desc.map_entry_key_field();
let value_field = message_desc.map_entry_value_field();
@@ -221,35 +231,49 @@ fn build_map_entry_node(message_desc: &MessageDescriptor) -> NetworkConfigSchema
Vec::new(),
Vec::new(),
vec![
build_schema_field_node(&key_field),
build_schema_field_node(&value_field),
build_schema_field_node(&key_field, path),
build_schema_field_node(&value_field, path),
],
Vec::new(),
)
}
fn field_children(field: &FieldDescriptor) -> Vec<NetworkConfigSchema> {
fn field_children(field: &FieldDescriptor, path: &mut ExpansionPath) -> Vec<NetworkConfigSchema> {
if field.is_map()
&& let Kind::Message(message_desc) = field.kind()
{
return vec![build_map_entry_node(&message_desc)];
return vec![build_map_entry_node(&message_desc, path)];
}
match field.kind() {
Kind::Message(message_desc) => build_message_children(&message_desc),
Kind::Message(message_desc) => build_message_children(&message_desc, path),
_ => Vec::new(),
}
}
fn build_message_children(message_desc: &MessageDescriptor) -> Vec<NetworkConfigSchema> {
message_desc
.fields()
.filter(should_expose_field)
.map(|field| build_schema_field_node(&field))
.collect()
fn build_message_children(
message_desc: &MessageDescriptor,
path: &mut ExpansionPath,
) -> Vec<NetworkConfigSchema> {
let full_name = message_desc.full_name().to_string();
if path.contains(&full_name) {
return Vec::new();
}
path.push(full_name);
let mut children = Vec::new();
for field in message_desc.fields().filter(should_expose_field) {
children.push(build_schema_field_node(&field, path));
}
path.pop();
children
}
fn build_schema_field_node(field: &FieldDescriptor) -> NetworkConfigSchema {
fn build_schema_field_node(
field: &FieldDescriptor,
path: &mut ExpansionPath,
) -> NetworkConfigSchema {
build_node(
"field",
field.name().to_string(),
@@ -262,17 +286,26 @@ fn build_schema_field_node(field: &FieldDescriptor) -> NetworkConfigSchema {
field_default_value_text(field),
enum_options(field.kind()),
build_validations(field),
field_children(field),
field_children(field, path),
Vec::new(),
)
}
/// The descriptor's own schema is not configuration surface, and its messages
/// are the ones that refer back to each other.
fn is_descriptor_schema(file: &FileDescriptor) -> bool {
file.name() == "google/protobuf/descriptor.proto"
}
fn collect_definitions() -> Vec<NetworkConfigSchema> {
let mut definitions = Vec::new();
for message_desc in descriptor_pool().all_messages() {
let full_name = message_desc.full_name();
if full_name == NETWORK_CONFIG_MESSAGE_NAME || message_desc.is_map_entry() {
if full_name == NETWORK_CONFIG_MESSAGE_NAME
|| message_desc.is_map_entry()
|| is_descriptor_schema(&message_desc.parent_file())
{
continue;
}
@@ -288,12 +321,16 @@ fn collect_definitions() -> Vec<NetworkConfigSchema> {
None,
Vec::new(),
Vec::new(),
build_message_children(&message_desc),
build_message_children(&message_desc, &mut Vec::new()),
Vec::new(),
));
}
for enum_desc in descriptor_pool().all_enums() {
if is_descriptor_schema(&enum_desc.parent_file()) {
continue;
}
definitions.push(build_node(
"enum",
enum_desc.full_name().to_string(),
@@ -329,7 +366,7 @@ fn build_network_config_schema() -> NetworkConfigSchema {
None,
Vec::new(),
Vec::new(),
build_message_children(&network_config),
build_message_children(&network_config, &mut Vec::new()),
collect_definitions(),
)
}
+3
View File
@@ -18,6 +18,7 @@ crate-type = ["rlib", "cdylib"]
wasm-opt = ["-Oz", "--enable-bulk-memory", "--enable-nontrapping-float-to-int"]
[dependencies]
optionize.workspace = true
anyhow.workspace = true
ariadne = { version = "0.6", optional = true }
arc-swap.workspace = true
@@ -164,6 +165,8 @@ zstd = ["dep:zstd"]
[dev-dependencies]
futures = { workspace = true, features = ["executor"] }
# Tests read the (easytier.flag) annotations off the schema itself.
prost-reflect = "0.16.4"
[target.'cfg(not(target_os = "wasi"))'.dev-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "test-util"] }
+5 -52
View File
@@ -4,8 +4,10 @@ use easytier_proto::api::manage::{
self, NetworkConfig, NetworkingMethod, PortForwardConfig as ApiPortForwardConfig,
};
use optionize::Optionizable as _;
use super::{
api_input::managed_credential_to_proto,
api_input::set_network_flags,
toml::{ConfigLoader as _, TomlConfig},
};
@@ -125,61 +127,12 @@ pub fn network_config_from_toml(config: &TomlConfig) -> NetworkConfig {
result.managed_credentials = config
.get_managed_credentials()
.into_iter()
.map(managed_credential_to_proto)
.map(|credential| credential.downgrade())
.collect();
let flags = config.get_flags();
let default_flags = default_config.get_flags();
result.latency_first = Some(flags.latency_first);
result.dev_name = Some(flags.dev_name.clone());
result.use_smoltcp = Some(flags.use_smoltcp);
result.disable_ipv6 = Some(!flags.enable_ipv6);
result.enable_kcp_proxy = Some(flags.enable_kcp_proxy);
result.disable_kcp_input = Some(flags.disable_kcp_input);
result.enable_quic_proxy = Some(flags.enable_quic_proxy);
result.disable_quic_input = Some(flags.disable_quic_input);
result.disable_p2p = Some(flags.disable_p2p);
result.p2p_only = Some(flags.p2p_only);
result.lazy_p2p = Some(flags.lazy_p2p);
result.bind_device = Some(flags.bind_device);
result.socket_mark = flags.socket_mark;
result.no_tun = Some(flags.no_tun);
result.enable_exit_node = Some(flags.enable_exit_node);
result.relay_all_peer_rpc = Some(flags.relay_all_peer_rpc);
result.need_p2p = Some(flags.need_p2p);
result.multi_thread = Some(flags.multi_thread);
result.proxy_forward_by_system = Some(flags.proxy_forward_by_system);
result.disable_encryption = Some(!flags.enable_encryption);
result.disable_tcp_hole_punching = Some(flags.disable_tcp_hole_punching);
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
result.disable_upnp = Some(flags.disable_upnp);
result.disable_relay_data = Some(flags.disable_relay_data);
result.prefer_peer_relay = Some(flags.prefer_peer_relay);
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
result.enable_magic_dns = Some(flags.accept_dns);
result.mtu = Some(flags.mtu as i32);
result.data_compress_algo = (flags.data_compress_algo != default_flags.data_compress_algo)
.then_some(flags.data_compress_algo);
result.encryption_algorithm = (flags.encryption_algorithm
!= default_flags.encryption_algorithm)
.then_some(flags.encryption_algorithm);
result.instance_recv_bps_limit =
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
result.enable_private_mode = Some(flags.private_mode);
set_network_flags(&mut result, config.get_flags_patch());
result.acl = config.get_acl();
if flags.relay_network_whitelist == "*" {
result.enable_relay_network_whitelist = Some(false);
} else {
result.enable_relay_network_whitelist = Some(true);
result.relay_network_whitelist = flags
.relay_network_whitelist
.split_whitespace()
.map(ToOwned::to_owned)
.collect();
}
result
}
+326 -279
View File
@@ -3,13 +3,20 @@
use std::net::SocketAddr;
use anyhow::Context;
use easytier_proto::api::manage;
use easytier_proto::{api::manage, common::FlagsPatch};
#[cfg(all(
feature = "browser-config",
any(test, all(target_arch = "wasm32", target_os = "unknown"))
))]
use easytier_proto::common::Flags;
use optionize::{Optionizable, Optionized};
use crate::config::{
MappedListenerPolicy, normalize_secure_mode_config,
toml::{
ConfigLoader, ManagedCredentialConfig, NetworkIdentity, PeerConfig, PortForwardConfig,
TomlConfigLoader, VpnPortalClientConfig, VpnPortalConfig, gen_default_flags,
ConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader,
VpnPortalClientConfig, VpnPortalConfig,
},
};
@@ -51,32 +58,50 @@ pub fn add_proxy_network_to_config(
pub type NetworkingMethod = easytier_proto::api::manage::NetworkingMethod;
pub type NetworkConfig = easytier_proto::api::manage::NetworkConfig;
pub(crate) fn managed_credential_from_proto(
credential: &manage::ManagedCredentialConfig,
) -> ManagedCredentialConfig {
ManagedCredentialConfig {
credential_id: credential.credential_id.clone(),
credential_secret: credential.credential_secret.clone(),
groups: credential.groups.clone(),
allow_relay: credential.allow_relay,
allowed_proxy_cidrs: credential.allowed_proxy_cidrs.clone(),
expiry_unix: credential.expiry_unix,
reusable: credential.reusable.unwrap_or(true),
}
}
pub(crate) fn set_network_flags(result: &mut NetworkConfig, flags: FlagsPatch) {
result.latency_first = flags.latency_first;
result.dev_name = flags.dev_name;
result.use_smoltcp = flags.use_smoltcp;
result.disable_ipv6 = flags.enable_ipv6.map(|enabled| !enabled);
result.enable_kcp_proxy = flags.enable_kcp_proxy;
result.disable_kcp_input = flags.disable_kcp_input;
result.enable_quic_proxy = flags.enable_quic_proxy;
result.disable_quic_input = flags.disable_quic_input;
result.disable_p2p = flags.disable_p2p;
result.p2p_only = flags.p2p_only;
result.lazy_p2p = flags.lazy_p2p;
result.bind_device = flags.bind_device;
result.socket_mark = flags.socket_mark;
result.no_tun = flags.no_tun;
result.enable_exit_node = flags.enable_exit_node;
result.relay_all_peer_rpc = flags.relay_all_peer_rpc;
result.need_p2p = flags.need_p2p;
result.multi_thread = flags.multi_thread;
result.proxy_forward_by_system = flags.proxy_forward_by_system;
result.disable_encryption = flags.enable_encryption.map(|enabled| !enabled);
result.disable_tcp_hole_punching = flags.disable_tcp_hole_punching;
result.disable_udp_hole_punching = flags.disable_udp_hole_punching;
result.disable_upnp = flags.disable_upnp;
result.disable_relay_data = flags.disable_relay_data;
result.prefer_peer_relay = flags.prefer_peer_relay;
result.enable_udp_broadcast_relay = flags.enable_udp_broadcast_relay;
result.disable_sym_hole_punching = flags.disable_sym_hole_punching;
result.enable_magic_dns = flags.accept_dns;
result.mtu = flags.mtu.map(|mtu| mtu as i32);
result.data_compress_algo = flags.data_compress_algo;
result.encryption_algorithm = flags.encryption_algorithm;
result.instance_recv_bps_limit = flags.instance_recv_bps_limit;
result.enable_private_mode = flags.private_mode;
pub(crate) fn managed_credential_to_proto(
credential: ManagedCredentialConfig,
) -> manage::ManagedCredentialConfig {
manage::ManagedCredentialConfig {
credential_id: credential.credential_id,
credential_secret: credential.credential_secret,
groups: credential.groups,
allow_relay: credential.allow_relay,
allowed_proxy_cidrs: credential.allowed_proxy_cidrs,
expiry_unix: credential.expiry_unix,
reusable: Some(credential.reusable),
}
result.enable_relay_network_whitelist = flags
.relay_network_whitelist
.as_ref()
.map(|list| list != "*");
result.relay_network_whitelist = flags
.relay_network_whitelist
.filter(|list| list != "*")
.map(|list| list.split_whitespace().map(ToOwned::to_owned).collect())
.unwrap_or_default();
}
pub trait NetworkConfigExt {
@@ -112,47 +137,6 @@ const FORM_MANAGED_TOML_FIELDS: &[&str] = &[
"managed_credentials",
];
#[cfg(all(
feature = "browser-config",
any(test, all(target_arch = "wasm32", target_os = "unknown"))
))]
const FORM_MANAGED_FLAG_FIELDS: &[&str] = &[
"latency_first",
"dev_name",
"use_smoltcp",
"enable_ipv6",
"enable_kcp_proxy",
"disable_kcp_input",
"enable_quic_proxy",
"disable_quic_input",
"disable_p2p",
"p2p_only",
"lazy_p2p",
"bind_device",
"socket_mark",
"no_tun",
"enable_exit_node",
"relay_all_peer_rpc",
"need_p2p",
"multi_thread",
"proxy_forward_by_system",
"enable_encryption",
"relay_network_whitelist",
"disable_tcp_hole_punching",
"disable_udp_hole_punching",
"disable_upnp",
"disable_relay_data",
"prefer_peer_relay",
"enable_udp_broadcast_relay",
"disable_sym_hole_punching",
"accept_dns",
"mtu",
"instance_recv_bps_limit",
"private_mode",
"encryption_algorithm",
"data_compress_algo",
];
#[cfg(all(
feature = "browser-config",
any(test, all(target_arch = "wasm32", target_os = "unknown"))
@@ -184,7 +168,22 @@ pub(crate) fn merge_network_config_toml(
.remove("flags")
.and_then(|value| value.try_into().ok())
.unwrap_or_default();
for key in FORM_MANAGED_FLAG_FIELDS {
// Normalize any camelCase aliases in original [flags] to their canonical field names
// so each field appears at most once in the merged table.
for field in Flags::flags() {
if let (Some(name), Some(json_name)) = (field.name.as_deref(), field.json_name.as_deref()) {
if name != json_name
&& let Some(val) = merged_flags.remove(json_name)
{
merged_flags.entry(name.to_owned()).or_insert(val);
}
}
}
// The keys a config form owns are the flags it offers a control for, which
// the schema declares with `(easytier.flag)`.
for key in Flags::form() {
if let Some(value) = generated_flags.get(*key) {
merged_flags.insert((*key).to_owned(), value.clone());
} else {
@@ -444,8 +443,8 @@ impl NetworkConfigExt for NetworkConfig {
cfg.set_managed_credentials(
self.managed_credentials
.iter()
.map(managed_credential_from_proto)
.collect(),
.map(|credential| credential.clone().upgrade())
.collect::<Result<Vec<_>, _>>()?,
);
if let Some(credential_secret) = credential_secret {
@@ -465,19 +464,6 @@ impl NetworkConfigExt for NetworkConfig {
);
}
let mut flags = gen_default_flags();
if let Some(latency_first) = self.latency_first {
flags.latency_first = latency_first;
}
if let Some(dev_name) = self.dev_name.clone() {
flags.dev_name = dev_name;
}
if let Some(use_smoltcp) = self.use_smoltcp {
flags.use_smoltcp = use_smoltcp;
}
if let Some(ipv6_public_addr_provider) = self.ipv6_public_addr_provider {
cfg.set_ipv6_public_addr_provider(ipv6_public_addr_provider);
}
@@ -496,145 +482,59 @@ impl NetworkConfigExt for NetworkConfig {
)?));
}
if let Some(disable_ipv6) = self.disable_ipv6 {
flags.enable_ipv6 = !disable_ipv6;
}
if let Some(enable_kcp_proxy) = self.enable_kcp_proxy {
flags.enable_kcp_proxy = enable_kcp_proxy;
}
if let Some(disable_kcp_input) = self.disable_kcp_input {
flags.disable_kcp_input = disable_kcp_input;
}
if let Some(enable_quic_proxy) = self.enable_quic_proxy {
flags.enable_quic_proxy = enable_quic_proxy;
}
if let Some(disable_quic_input) = self.disable_quic_input {
flags.disable_quic_input = disable_quic_input;
}
if let Some(disable_p2p) = self.disable_p2p {
flags.disable_p2p = disable_p2p;
}
if let Some(p2p_only) = self.p2p_only {
flags.p2p_only = p2p_only;
}
if let Some(lazy_p2p) = self.lazy_p2p {
flags.lazy_p2p = lazy_p2p;
}
if let Some(bind_device) = self.bind_device {
flags.bind_device = bind_device;
}
if self.socket_mark.is_some() {
flags.socket_mark = self.socket_mark;
}
if let Some(no_tun) = self.no_tun {
flags.no_tun = no_tun;
}
if let Some(enable_exit_node) = self.enable_exit_node {
flags.enable_exit_node = enable_exit_node;
}
if let Some(relay_all_peer_rpc) = self.relay_all_peer_rpc {
flags.relay_all_peer_rpc = relay_all_peer_rpc;
}
if let Some(need_p2p) = self.need_p2p {
flags.need_p2p = need_p2p;
}
if let Some(multi_thread) = self.multi_thread {
flags.multi_thread = multi_thread;
}
if let Some(proxy_forward_by_system) = self.proxy_forward_by_system {
flags.proxy_forward_by_system = proxy_forward_by_system;
}
if let Some(disable_encryption) = self.disable_encryption {
flags.enable_encryption = !disable_encryption;
}
if self.enable_relay_network_whitelist.unwrap_or_default() {
if !self.relay_network_whitelist.is_empty() {
flags.relay_network_whitelist = self.relay_network_whitelist.join(" ");
} else {
flags.relay_network_whitelist = "".to_string();
}
}
if let Some(disable_tcp_hole_punching) = self.disable_tcp_hole_punching {
flags.disable_tcp_hole_punching = disable_tcp_hole_punching;
}
if let Some(disable_udp_hole_punching) = self.disable_udp_hole_punching {
flags.disable_udp_hole_punching = disable_udp_hole_punching;
}
if let Some(disable_upnp) = self.disable_upnp {
flags.disable_upnp = disable_upnp;
}
if let Some(disable_relay_data) = self.disable_relay_data {
flags.disable_relay_data = disable_relay_data;
}
if let Some(prefer_peer_relay) = self.prefer_peer_relay {
flags.prefer_peer_relay = prefer_peer_relay;
}
if let Some(enable_udp_broadcast_relay) = self.enable_udp_broadcast_relay {
flags.enable_udp_broadcast_relay = enable_udp_broadcast_relay;
}
if let Some(disable_sym_hole_punching) = self.disable_sym_hole_punching {
flags.disable_sym_hole_punching = disable_sym_hole_punching;
}
if let Some(enable_magic_dns) = self.enable_magic_dns {
flags.accept_dns = enable_magic_dns;
}
if let Some(mtu) = self.mtu {
flags.mtu = mtu as u32;
}
if let Some(instance_recv_bps_limit) = self.instance_recv_bps_limit {
flags.instance_recv_bps_limit = instance_recv_bps_limit;
}
if let Some(enable_private_mode) = self.enable_private_mode {
flags.private_mode = enable_private_mode;
}
if let Some(encryption_algorithm) = self.encryption_algorithm.clone() {
flags.encryption_algorithm = encryption_algorithm;
}
if let Some(acl) = self.acl.as_ref()
&& !acl.is_empty()
{
cfg.set_acl(Some(acl.clone()));
}
if let Some(data_compress_algo) = self.data_compress_algo {
if data_compress_algo < 1 {
flags.data_compress_algo = 1;
} else {
flags.data_compress_algo = data_compress_algo
}
}
cfg.set_flags(flags);
cfg.patch_flags(FlagsPatch {
dev_name: self.dev_name.clone(),
enable_ipv6: self.disable_ipv6.map(|disabled| !disabled),
socket_mark: self.socket_mark,
enable_encryption: self.disable_encryption.map(|disabled| !disabled),
relay_network_whitelist: self.enable_relay_network_whitelist.map(|enabled| {
if enabled {
self.relay_network_whitelist.join(" ")
} else {
"*".to_owned()
}
}),
accept_dns: self.enable_magic_dns,
mtu: self
.mtu
.map(u32::try_from)
.transpose()
.context("invalid mtu: expected a non-negative integer")?,
private_mode: self.enable_private_mode,
encryption_algorithm: self.encryption_algorithm.clone(),
data_compress_algo: self.data_compress_algo.map(|algorithm| algorithm.max(1)),
latency_first: self.latency_first,
use_smoltcp: self.use_smoltcp,
enable_kcp_proxy: self.enable_kcp_proxy,
disable_kcp_input: self.disable_kcp_input,
enable_quic_proxy: self.enable_quic_proxy,
disable_quic_input: self.disable_quic_input,
disable_p2p: self.disable_p2p,
p2p_only: self.p2p_only,
lazy_p2p: self.lazy_p2p,
bind_device: self.bind_device,
no_tun: self.no_tun,
enable_exit_node: self.enable_exit_node,
relay_all_peer_rpc: self.relay_all_peer_rpc,
need_p2p: self.need_p2p,
multi_thread: self.multi_thread,
proxy_forward_by_system: self.proxy_forward_by_system,
disable_tcp_hole_punching: self.disable_tcp_hole_punching,
disable_udp_hole_punching: self.disable_udp_hole_punching,
disable_upnp: self.disable_upnp,
disable_relay_data: self.disable_relay_data,
prefer_peer_relay: self.prefer_peer_relay,
enable_udp_broadcast_relay: self.enable_udp_broadcast_relay,
disable_sym_hole_punching: self.disable_sym_hole_punching,
instance_recv_bps_limit: self.instance_recv_bps_limit,
..Default::default()
});
Ok(cfg)
}
@@ -764,65 +664,11 @@ impl NetworkConfigExt for NetworkConfig {
result.managed_credentials = config
.get_managed_credentials()
.into_iter()
.map(managed_credential_to_proto)
.map(|credential| credential.downgrade())
.collect();
let flags = config.get_flags();
let default_flags = default_config.get_flags();
result.latency_first = Some(flags.latency_first);
result.dev_name = Some(flags.dev_name.clone());
result.use_smoltcp = Some(flags.use_smoltcp);
result.disable_ipv6 = Some(!flags.enable_ipv6);
result.enable_kcp_proxy = Some(flags.enable_kcp_proxy);
result.disable_kcp_input = Some(flags.disable_kcp_input);
result.enable_quic_proxy = Some(flags.enable_quic_proxy);
result.disable_quic_input = Some(flags.disable_quic_input);
result.disable_p2p = Some(flags.disable_p2p);
result.p2p_only = Some(flags.p2p_only);
result.lazy_p2p = Some(flags.lazy_p2p);
result.bind_device = Some(flags.bind_device);
result.socket_mark = flags.socket_mark;
result.no_tun = Some(flags.no_tun);
result.enable_exit_node = Some(flags.enable_exit_node);
result.relay_all_peer_rpc = Some(flags.relay_all_peer_rpc);
result.need_p2p = Some(flags.need_p2p);
result.multi_thread = Some(flags.multi_thread);
result.proxy_forward_by_system = Some(flags.proxy_forward_by_system);
result.disable_encryption = Some(!flags.enable_encryption);
result.disable_tcp_hole_punching = Some(flags.disable_tcp_hole_punching);
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
result.disable_upnp = Some(flags.disable_upnp);
result.disable_relay_data = Some(flags.disable_relay_data);
result.prefer_peer_relay = Some(flags.prefer_peer_relay);
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
result.enable_magic_dns = Some(flags.accept_dns);
result.mtu = Some(flags.mtu as i32);
result.data_compress_algo = (flags.data_compress_algo != default_flags.data_compress_algo)
.then_some(flags.data_compress_algo);
result.encryption_algorithm = (flags.encryption_algorithm
!= default_flags.encryption_algorithm)
.then_some(flags.encryption_algorithm.clone());
result.instance_recv_bps_limit =
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
result.enable_private_mode = Some(flags.private_mode);
set_network_flags(&mut result, config.get_flags_patch());
result.acl = config.get_acl();
if flags.relay_network_whitelist == "*" {
result.enable_relay_network_whitelist = Some(false);
} else {
result.enable_relay_network_whitelist = Some(true);
if flags.relay_network_whitelist.is_empty() {
result.relay_network_whitelist = vec![];
} else {
result.relay_network_whitelist = flags
.relay_network_whitelist
.split_whitespace()
.map(|s| s.to_string())
.collect();
}
}
Ok(result)
}
}
@@ -832,6 +678,7 @@ mod tests {
#![allow(deprecated)]
use super::*;
use easytier_proto::common::Flags;
fn api_portal_config() -> manage::VpnPortalConfig {
manage::VpnPortalConfig {
@@ -853,6 +700,103 @@ mod tests {
}
}
/// The management API's name for a flag is the schema's to state, and the
/// projection is what actually names the fields. Turning the flag the
/// annotation names must turn the field it points at, in the direction the
/// annotation declares.
#[test]
fn declared_api_names_match_the_projection() {
use prost_reflect::{DescriptorPool, Value};
let pool = DescriptorPool::decode(easytier_proto::ALL_DESCRIPTOR_BYTES).unwrap();
let flags = pool.get_message_by_name("common.Flags").unwrap();
let extension = pool.get_extension_by_name("easytier.flag").unwrap();
// A patch states every field, so the values survive serialization:
// protobuf JSON omits a field holding its zero value.
let patch: FlagsPatch = Flags::defaults().downgrade();
let defaults = serde_json::to_value(&patch).unwrap();
for field in flags.fields() {
let name = field.name();
let options = field.options();
let annotation = options.get_extension(&extension).into_owned();
let Value::Message(meta) = annotation else {
panic!("{name}: the annotation is not a message");
};
if !meta.has_field_by_name("api") {
continue;
}
let Value::Message(spelling) = meta.get_field_by_name("api").unwrap().into_owned()
else {
panic!("{name}: the api spelling is not a message");
};
let api_field = match spelling.get_field_by_name("field").as_deref() {
Some(Value::String(api_field)) if !api_field.is_empty() => api_field.clone(),
_ => continue,
};
let negate = matches!(
spelling.get_field_by_name("negate").as_deref(),
Some(Value::Bool(true))
);
let declared = defaults[name]
.as_bool()
.unwrap_or_else(|| panic!("{name} is not a boolean flag"));
let mut flipped = defaults.clone();
flipped[name] = serde_json::json!(!declared);
let patch: FlagsPatch = serde_json::from_value(flipped).unwrap();
let mut projected = NetworkConfig::default();
set_network_flags(&mut projected, patch);
// Protobuf JSON omits a field holding its zero value.
let projected = serde_json::to_value(&projected).unwrap();
let value = projected[&api_field].as_bool().unwrap_or(false);
assert_eq!(
value,
if negate { declared } else { !declared },
"flipping {name} should leave {api_field} as the annotation declares"
);
}
}
#[cfg(feature = "config-write")]
#[test]
fn api_flags_round_trip_preserves_missing_default_and_custom_values() {
for disabled in [None, Some(false), Some(true)] {
for whitelist in [None, Some(false), Some(true)] {
let input = NetworkConfig {
disable_encryption: disabled,
enable_relay_network_whitelist: whitelist,
latency_first: Some(false),
mtu: Some(0),
instance_recv_bps_limit: Some(u64::MAX),
..standalone_config()
};
let config = input.gen_config().unwrap();
let config = TomlConfigLoader::new_from_str(&config.dump()).unwrap();
let output = NetworkConfig::new_from_config(&config).unwrap();
assert_eq!(output.disable_encryption, disabled);
assert_eq!(output.enable_relay_network_whitelist, whitelist);
assert_eq!(output.latency_first, Some(false));
assert_eq!(output.mtu, Some(0));
assert_eq!(output.instance_recv_bps_limit, Some(u64::MAX));
assert_eq!(output.disable_ipv6, None);
assert_eq!(output.encryption_algorithm, None);
}
}
assert!(
NetworkConfig {
mtu: Some(-1),
..standalone_config()
}
.gen_config()
.is_err()
);
}
#[test]
fn vpn_portal_api_config_round_trips_through_toml_model() {
let input = NetworkConfig {
@@ -969,6 +913,46 @@ mod tests {
assert!(config.get_vpn_portal_config().is_none());
}
#[cfg(feature = "browser-config")]
#[test]
fn browser_merge_reconciles_renamed_flags() {
let original = r#"
[flags]
default_protocol = "udp"
enable_encryption = false
enable_ipv6 = false
accept_dns = false
private_mode = false
"#;
for value in [Some(false), Some(true), None] {
let network_config = NetworkConfig {
disable_encryption: value,
disable_ipv6: value,
enable_magic_dns: value,
enable_private_mode: value,
..standalone_config()
};
let merged = merge_network_config_toml(original, &network_config).unwrap();
let merged: toml::Table = toml::from_str(&merged).unwrap();
let flags = merged["flags"].as_table().unwrap();
assert_eq!(flags["default_protocol"].as_str(), Some("udp"));
for (field, expected) in [
("enable_encryption", value.map(|disabled| !disabled)),
("enable_ipv6", value.map(|disabled| !disabled)),
("accept_dns", value),
("private_mode", value),
] {
assert_eq!(
flags.get(field).and_then(toml::Value::as_bool),
expected,
"merging {field} from API value {value:?}"
);
}
}
}
#[cfg(feature = "browser-config")]
#[test]
fn browser_merge_preserves_fields_outside_the_shared_form() {
@@ -1006,11 +990,74 @@ disable_p2p = true
Some("edited-network")
);
assert_eq!(merged["flags"]["default_protocol"].as_str(), Some("udp"));
assert!(
!merged["flags"]
.as_table()
.unwrap()
.contains_key("disable_p2p")
assert_eq!(merged["flags"]["disable_p2p"].as_bool(), Some(false));
}
#[cfg(feature = "browser-config")]
#[test]
fn browser_merge_normalizes_camel_case_flag_aliases() {
let original = r#"
[flags]
disableP2p = true
enableEncryption = false
foreignRelayBpsLimit = 1234
"#;
let parsed_orig = TomlConfigLoader::new_from_str(original).unwrap();
let mut network_config = NetworkConfig::new_from_config(&parsed_orig).unwrap();
network_config.disable_p2p = Some(false);
let merged = merge_network_config_toml(original, &network_config).unwrap();
let parsed = TomlConfigLoader::new_from_str(&merged)
.expect("merged config should be parseable without duplicate-field error");
assert!(!parsed.get_flags().disable_p2p);
assert!(!parsed.get_flags().enable_encryption);
assert_eq!(parsed.get_flags().foreign_relay_bps_limit, 1234);
let merged_table: toml::Table = toml::from_str(&merged).unwrap();
let flags = merged_table["flags"].as_table().unwrap();
assert_eq!(
flags.get("disable_p2p").and_then(toml::Value::as_bool),
Some(false)
);
assert!(flags.get("disableP2p").is_none());
assert_eq!(
flags
.get("enable_encryption")
.and_then(toml::Value::as_bool),
Some(false)
);
assert!(flags.get("enableEncryption").is_none());
assert_eq!(
flags
.get("foreign_relay_bps_limit")
.and_then(toml::Value::as_integer),
Some(1234)
);
assert!(flags.get("foreignRelayBpsLimit").is_none());
}
#[cfg(feature = "browser-config")]
#[test]
fn browser_merge_repro_camel_case_disable_p2p_collision() {
let original = r#"
[flags]
disableP2p = false
"#;
let mut network_config = standalone_config();
network_config.hostname = Some("new-hostname".to_owned());
network_config.disable_p2p = Some(false);
let merged = merge_network_config_toml(original, &network_config).unwrap();
let parsed = TomlConfigLoader::new_from_str(&merged)
.expect("saving config with camelCase alias must not fail on reload");
assert!(!parsed.get_flags().disable_p2p);
let merged_table: toml::Table = toml::from_str(&merged).unwrap();
let flags = merged_table["flags"].as_table().unwrap();
assert_eq!(
flags.get("disable_p2p").and_then(toml::Value::as_bool),
Some(false)
);
assert!(flags.get("disableP2p").is_none());
}
}
+45 -34
View File
@@ -1,48 +1,59 @@
use std::{fmt, str::FromStr};
use serde::{Deserialize, Serialize};
use strum::VariantArray;
/// Stable configuration vocabulary for every known encryption algorithm.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, VariantArray)]
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
VariantArray,
strum::Display,
strum::EnumString,
strum::IntoStaticStr,
strum::AsRefStr,
Serialize,
Deserialize,
)]
#[strum(ascii_case_insensitive)]
pub enum EncryptionAlgorithm {
#[strum(to_string = "xor", serialize = "xor")]
#[serde(rename = "xor")]
Xor,
#[default]
#[strum(
to_string = "aes-gcm",
serialize = "aes-gcm",
serialize = "openssl-aes-gcm"
)]
#[serde(rename = "aes-gcm", alias = "openssl-aes-gcm")]
AesGcm,
#[strum(
to_string = "aes-256-gcm",
serialize = "aes-256-gcm",
serialize = "openssl-aes-256-gcm"
)]
#[serde(rename = "aes-256-gcm", alias = "openssl-aes-256-gcm")]
Aes256Gcm,
#[strum(
to_string = "chacha20",
serialize = "chacha20",
serialize = "chacha20-poly1305",
serialize = "openssl-chacha20"
)]
#[serde(
rename = "chacha20",
alias = "chacha20-poly1305",
alias = "openssl-chacha20"
)]
ChaCha20,
}
impl EncryptionAlgorithm {
pub const fn as_str(self) -> &'static str {
match self {
Self::Xor => "xor",
Self::AesGcm => "aes-gcm",
Self::Aes256Gcm => "aes-256-gcm",
Self::ChaCha20 => "chacha20",
}
}
}
impl fmt::Display for EncryptionAlgorithm {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for EncryptionAlgorithm {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_ascii_lowercase().as_str() {
"xor" => Ok(Self::Xor),
"aes-gcm" | "openssl-aes-gcm" => Ok(Self::AesGcm),
"aes-256-gcm" | "openssl-aes-256-gcm" => Ok(Self::Aes256Gcm),
"chacha20" | "chacha20-poly1305" | "openssl-chacha20" => Ok(Self::ChaCha20),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+7 -17
View File
@@ -48,6 +48,7 @@ use std::{
use anyhow::Context as _;
use base64::{Engine as _, prelude::BASE64_STANDARD};
use easytier_proto::{common as common_pb, core_config as pb};
use optionize::Optionizable;
use serde::{Deserialize, Serialize};
use url::Url;
@@ -280,6 +281,8 @@ pub struct ForeignNetworkConfig {
pub cidrs: Vec<IpPrefix>,
}
#[optionize::optionized]
#[optionize(object = pb::PeerPolicyConfig)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerPolicyConfig {
pub p2p_enabled: bool,
@@ -522,28 +525,15 @@ impl From<ForeignNetworkConfig> for pb::ForeignNetworkConfig {
impl From<pb::PeerPolicyConfig> for PeerPolicyConfig {
fn from(value: pb::PeerPolicyConfig) -> Self {
let default = Self::default();
Self {
p2p_enabled: value.p2p_enabled.unwrap_or(default.p2p_enabled),
relay_peer_rpc: value.relay_peer_rpc.unwrap_or(default.relay_peer_rpc),
relay_data: value.relay_data.unwrap_or(default.relay_data),
latency_first: value.latency_first.unwrap_or(default.latency_first),
encryption_required: value
.encryption_required
.unwrap_or(default.encryption_required),
}
let mut config = Self::default();
config.load(value);
config
}
}
impl From<PeerPolicyConfig> for pb::PeerPolicyConfig {
fn from(value: PeerPolicyConfig) -> Self {
Self {
p2p_enabled: Some(value.p2p_enabled),
relay_peer_rpc: Some(value.relay_peer_rpc),
relay_data: Some(value.relay_data),
latency_first: Some(value.latency_first),
encryption_required: Some(value.encryption_required),
}
value.downgrade()
}
}
+4 -4
View File
@@ -6,7 +6,7 @@
use anyhow::Context as _;
use cidr::Ipv6Cidr;
use easytier_proto::common::{FlagsInConfig, PeerFeatureFlag, SecureModeConfig, StunInfo};
use easytier_proto::common::{Flags, PeerFeatureFlag, SecureModeConfig, StunInfo};
use serde::{Deserialize, Serialize};
use crate::proto::acl::{Acl, AclV1, Action, Chain, ChainType, GroupInfo, Protocol, Rule};
@@ -236,7 +236,7 @@ pub struct PeerRuntimeSnapshot {
pub runtime: PeerRuntimeConfig,
pub easytier_version: String,
pub avoid_relay_data_preference: bool,
pub flags: FlagsInConfig,
pub flags: Flags,
pub pinned_peers: Vec<(url::Url, Option<String>)>,
pub peer_group_memberships: Vec<PeerGroupIdentity>,
pub acl_group_declarations: Vec<PeerGroupIdentity>,
@@ -246,7 +246,7 @@ pub struct PeerRuntimeSnapshot {
}
impl PeerRuntimeSnapshot {
pub fn new(runtime: PeerRuntimeConfig, flags: FlagsInConfig) -> Self {
pub fn new(runtime: PeerRuntimeConfig, flags: Flags) -> Self {
let avoid_relay_data_preference = runtime.feature_flags.avoid_relay_data;
Self {
runtime,
@@ -274,7 +274,7 @@ impl Default for PeerRuntimeSnapshot {
secure_mode: None,
host_routing: HostRoutingPolicy::default(),
},
FlagsInConfig::default(),
Flags::default(),
)
}
}
+124 -212
View File
@@ -11,167 +11,19 @@ pub use super::{EncryptionAlgorithm, gateway::PortForwardConfig};
use anyhow::Context;
#[cfg(feature = "rich-config-errors")]
use ariadne::{CharSet, Config as AriadneConfig, IndexType, Label, Report, ReportKind, Source};
use optionize::Optionizable;
use serde::{Deserialize, Serialize};
use crate::proto::{
acl::Acl,
common::{CompressionAlgoPb, SecureModeConfig},
};
use crate::proto::{acl::Acl, common::SecureModeConfig};
pub const DEFAULT_ET_DNS_ZONE: &str = "et.net.";
pub type Flags = crate::proto::common::FlagsInConfig;
pub use crate::proto::common::{Flags, FlagsPatch};
pub(crate) fn default_instance_name() -> String {
"default".to_owned()
}
pub fn gen_default_flags() -> Flags {
#[allow(deprecated)]
Flags {
default_protocol: "tcp".to_string(),
dev_name: "".to_string(),
enable_encryption: true,
enable_ipv6: true,
mtu: 1380,
latency_first: false,
enable_exit_node: false,
proxy_forward_by_system: false,
no_tun: false,
use_smoltcp: false,
relay_network_whitelist: "*".to_string(),
disable_p2p: false,
p2p_only: false,
lazy_p2p: false,
relay_all_peer_rpc: false,
disable_tcp_hole_punching: false,
disable_udp_hole_punching: false,
multi_thread: true,
data_compress_algo: CompressionAlgoPb::None.into(),
bind_device: true,
enable_kcp_proxy: false,
disable_kcp_input: false,
disable_relay_kcp: false,
enable_relay_foreign_network_kcp: false,
accept_dns: false,
private_mode: false,
enable_quic_proxy: false,
disable_quic_input: false,
disable_relay_quic: false,
enable_relay_foreign_network_quic: false,
foreign_relay_bps_limit: u64::MAX,
multi_thread_count: 2,
encryption_algorithm: EncryptionAlgorithm::default().to_string(),
disable_sym_hole_punching: false,
tld_dns_zone: DEFAULT_ET_DNS_ZONE.to_string(),
quic_listen_port: u32::MAX,
need_p2p: false,
instance_recv_bps_limit: u64::MAX,
disable_upnp: false,
disable_relay_data: false,
prefer_peer_relay: false,
enable_udp_broadcast_relay: false,
socket_mark: None,
}
}
#[cfg(feature = "config-write")]
macro_rules! define_flags_diff {
(
fields: [$($field:ident),* $(,)?],
u64s: [$($u64_field:ident),* $(,)?],
enums: [$($enum_field:ident),* $(,)?]
) => {
#[allow(deprecated)]
fn flags_diff_from_default(flags: &Flags) -> serde_json::Map<String, serde_json::Value> {
let defaults = gen_default_flags();
let mut changed = serde_json::Map::new();
$(
if flags.$field != defaults.$field {
changed.insert(
stringify!($field).to_owned(),
serde_json::to_value(&flags.$field)
.expect("FlagsInConfig field should serialize to JSON"),
);
}
)*
$(
if flags.$u64_field != defaults.$u64_field {
changed.insert(
stringify!($u64_field).to_owned(),
serde_json::json!(flags.$u64_field.to_string()),
);
}
)*
$(
if flags.$enum_field != defaults.$enum_field {
let value = CompressionAlgoPb::try_from(flags.$enum_field)
.map(|value| serde_json::to_value(value).expect("enum should serialize"))
.unwrap_or_else(|_| serde_json::json!(flags.$enum_field));
changed.insert(stringify!($enum_field).to_owned(), value);
}
)*
changed
}
#[cfg(all(test, feature = "config-write"))]
const FLAGS_DIFF_FIELDS: &[&str] = &[
$(stringify!($field),)*
$(stringify!($u64_field),)*
$(stringify!($enum_field),)*
];
};
}
#[cfg(feature = "config-write")]
define_flags_diff! {
fields: [
default_protocol,
dev_name,
enable_encryption,
enable_ipv6,
mtu,
latency_first,
enable_exit_node,
no_tun,
use_smoltcp,
relay_network_whitelist,
disable_p2p,
relay_all_peer_rpc,
disable_udp_hole_punching,
multi_thread,
bind_device,
enable_kcp_proxy,
disable_kcp_input,
disable_relay_kcp,
proxy_forward_by_system,
accept_dns,
private_mode,
enable_quic_proxy,
disable_quic_input,
disable_relay_quic,
quic_listen_port,
multi_thread_count,
enable_relay_foreign_network_kcp,
enable_relay_foreign_network_quic,
encryption_algorithm,
disable_sym_hole_punching,
tld_dns_zone,
p2p_only,
disable_tcp_hole_punching,
lazy_p2p,
need_p2p,
disable_upnp,
disable_relay_data,
prefer_peer_relay,
enable_udp_broadcast_relay,
socket_mark,
],
u64s: [foreign_relay_bps_limit, instance_recv_bps_limit],
enums: [data_compress_algo]
}
#[auto_impl::auto_impl(Box, &)]
pub trait ConfigLoader: Send + Sync {
fn get_id(&self) -> uuid::Uuid;
@@ -231,7 +83,12 @@ pub trait ConfigLoader: Send + Sync {
fn set_vpn_portal_config(&self, config: VpnPortalConfig);
fn get_flags(&self) -> Flags;
/// Replace all flags with explicitly supplied values.
fn set_flags(&self, flags: Flags);
/// The user-supplied values, before application defaults are resolved.
fn get_flags_patch(&self) -> FlagsPatch;
/// Apply only supplied fields, preserving absence and explicit defaults.
fn patch_flags(&self, flags: FlagsPatch);
fn get_exit_nodes(&self) -> Vec<IpAddr>;
fn set_exit_nodes(&self, nodes: Vec<IpAddr>);
@@ -486,19 +343,30 @@ fn default_true() -> bool {
true
}
#[optionize::optionized]
#[cfg_attr(any(feature = "web-client", feature = "browser-config"), optionize(object = easytier_proto::api::manage::ManagedCredentialConfig))]
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ManagedCredentialConfig {
#[optionize(flatten)]
pub credential_id: String,
#[optionize(flatten)]
pub credential_secret: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[optionize(flatten)]
pub groups: Vec<String>,
#[serde(default)]
#[optionize(flatten)]
pub allow_relay: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[optionize(flatten)]
pub allowed_proxy_cidrs: Vec<String>,
#[optionize(flatten)]
pub expiry_unix: i64,
#[serde(default = "default_true")]
// The mapped field is `Option<bool>` (the protocol spells it `optional bool`),
// so this document default does not describe it.
#[optionize(attrs(.., -serde), default = |_| default_true())]
pub reusable: bool,
}
@@ -548,10 +416,8 @@ struct Config {
secure_mode: Option<SecureModeConfig>,
flags: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(skip)]
flags_struct: Option<Flags>,
#[serde(default)]
flags: FlagsPatch,
acl: Option<Acl>,
@@ -631,7 +497,6 @@ impl TomlConfig {
fn config_for_dump(&self) -> Config {
let mut config = self.config.lock().unwrap().clone();
Self::normalize_config_source(&mut config);
config.flags = Some(flags_diff_from_default(&self.get_flags()));
config
}
@@ -705,10 +570,6 @@ impl TomlConfig {
}
fn new_from_config(mut config: Config) -> Result<Self, anyhow::Error> {
config.flags_struct = Some(
Self::gen_flags(config.flags.clone().unwrap_or_default())
.context("failed to parse flags")?,
);
config.secure_mode = config
.secure_mode
.take()
@@ -745,17 +606,6 @@ impl TomlConfig {
Ok(config)
}
fn gen_flags(
flags_hashmap: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Result<Flags> {
let mut merged_hashmap = match serde_json::to_value(gen_default_flags()) {
Ok(serde_json::Value::Object(map)) => map,
_ => serde_json::Map::new(),
};
merged_hashmap.extend(flags_hashmap);
serde_json::from_value(serde_json::Value::Object(merged_hashmap))
}
}
#[cfg(feature = "web-client")]
@@ -1019,16 +869,27 @@ impl ConfigLoader for TomlConfig {
}
fn get_flags(&self) -> Flags {
self.config
.lock()
.unwrap()
.flags_struct
.clone()
.unwrap_or_default()
Flags::resolve(self.get_flags_patch())
}
fn set_flags(&self, flags: Flags) {
self.config.lock().unwrap().flags_struct = Some(flags);
self.config.lock().unwrap().flags = flags.downgrade();
}
fn get_flags_patch(&self) -> FlagsPatch {
self.config.lock().unwrap().flags.clone()
}
fn patch_flags(&self, flags: FlagsPatch) {
// Protobuf merge: an update overwrites exactly the fields it carries,
// and presence is the encoding, so a flag explicitly set to its default
// value is applied like any other. Optionize's merge cannot express that
// here, because the one flattened nullable field would have to assign
// unconditionally.
let mut config = self.config.lock().unwrap();
let update = prost::Message::encode_to_vec(&flags);
prost::Message::merge(&mut config.flags, update.as_slice())
.expect("decoding the bytes just encoded cannot fail");
}
fn get_exit_nodes(&self) -> Vec<IpAddr> {
@@ -1406,37 +1267,9 @@ source = "web"
#[cfg(test)]
mod compatibility_tests {
use super::*;
use crate::proto::common::CompressionAlgoPb;
use base64::{Engine as _, prelude::BASE64_STANDARD};
#[cfg(feature = "config-write")]
#[test]
fn flags_diff_covers_every_protobuf_field() {
use prost::Message as _;
let descriptor_set =
prost_types::FileDescriptorSet::decode(crate::proto::DESCRIPTOR_POOL_BYTES).unwrap();
let proto_fields = descriptor_set
.file
.iter()
.find(|file| file.package.as_deref() == Some("common"))
.and_then(|file| {
file.message_type
.iter()
.find(|message| message.name.as_deref() == Some("FlagsInConfig"))
})
.unwrap()
.field
.iter()
.map(|field| field.name.as_deref().unwrap())
.collect::<std::collections::BTreeSet<_>>();
let diff_fields = FLAGS_DIFF_FIELDS
.iter()
.copied()
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(diff_fields, proto_fields);
}
#[test]
fn socket_mark_config_file_roundtrip_none_some_and_zero() {
// Omitting the flag leaves socket_mark unset (None) -> SO_MARK untouched.
@@ -1486,13 +1319,91 @@ socket_mark = 66
..cfg.get_flags()
});
assert_eq!(cfg.get_flags().socket_mark, None);
#[cfg(feature = "config-write")]
assert_eq!(
TomlConfigLoader::new_from_str(&cfg.dump())
.unwrap()
.get_flags()
.socket_mark,
None
);
}
#[cfg(feature = "config-write")]
#[test]
fn flags_presence_survives_parse_patch_and_dump() {
for (input, expected) in [
("", None),
("enable_encryption = true", Some(true)),
("enable_encryption = false", Some(false)),
] {
let config = TomlConfig::new_from_str(&format!("[flags]\n{input}")).unwrap();
assert_eq!(config.get_flags_patch().enable_encryption, expected);
config.patch_flags(FlagsPatch {
latency_first: Some(false),
..Default::default()
});
let restored = TomlConfig::new_from_str(&config.dump()).unwrap();
assert_eq!(restored.get_flags_patch().enable_encryption, expected);
assert_eq!(restored.get_flags_patch().latency_first, Some(false));
assert_eq!(restored.get_flags_patch().mtu, None);
assert_eq!(
restored.get_flags().enable_encryption,
expected.unwrap_or(true)
);
}
}
#[test]
fn flags_accept_protobuf_aliases_and_numbers_without_default_collisions() {
let config = TomlConfig::new_from_str(
r#"[flags]
enableEncryption = false
mtu = "1420"
foreignRelayBpsLimit = "18446744073709551615"
dataCompressAlgo = "Zstd"
socketMark = "0"
"#,
)
.unwrap();
let flags = config.get_flags();
assert!(!flags.enable_encryption);
assert_eq!(flags.mtu, 1420);
assert_eq!(flags.foreign_relay_bps_limit, u64::MAX);
assert_eq!(flags.data_compress_algo, CompressionAlgoPb::Zstd as i32);
assert_eq!(flags.socket_mark, Some(0));
for fields in [
"enableEncryption = true\nenable_encryption = false",
"mtu = -1",
"mtu = 4294967296",
"unknown_flag = true",
"data_compress_algo = 99",
] {
assert!(
TomlConfig::new_from_str(&format!("[flags]\n{fields}")).is_err(),
"{fields}"
);
}
}
#[cfg(feature = "config-write")]
#[test]
fn dump_omits_all_default_flags() {
let config = TomlConfigLoader::default();
let dumped = config.dump();
let document: toml::Table = toml::from_str(&dumped).unwrap();
assert!(document["flags"].as_table().unwrap().is_empty());
assert_eq!(
TomlConfigLoader::new_from_str(&dumped).unwrap().get_flags(),
Flags::resolve(FlagsPatch::default())
);
}
#[cfg(feature = "config-write")]
#[test]
fn dump_preserves_flags_that_differ_from_easytier_defaults() {
let cfg = TomlConfigLoader::default();
let mut flags = gen_default_flags();
let mut flags = Flags::resolve(FlagsPatch::default());
flags.dev_name = "et_test".to_string();
flags.enable_quic_proxy = true;
flags.disable_tcp_hole_punching = true;
@@ -1996,7 +1907,7 @@ mod diagnostic_compatibility_tests {
}
#[test]
fn flags_conversion_error_keeps_source_and_cause_chain() {
fn flags_parse_error_keeps_source_span_and_cause_chain() {
let error = TomlConfig::new_from_str_with_source(
"flags-fixture.toml",
"[flags]\nsocket_mark = \"bad\"",
@@ -2005,12 +1916,13 @@ mod diagnostic_compatibility_tests {
let display = error.to_string();
assert!(display.contains("flags-fixture.toml"));
assert!(display.contains("failed to load config"));
assert!(display.contains("failed to parse flags"));
assert!(display.contains("failed to parse config TOML"));
assert!(display.contains("socket_mark = \"bad\""));
assert!(display.contains('^'));
assert!(
error
.chain()
.any(|cause| cause.to_string().contains("failed to parse flags"))
.any(|cause| cause.downcast_ref::<toml::de::Error>().is_some())
);
}
}
@@ -5,10 +5,11 @@ use easytier_proto::api::config::{
self, AclPatch, ConfigPatchAction, ExitNodePatch, InstanceConfigPatch, Patchable,
PortForwardPatch, ProxyNetworkPatch, RoutePatch, UrlPatch, VpnPortalClientPatch,
};
use easytier_proto::common::FlagsPatch;
use optionize::Optionized as _;
use crate::{
config::{
api_input::managed_credential_from_proto,
peers::AclRuleConfig,
runtime::CoreInstanceRuntimeConfig,
toml::{ConfigLoader as _, TomlConfig},
@@ -93,16 +94,11 @@ where
if let Some(ipv6) = patch.ipv6 {
candidate.set_ipv6(Some(ipv6.into()));
}
if let Some(disable_relay_data) = patch.disable_relay_data {
let mut flags = candidate.get_flags();
flags.disable_relay_data = disable_relay_data;
candidate.set_flags(flags);
}
if let Some(prefer_peer_relay) = patch.prefer_peer_relay {
let mut flags = candidate.get_flags();
flags.prefer_peer_relay = prefer_peer_relay;
candidate.set_flags(flags);
}
candidate.patch_flags(FlagsPatch {
disable_relay_data: patch.disable_relay_data,
prefer_peer_relay: patch.prefer_peer_relay,
..Default::default()
});
if let Some(enabled) = patch.ipv6_public_addr_provider {
candidate.set_ipv6_public_addr_provider(enabled);
provider_config_changed = true;
@@ -188,8 +184,8 @@ where
let entries = managed
.entries
.iter()
.map(managed_credential_from_proto)
.collect::<Vec<_>>();
.map(|credential| credential.clone().upgrade())
.collect::<Result<Vec<_>, _>>()?;
let replacement = credential_manager
.validate_managed_credentials(&entries)
.map_err(anyhow::Error::msg)?;
+20 -20
View File
@@ -13,7 +13,7 @@ use cidr::{Ipv4Cidr, Ipv4Inet, Ipv6Cidr, Ipv6Inet};
use dashmap::DashMap;
use easytier_proto::{
acl::Acl,
common::{FlagsInConfig, PeerFeatureFlag, SecureModeConfig, StunInfo, TunnelInfo},
common::{Flags, PeerFeatureFlag, SecureModeConfig, StunInfo, TunnelInfo},
peer_rpc::{PeerGroupInfo, TrustedCredentialPubkeyProof},
};
use hmac::{Hmac, Mac};
@@ -71,7 +71,7 @@ pub struct PeerRuntimeSnapshotInput {
pub routes: RouteConfig,
pub network_identity: NetworkIdentity,
pub stun_info: StunInfo,
pub flags: FlagsInConfig,
pub flags: Flags,
pub secure_mode: Option<SecureModeConfig>,
pub host_routing: HostRoutingPolicy,
pub acl: Option<Acl>,
@@ -89,7 +89,7 @@ struct PeerTrafficLimits {
}
impl PeerTrafficLimits {
fn from_portable(runtime: &PeerRuntimeConfig, flags: &FlagsInConfig) -> Self {
fn from_portable(runtime: &PeerRuntimeConfig, flags: &Flags) -> Self {
let traffic = &runtime.core.traffic;
Self {
instance_recv_bps: Self::normalize(
@@ -527,7 +527,7 @@ pub(crate) struct PeerPacketPolicy {
}
impl PeerPacketPolicy {
fn from_flags(flags: &FlagsInConfig) -> Self {
fn from_flags(flags: &Flags) -> Self {
Self {
disable_relay_data: flags.disable_relay_data,
p2p_only: flags.p2p_only,
@@ -549,8 +549,8 @@ pub(crate) trait PeerContext: Send + Sync {
self.network_identity().network_name
}
fn flags(&self) -> FlagsInConfig {
FlagsInConfig::default()
fn flags(&self) -> Flags {
Flags::default()
}
fn packet_policy(&self) -> PeerPacketPolicy {
@@ -743,7 +743,7 @@ impl PeerContext for CorePeerContext {
self.snapshot().runtime.network_identity.clone()
}
fn flags(&self) -> FlagsInConfig {
fn flags(&self) -> Flags {
self.snapshot().flags.clone()
}
@@ -1082,7 +1082,7 @@ pub(crate) mod tests {
}
fn submitted_snapshot(hostname: &str, disable_relay_data: bool) -> PeerRuntimeSnapshot {
let mut flags = FlagsInConfig::default();
let mut flags = Flags::default();
flags.disable_relay_data = disable_relay_data;
PeerRuntimeSnapshot::new(
PeerRuntimeConfig {
@@ -1103,7 +1103,7 @@ pub(crate) mod tests {
)
}
fn host_snapshot_input(flags: FlagsInConfig, acl: Option<Acl>) -> PeerRuntimeSnapshotInput {
fn host_snapshot_input(flags: Flags, acl: Option<Acl>) -> PeerRuntimeSnapshotInput {
PeerRuntimeSnapshotInput {
node: NodeConfig {
peer_id: None,
@@ -1136,7 +1136,7 @@ pub(crate) mod tests {
#[test]
fn host_input_derives_peer_policy_features_and_traffic() {
let mut flags = FlagsInConfig::default();
let mut flags = Flags::default();
flags.disable_p2p = true;
flags.need_p2p = true;
flags.relay_all_peer_rpc = true;
@@ -1196,7 +1196,7 @@ pub(crate) mod tests {
}),
}),
};
let mut flags = FlagsInConfig::default();
let mut flags = Flags::default();
flags.relay_network_whitelist = "other-network".to_owned();
let snapshot = PeerRuntimeSnapshot::from_host_input(host_snapshot_input(flags, Some(acl)));
@@ -1274,7 +1274,7 @@ pub(crate) mod tests {
let mut runtime = PeerRuntimeSnapshot::default().runtime;
runtime.core.traffic.instance_recv_bps_limit = Some(0);
runtime.core.traffic.foreign_relay_bps_limit = Some(2048);
let mut flags = FlagsInConfig::default();
let mut flags = Flags::default();
flags.instance_recv_bps_limit = 1024;
flags.foreign_relay_bps_limit = 4096;
@@ -1287,7 +1287,7 @@ pub(crate) mod tests {
#[test]
fn legacy_traffic_limits_ignore_unlimited_sentinels() {
let runtime = PeerRuntimeSnapshot::default().runtime;
let mut flags = FlagsInConfig::default();
let mut flags = Flags::default();
flags.instance_recv_bps_limit = 1024;
flags.foreign_relay_bps_limit = u64::MAX;
@@ -1302,7 +1302,7 @@ pub(crate) mod tests {
let mut runtime = PeerRuntimeSnapshot::default().runtime;
runtime.core.traffic.instance_recv_bps_limit = Some(u64::MAX);
runtime.core.traffic.foreign_relay_bps_limit = Some(u64::MAX);
let mut flags = FlagsInConfig::default();
let mut flags = Flags::default();
flags.instance_recv_bps_limit = 1024;
flags.foreign_relay_bps_limit = 2048;
@@ -1315,7 +1315,7 @@ pub(crate) mod tests {
fn portable_traffic_limits_default_to_unlimited() {
let runtime = PeerRuntimeSnapshot::default().runtime;
let snapshot = PeerRuntimeSnapshot::new(runtime, FlagsInConfig::default());
let snapshot = PeerRuntimeSnapshot::new(runtime, Flags::default());
assert_eq!(snapshot.traffic_limits(), PeerTrafficLimits::default());
}
@@ -1550,7 +1550,7 @@ pub(crate) mod tests {
local_exit_node_fallback: true,
},
};
let mut flags = FlagsInConfig::default();
let mut flags = Flags::default();
flags.p2p_only = true;
let acl = Acl {
acl_v1: Some(easytier_proto::acl::AclV1 {
@@ -1626,7 +1626,7 @@ pub(crate) mod tests {
live.tcp_nat_type = 4;
let context = core_owned_context(
runtime,
FlagsInConfig::default(),
Flags::default(),
Some(Arc::new(TestStunInfoSource(live.clone()))),
);
@@ -1636,7 +1636,7 @@ pub(crate) mod tests {
fn core_owned_context(
runtime: PeerRuntimeConfig,
flags: FlagsInConfig,
flags: Flags,
stun_info_source: Option<Arc<dyn PeerStunInfoSource>>,
) -> CorePeerContext {
core_owned_context_with_acl(runtime, flags, stun_info_source, None)
@@ -1644,7 +1644,7 @@ pub(crate) mod tests {
fn core_owned_context_with_acl(
runtime: PeerRuntimeConfig,
flags: FlagsInConfig,
flags: Flags,
stun_info_source: Option<Arc<dyn PeerStunInfoSource>>,
acl: Option<&Acl>,
) -> CorePeerContext {
@@ -1679,7 +1679,7 @@ pub(crate) mod tests {
secure_mode: None,
host_routing: HostRoutingPolicy::default(),
},
FlagsInConfig::default(),
Flags::default(),
None,
)
}
+14 -12
View File
@@ -11,7 +11,7 @@ use std::{
};
use dashmap::{DashMap, DashSet};
use easytier_proto::common::FlagsInConfig;
use easytier_proto::common::Flags;
use guarden::defer;
use tokio::sync::{
Mutex, RwLock, RwLockReadGuard,
@@ -84,7 +84,7 @@ fn build_foreign_peer_context(
network: &NetworkIdentity,
parent_context: &Arc<CorePeerContext>,
relay_data: bool,
mut flags: FlagsInConfig,
mut flags: Flags,
) -> Arc<CorePeerContext> {
let parent_context_dyn: ArcPeerContext = parent_context.clone();
let parent_flags = parent_context_dyn.flags();
@@ -372,7 +372,7 @@ impl ForeignNetworkEntry {
my_peer_id: PeerId,
rpc_registrar: Arc<dyn ForeignNetworkRpcRegistrar>,
parent_context: Arc<CorePeerContext>,
foreign_context_default_flags: FlagsInConfig,
foreign_context_default_flags: Flags,
relay_data: bool,
peer_session_store: Arc<PeerSessionStore>,
pm_packet_sender: PacketRecvChan,
@@ -822,7 +822,7 @@ impl ForeignNetworkManagerData {
relay_data: bool,
rpc_registrar: Arc<dyn ForeignNetworkRpcRegistrar>,
parent_context: Arc<CorePeerContext>,
foreign_context_default_flags: FlagsInConfig,
foreign_context_default_flags: Flags,
peer_session_store: Arc<PeerSessionStore>,
pm_packet_sender: &PacketRecvChan,
) -> Option<(Arc<ForeignNetworkEntry>, bool)> {
@@ -874,7 +874,7 @@ enum ForeignNetworkManagerState {
pub(crate) struct ForeignNetworkManager {
rpc_registrar: Arc<dyn ForeignNetworkRpcRegistrar>,
parent_context: Arc<CorePeerContext>,
foreign_context_default_flags: FlagsInConfig,
foreign_context_default_flags: Flags,
peer_session_store: Arc<PeerSessionStore>,
packet_sender_to_mgr: PacketRecvChan,
@@ -912,7 +912,7 @@ impl ForeignNetworkManager {
pub fn new(
rpc_registrar: Arc<dyn ForeignNetworkRpcRegistrar>,
parent_context: Arc<CorePeerContext>,
foreign_context_default_flags: FlagsInConfig,
foreign_context_default_flags: Flags,
peer_session_store: Arc<PeerSessionStore>,
packet_sender_to_mgr: PacketRecvChan,
global_peer_map: Weak<PeerMap>,
@@ -1593,7 +1593,7 @@ mod tests {
};
use dashmap::DashMap;
use easytier_proto::common::{FlagsInConfig, PeerFeatureFlag};
use easytier_proto::common::{Flags, PeerFeatureFlag};
use super::{
ForeignNetworkEntry, ForeignNetworkManager, ForeignNetworkManagerData,
@@ -1638,7 +1638,7 @@ mod tests {
1,
Arc::new(()),
parent,
FlagsInConfig::default(),
Flags::default(),
true,
Arc::new(PeerSessionStore::new()),
packet_sender,
@@ -1825,7 +1825,7 @@ mod tests {
struct FeatureContext {
avoid_relay_data: AtomicBool,
flags: FlagsInConfig,
flags: Flags,
hostname: String,
}
@@ -1833,7 +1833,7 @@ mod tests {
fn new(avoid_relay_data: bool) -> Self {
Self {
avoid_relay_data: AtomicBool::new(avoid_relay_data),
flags: FlagsInConfig::default(),
flags: Flags::default(),
hostname: String::new(),
}
}
@@ -1851,7 +1851,7 @@ mod tests {
}
}
fn flags(&self) -> FlagsInConfig {
fn flags(&self) -> Flags {
self.flags.clone()
}
@@ -1873,6 +1873,8 @@ mod tests {
assert!(check_network_in_relay_whitelist("*", "any-network").is_ok());
assert!(check_network_in_relay_whitelist("", "net1").is_err());
assert!(check_network_in_relay_whitelist("net1 net2*", "net3").is_err());
assert!(check_network_in_relay_whitelist("net1,net2*", "net1").is_ok());
assert!(check_network_in_relay_whitelist("net1, net2*", "net2-west").is_ok());
}
#[test]
@@ -1943,7 +1945,7 @@ mod tests {
network_secret: Some("secret".to_owned()),
network_secret_digest: None,
};
let mut defaults = FlagsInConfig::default();
let mut defaults = Flags::default();
defaults.mtu = 1400;
defaults.relay_network_whitelist = "baseline".to_owned();
let foreign = build_foreign_peer_context(&network, &parent, false, defaults);
+4 -4
View File
@@ -37,7 +37,7 @@ use crate::{
CompressorAlgo, PacketType, ZCPacket,
compressor::{Compressor as _, DefaultCompressor},
},
proto::common::{FlagsInConfig, PeerFeatureFlag, StunInfo, Url as ProtoUrl},
proto::common::{Flags, PeerFeatureFlag, StunInfo, Url as ProtoUrl},
proto::core_peer::peer::{ListPublicIpv6InfoResponse, PeerConnInfo, Route as CoreRoute},
tunnel::{
Tunnel,
@@ -213,14 +213,14 @@ pub struct PortablePeerManagerConfig {
///
/// This is explicit because those contexts participate in the same
/// handshake as the parent but do not inherit all parent policy flags.
pub foreign_context_default_flags: FlagsInConfig,
pub foreign_context_default_flags: Flags,
}
impl PortablePeerManagerConfig {
pub fn new(mut runtime: PeerRuntimeConfig) -> Self {
let policy = &runtime.core.peer_policy;
let traffic = &runtime.core.traffic;
let flags = FlagsInConfig {
let flags = Flags {
enable_encryption: policy.encryption_required,
encryption_algorithm: crate::config::EncryptionAlgorithm::default().to_string(),
disable_p2p: !policy.p2p_enabled,
@@ -956,7 +956,7 @@ impl PeerManagerCore {
is_secure_mode_enabled: bool,
data_compress_algo: CompressorAlgo,
exit_nodes: Vec<IpAddr>,
foreign_context_default_flags: FlagsInConfig,
foreign_context_default_flags: Flags,
foreign_rpc_registrar: Arc<dyn ForeignNetworkRpcRegistrar>,
) -> Self {
let stats_manager = core_context.stats_manager();
+4 -4
View File
@@ -774,13 +774,13 @@ mod tests {
use super::*;
use crate::{
peers::context::{ArcPeerContext, NetworkIdentity, PeerContext},
proto::common::{FlagsInConfig, SecureModeConfig},
proto::common::{Flags, SecureModeConfig},
};
struct RelayTestContext {
network_identity: NetworkIdentity,
secure_mode: SecureModeConfig,
flags: FlagsInConfig,
flags: Flags,
}
impl PeerContext for RelayTestContext {
@@ -792,7 +792,7 @@ mod tests {
Some(self.secure_mode.clone())
}
fn flags(&self) -> FlagsInConfig {
fn flags(&self) -> Flags {
self.flags.clone()
}
}
@@ -866,7 +866,7 @@ mod tests {
local_private_key: Some(BASE64_STANDARD.encode(private.as_bytes())),
local_public_key: Some(BASE64_STANDARD.encode(public.as_bytes())),
},
flags: FlagsInConfig {
flags: Flags {
encryption_algorithm: "aes-gcm".to_owned(),
..Default::default()
},
@@ -4886,8 +4886,8 @@ mod tests {
CoreNetworkIdentity::default()
}
fn flags(&self) -> crate::proto::common::FlagsInConfig {
crate::proto::common::FlagsInConfig {
fn flags(&self) -> crate::proto::common::Flags {
crate::proto::common::Flags {
prefer_peer_relay: self.enabled.load(Ordering::Relaxed),
..Default::default()
}
@@ -5012,7 +5012,7 @@ mod tests {
}
fn test_peer_relay_service_impl(my_peer_id: PeerId) -> PeerRouteServiceImpl {
let flags = crate::proto::common::FlagsInConfig {
let flags = crate::proto::common::Flags {
prefer_peer_relay: true,
..Default::default()
};
+5 -5
View File
@@ -6,7 +6,7 @@
use std::net::IpAddr;
use cidr::{Ipv4Inet, Ipv6Inet};
use easytier_proto::common::{FlagsInConfig, SecureModeConfig};
use easytier_proto::common::{Flags, SecureModeConfig};
use hmac::Hmac;
use sha2::Sha256;
@@ -58,7 +58,7 @@ fn ipv6_inet_to_config(value: Ipv6Inet) -> IpPrefix {
#[derive(Debug, Clone)]
pub(crate) struct NoopPeerContext {
network_identity: NetworkIdentity,
flags: FlagsInConfig,
flags: Flags,
secure_mode: Option<SecureModeConfig>,
}
@@ -66,7 +66,7 @@ impl NoopPeerContext {
pub(crate) fn new(network_identity: NetworkIdentity) -> Self {
Self {
network_identity,
flags: FlagsInConfig::default(),
flags: Flags::default(),
secure_mode: None,
}
}
@@ -77,7 +77,7 @@ impl NoopPeerContext {
self
}
pub(crate) fn with_flags(mut self, flags: FlagsInConfig) -> Self {
pub(crate) fn with_flags(mut self, flags: Flags) -> Self {
self.flags = flags;
self
}
@@ -94,7 +94,7 @@ impl PeerContext for NoopPeerContext {
self.network_identity.clone()
}
fn flags(&self) -> FlagsInConfig {
fn flags(&self) -> Flags {
self.flags.clone()
}
+2 -1
View File
@@ -7,7 +7,8 @@ pub(crate) fn check_network_in_relay_whitelist(
network_name: &str,
) -> Result<(), anyhow::Error> {
if relay_network_whitelist
.split(' ')
.split(|c: char| c.is_whitespace() || c == ',')
.filter(|s| !s.is_empty())
.map(wildmatch::WildMatch::new)
.any(|whitelist| whitelist.matches(network_name))
{
+1 -1
View File
@@ -277,7 +277,7 @@ fn maybe_offload_aead(
pub(crate) fn validate_algorithm(algorithm: &str) -> Result<(), Error> {
let parsed = algorithm
.parse::<EncryptionAlgorithm>()
.map_err(|()| Error::InvalidAlgorithm(algorithm.to_owned()))?;
.map_err(|_| Error::InvalidAlgorithm(algorithm.to_owned()))?;
if algorithm_is_available(parsed) {
Ok(())
} else {
+1
View File
@@ -19,6 +19,7 @@ const (
)
var managementProtoFiles = []string{
"annotations.proto",
"common.proto",
"error.proto",
"acl.proto",
Binary file not shown.
+2 -2
View File
@@ -3,6 +3,6 @@
package artifact
const (
Commit = "599e4eacaa9c9a6f84b8d6439418af9d860f9aa3"
SHA256 = "f5cc76a30d4c6128f7ce77d494dd4e8130aa7e3ce9d5b9ede7604745064bed8d"
Commit = "9a2290f312dbdec6f40e8b13d8a6de5f66e4c570"
SHA256 = "044f9ed79079f449741744d9dd0559d6844e1fff2f1a1994710811904f119986"
)
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: acl.proto
package acl
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: api_config.proto
package config
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: api_instance.proto
package instance
@@ -4399,17 +4399,17 @@ var file_api_instance_proto_goTypes = []any{
nil, // 66: api.instance.ListForeignNetworkResponse.ForeignNetworksEntry
(*ListGlobalForeignNetworkResponse_OneForeignNetwork)(nil), // 67: api.instance.ListGlobalForeignNetworkResponse.OneForeignNetwork
(*ListGlobalForeignNetworkResponse_ForeignNetworks)(nil), // 68: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworks
nil, // 69: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksEntry
nil, // 70: api.instance.MetricSnapshot.LabelsEntry
(*common.UUID)(nil), // 71: common.UUID
(*common.TunnelInfo)(nil), // 72: common.TunnelInfo
(peer_rpc.SecureAuthLevel)(0), // 73: peer_rpc.SecureAuthLevel
(peer_rpc.PeerIdentityType)(0), // 74: peer_rpc.PeerIdentityType
(*common.Ipv4Inet)(nil), // 75: common.Ipv4Inet
(*common.StunInfo)(nil), // 76: common.StunInfo
(*common.PeerFeatureFlag)(nil), // 77: common.PeerFeatureFlag
(*common.Ipv6Inet)(nil), // 78: common.Ipv6Inet
(*peer_rpc.GetIpListResponse)(nil), // 79: peer_rpc.GetIpListResponse
nil, // 69: api.instance.ListGlobalForeignNetworkResponse.ForeignNetworksEntry
nil, // 70: api.instance.MetricSnapshot.LabelsEntry
(*common.UUID)(nil), // 71: common.UUID
(*common.TunnelInfo)(nil), // 72: common.TunnelInfo
(peer_rpc.SecureAuthLevel)(0), // 73: peer_rpc.SecureAuthLevel
(peer_rpc.PeerIdentityType)(0), // 74: peer_rpc.PeerIdentityType
(*common.Ipv4Inet)(nil), // 75: common.Ipv4Inet
(*common.StunInfo)(nil), // 76: common.StunInfo
(*common.PeerFeatureFlag)(nil), // 77: common.PeerFeatureFlag
(*common.Ipv6Inet)(nil), // 78: common.Ipv6Inet
(*peer_rpc.GetIpListResponse)(nil), // 79: peer_rpc.GetIpListResponse
(*peer_rpc.RouteForeignNetworkSummary)(nil), // 80: peer_rpc.RouteForeignNetworkSummary
(*common.Url)(nil), // 81: common.Url
(*common.SocketAddr)(nil), // 82: common.SocketAddr
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: api_manage.proto
package manage
+155 -105
View File
@@ -1,12 +1,13 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: common.proto
package common
import (
_ "github.com/easytier/easytier/easytier-go/proto/easytier"
error1 "github.com/easytier/easytier/easytier-go/proto/error"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -188,7 +189,7 @@ func (SocketType) EnumDescriptor() ([]byte, []int) {
return file_common_proto_rawDescGZIP(), []int{2}
}
type FlagsInConfig struct {
type Flags struct {
state protoimpl.MessageState `protogen:"open.v1"`
DefaultProtocol string `protobuf:"bytes,1,opt,name=default_protocol,json=defaultProtocol,proto3" json:"default_protocol,omitempty"`
DevName string `protobuf:"bytes,2,opt,name=dev_name,json=devName,proto3" json:"dev_name,omitempty"`
@@ -263,20 +264,20 @@ type FlagsInConfig struct {
sizeCache protoimpl.SizeCache
}
func (x *FlagsInConfig) Reset() {
*x = FlagsInConfig{}
func (x *Flags) Reset() {
*x = Flags{}
mi := &file_common_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FlagsInConfig) String() string {
func (x *Flags) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FlagsInConfig) ProtoMessage() {}
func (*Flags) ProtoMessage() {}
func (x *FlagsInConfig) ProtoReflect() protoreflect.Message {
func (x *Flags) ProtoReflect() protoreflect.Message {
mi := &file_common_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -288,180 +289,180 @@ func (x *FlagsInConfig) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use FlagsInConfig.ProtoReflect.Descriptor instead.
func (*FlagsInConfig) Descriptor() ([]byte, []int) {
// Deprecated: Use Flags.ProtoReflect.Descriptor instead.
func (*Flags) Descriptor() ([]byte, []int) {
return file_common_proto_rawDescGZIP(), []int{0}
}
func (x *FlagsInConfig) GetDefaultProtocol() string {
func (x *Flags) GetDefaultProtocol() string {
if x != nil {
return x.DefaultProtocol
}
return ""
}
func (x *FlagsInConfig) GetDevName() string {
func (x *Flags) GetDevName() string {
if x != nil {
return x.DevName
}
return ""
}
func (x *FlagsInConfig) GetEnableEncryption() bool {
func (x *Flags) GetEnableEncryption() bool {
if x != nil {
return x.EnableEncryption
}
return false
}
func (x *FlagsInConfig) GetEnableIpv6() bool {
func (x *Flags) GetEnableIpv6() bool {
if x != nil {
return x.EnableIpv6
}
return false
}
func (x *FlagsInConfig) GetMtu() uint32 {
func (x *Flags) GetMtu() uint32 {
if x != nil {
return x.Mtu
}
return 0
}
func (x *FlagsInConfig) GetLatencyFirst() bool {
func (x *Flags) GetLatencyFirst() bool {
if x != nil {
return x.LatencyFirst
}
return false
}
func (x *FlagsInConfig) GetEnableExitNode() bool {
func (x *Flags) GetEnableExitNode() bool {
if x != nil {
return x.EnableExitNode
}
return false
}
func (x *FlagsInConfig) GetNoTun() bool {
func (x *Flags) GetNoTun() bool {
if x != nil {
return x.NoTun
}
return false
}
func (x *FlagsInConfig) GetUseSmoltcp() bool {
func (x *Flags) GetUseSmoltcp() bool {
if x != nil {
return x.UseSmoltcp
}
return false
}
func (x *FlagsInConfig) GetRelayNetworkWhitelist() string {
func (x *Flags) GetRelayNetworkWhitelist() string {
if x != nil {
return x.RelayNetworkWhitelist
}
return ""
}
func (x *FlagsInConfig) GetDisableP2P() bool {
func (x *Flags) GetDisableP2P() bool {
if x != nil {
return x.DisableP2P
}
return false
}
func (x *FlagsInConfig) GetRelayAllPeerRpc() bool {
func (x *Flags) GetRelayAllPeerRpc() bool {
if x != nil {
return x.RelayAllPeerRpc
}
return false
}
func (x *FlagsInConfig) GetDisableUdpHolePunching() bool {
func (x *Flags) GetDisableUdpHolePunching() bool {
if x != nil {
return x.DisableUdpHolePunching
}
return false
}
func (x *FlagsInConfig) GetMultiThread() bool {
func (x *Flags) GetMultiThread() bool {
if x != nil {
return x.MultiThread
}
return false
}
func (x *FlagsInConfig) GetDataCompressAlgo() CompressionAlgoPb {
func (x *Flags) GetDataCompressAlgo() CompressionAlgoPb {
if x != nil {
return x.DataCompressAlgo
}
return CompressionAlgoPb_Invalid
}
func (x *FlagsInConfig) GetBindDevice() bool {
func (x *Flags) GetBindDevice() bool {
if x != nil {
return x.BindDevice
}
return false
}
func (x *FlagsInConfig) GetEnableKcpProxy() bool {
func (x *Flags) GetEnableKcpProxy() bool {
if x != nil {
return x.EnableKcpProxy
}
return false
}
func (x *FlagsInConfig) GetDisableKcpInput() bool {
func (x *Flags) GetDisableKcpInput() bool {
if x != nil {
return x.DisableKcpInput
}
return false
}
func (x *FlagsInConfig) GetDisableRelayKcp() bool {
func (x *Flags) GetDisableRelayKcp() bool {
if x != nil {
return x.DisableRelayKcp
}
return false
}
func (x *FlagsInConfig) GetProxyForwardBySystem() bool {
func (x *Flags) GetProxyForwardBySystem() bool {
if x != nil {
return x.ProxyForwardBySystem
}
return false
}
func (x *FlagsInConfig) GetAcceptDns() bool {
func (x *Flags) GetAcceptDns() bool {
if x != nil {
return x.AcceptDns
}
return false
}
func (x *FlagsInConfig) GetPrivateMode() bool {
func (x *Flags) GetPrivateMode() bool {
if x != nil {
return x.PrivateMode
}
return false
}
func (x *FlagsInConfig) GetEnableQuicProxy() bool {
func (x *Flags) GetEnableQuicProxy() bool {
if x != nil {
return x.EnableQuicProxy
}
return false
}
func (x *FlagsInConfig) GetDisableQuicInput() bool {
func (x *Flags) GetDisableQuicInput() bool {
if x != nil {
return x.DisableQuicInput
}
return false
}
func (x *FlagsInConfig) GetDisableRelayQuic() bool {
func (x *Flags) GetDisableRelayQuic() bool {
if x != nil {
return x.DisableRelayQuic
}
@@ -469,126 +470,126 @@ func (x *FlagsInConfig) GetDisableRelayQuic() bool {
}
// Deprecated: Marked as deprecated in common.proto.
func (x *FlagsInConfig) GetQuicListenPort() uint32 {
func (x *Flags) GetQuicListenPort() uint32 {
if x != nil {
return x.QuicListenPort
}
return 0
}
func (x *FlagsInConfig) GetForeignRelayBpsLimit() uint64 {
func (x *Flags) GetForeignRelayBpsLimit() uint64 {
if x != nil {
return x.ForeignRelayBpsLimit
}
return 0
}
func (x *FlagsInConfig) GetMultiThreadCount() uint32 {
func (x *Flags) GetMultiThreadCount() uint32 {
if x != nil {
return x.MultiThreadCount
}
return 0
}
func (x *FlagsInConfig) GetEnableRelayForeignNetworkKcp() bool {
func (x *Flags) GetEnableRelayForeignNetworkKcp() bool {
if x != nil {
return x.EnableRelayForeignNetworkKcp
}
return false
}
func (x *FlagsInConfig) GetEnableRelayForeignNetworkQuic() bool {
func (x *Flags) GetEnableRelayForeignNetworkQuic() bool {
if x != nil {
return x.EnableRelayForeignNetworkQuic
}
return false
}
func (x *FlagsInConfig) GetEncryptionAlgorithm() string {
func (x *Flags) GetEncryptionAlgorithm() string {
if x != nil {
return x.EncryptionAlgorithm
}
return ""
}
func (x *FlagsInConfig) GetDisableSymHolePunching() bool {
func (x *Flags) GetDisableSymHolePunching() bool {
if x != nil {
return x.DisableSymHolePunching
}
return false
}
func (x *FlagsInConfig) GetTldDnsZone() string {
func (x *Flags) GetTldDnsZone() string {
if x != nil {
return x.TldDnsZone
}
return ""
}
func (x *FlagsInConfig) GetP2POnly() bool {
func (x *Flags) GetP2POnly() bool {
if x != nil {
return x.P2POnly
}
return false
}
func (x *FlagsInConfig) GetDisableTcpHolePunching() bool {
func (x *Flags) GetDisableTcpHolePunching() bool {
if x != nil {
return x.DisableTcpHolePunching
}
return false
}
func (x *FlagsInConfig) GetLazyP2P() bool {
func (x *Flags) GetLazyP2P() bool {
if x != nil {
return x.LazyP2P
}
return false
}
func (x *FlagsInConfig) GetNeedP2P() bool {
func (x *Flags) GetNeedP2P() bool {
if x != nil {
return x.NeedP2P
}
return false
}
func (x *FlagsInConfig) GetInstanceRecvBpsLimit() uint64 {
func (x *Flags) GetInstanceRecvBpsLimit() uint64 {
if x != nil {
return x.InstanceRecvBpsLimit
}
return 0
}
func (x *FlagsInConfig) GetDisableUpnp() bool {
func (x *Flags) GetDisableUpnp() bool {
if x != nil {
return x.DisableUpnp
}
return false
}
func (x *FlagsInConfig) GetDisableRelayData() bool {
func (x *Flags) GetDisableRelayData() bool {
if x != nil {
return x.DisableRelayData
}
return false
}
func (x *FlagsInConfig) GetEnableUdpBroadcastRelay() bool {
func (x *Flags) GetEnableUdpBroadcastRelay() bool {
if x != nil {
return x.EnableUdpBroadcastRelay
}
return false
}
func (x *FlagsInConfig) GetSocketMark() uint32 {
func (x *Flags) GetSocketMark() uint32 {
if x != nil && x.SocketMark != nil {
return *x.SocketMark
}
return 0
}
func (x *FlagsInConfig) GetPreferPeerRelay() bool {
func (x *Flags) GetPreferPeerRelay() bool {
if x != nil {
return x.PreferPeerRelay
}
@@ -2147,59 +2148,108 @@ var File_common_proto protoreflect.FileDescriptor
const file_common_proto_rawDesc = "" +
"\n" +
"\fcommon.proto\x12\x06common\x1a\verror.proto\"\xf5\x0e\n" +
"\rFlagsInConfig\x12)\n" +
"\x10default_protocol\x18\x01 \x01(\tR\x0fdefaultProtocol\x12\x19\n" +
"\bdev_name\x18\x02 \x01(\tR\adevName\x12+\n" +
"\x11enable_encryption\x18\x03 \x01(\bR\x10enableEncryption\x12\x1f\n" +
"\venable_ipv6\x18\x04 \x01(\bR\n" +
"enableIpv6\x12\x10\n" +
"\x03mtu\x18\x05 \x01(\rR\x03mtu\x12#\n" +
"\rlatency_first\x18\x06 \x01(\bR\flatencyFirst\x12(\n" +
"\x10enable_exit_node\x18\a \x01(\bR\x0eenableExitNode\x12\x15\n" +
"\x06no_tun\x18\b \x01(\bR\x05noTun\x12\x1f\n" +
"\vuse_smoltcp\x18\t \x01(\bR\n" +
"useSmoltcp\x126\n" +
"\fcommon.proto\x12\x06common\x1a\x11annotations.proto\x1a\verror.proto\"\x8a\x14\n" +
"\x05Flags\x124\n" +
"\x10default_protocol\x18\x01 \x01(\tB\t\x8a\xb5\x18\x05\n" +
"\x03tcpR\x0fdefaultProtocol\x12#\n" +
"\bdev_name\x18\x02 \x01(\tB\b\x8a\xb5\x18\x04\n" +
"\x00\x18\x01R\adevName\x12O\n" +
"\x11enable_encryption\x18\x03 \x01(\bB\"\x8a\xb5\x18\x1e\n" +
"\x04true\x12\x16\n" +
"\x12disable_encryption\x10\x01R\x10enableEncryption\x12=\n" +
"\venable_ipv6\x18\x04 \x01(\bB\x1c\x8a\xb5\x18\x18\n" +
"\x04true\x12\x10\n" +
"\fdisable_ipv6\x10\x01R\n" +
"enableIpv6\x12\x1e\n" +
"\x03mtu\x18\x05 \x01(\rB\f\x8a\xb5\x18\b\n" +
"\x041380\x18\x01R\x03mtu\x120\n" +
"\rlatency_first\x18\x06 \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\flatencyFirst\x125\n" +
"\x10enable_exit_node\x18\a \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x0eenableExitNode\x12\"\n" +
"\x06no_tun\x18\b \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x05noTun\x12,\n" +
"\vuse_smoltcp\x18\t \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\n" +
"useSmoltcp\x12A\n" +
"\x17relay_network_whitelist\x18\n" +
" \x01(\tR\x15relayNetworkWhitelist\x12\x1f\n" +
"\vdisable_p2p\x18\v \x01(\bR\n" +
"disableP2p\x12+\n" +
"\x12relay_all_peer_rpc\x18\f \x01(\bR\x0frelayAllPeerRpc\x129\n" +
"\x19disable_udp_hole_punching\x18\r \x01(\bR\x16disableUdpHolePunching\x12!\n" +
"\fmulti_thread\x18\x0f \x01(\bR\vmultiThread\x12G\n" +
"\x12data_compress_algo\x18\x10 \x01(\x0e2\x19.common.CompressionAlgoPbR\x10dataCompressAlgo\x12\x1f\n" +
"\vbind_device\x18\x11 \x01(\bR\n" +
"bindDevice\x12(\n" +
"\x10enable_kcp_proxy\x18\x12 \x01(\bR\x0eenableKcpProxy\x12*\n" +
"\x11disable_kcp_input\x18\x13 \x01(\bR\x0fdisableKcpInput\x12*\n" +
"\x11disable_relay_kcp\x18\x14 \x01(\bR\x0fdisableRelayKcp\x125\n" +
"\x17proxy_forward_by_system\x18\x15 \x01(\bR\x14proxyForwardBySystem\x12\x1d\n" +
" \x01(\tB\t\x8a\xb5\x18\x05\n" +
"\x01*\x18\x01R\x15relayNetworkWhitelist\x12,\n" +
"\vdisable_p2p\x18\v \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\n" +
"disableP2p\x128\n" +
"\x12relay_all_peer_rpc\x18\f \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x0frelayAllPeerRpc\x12F\n" +
"\x19disable_udp_hole_punching\x18\r \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x16disableUdpHolePunching\x12-\n" +
"\fmulti_thread\x18\x0f \x01(\bB\n" +
"\x8a\xb5\x18\x06\n" +
"\x04trueR\vmultiThread\x12U\n" +
"\x12data_compress_algo\x18\x10 \x01(\x0e2\x19.common.CompressionAlgoPbB\f\x8a\xb5\x18\b\n" +
"\x04None\x18\x01R\x10dataCompressAlgo\x12+\n" +
"\vbind_device\x18\x11 \x01(\bB\n" +
"\x8a\xb5\x18\x06\n" +
"\x04trueR\n" +
"bindDevice\x125\n" +
"\x10enable_kcp_proxy\x18\x12 \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x0eenableKcpProxy\x127\n" +
"\x11disable_kcp_input\x18\x13 \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x0fdisableKcpInput\x127\n" +
"\x11disable_relay_kcp\x18\x14 \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x0fdisableRelayKcp\x12B\n" +
"\x17proxy_forward_by_system\x18\x15 \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x14proxyForwardBySystem\x12>\n" +
"\n" +
"accept_dns\x18\x16 \x01(\bR\tacceptDns\x12!\n" +
"\fprivate_mode\x18\x17 \x01(\bR\vprivateMode\x12*\n" +
"\x11enable_quic_proxy\x18\x18 \x01(\bR\x0fenableQuicProxy\x12,\n" +
"\x12disable_quic_input\x18\x19 \x01(\bR\x10disableQuicInput\x12,\n" +
"\x12disable_relay_quic\x18# \x01(\bR\x10disableRelayQuic\x12,\n" +
"\x10quic_listen_port\x18! \x01(\rB\x02\x18\x01R\x0equicListenPort\x125\n" +
"\x17foreign_relay_bps_limit\x18\x1a \x01(\x04R\x14foreignRelayBpsLimit\x12,\n" +
"\x12multi_thread_count\x18\x1b \x01(\rR\x10multiThreadCount\x12F\n" +
" enable_relay_foreign_network_kcp\x18\x1c \x01(\bR\x1cenableRelayForeignNetworkKcp\x12H\n" +
"!enable_relay_foreign_network_quic\x18$ \x01(\bR\x1denableRelayForeignNetworkQuic\x121\n" +
"\x14encryption_algorithm\x18\x1d \x01(\tR\x13encryptionAlgorithm\x129\n" +
"\x19disable_sym_hole_punching\x18\x1e \x01(\bR\x16disableSymHolePunching\x12 \n" +
"\ftld_dns_zone\x18\x1f \x01(\tR\n" +
"tldDnsZone\x12\x19\n" +
"\bp2p_only\x18 \x01(\bR\ap2pOnly\x129\n" +
"\x19disable_tcp_hole_punching\x18\" \x01(\bR\x16disableTcpHolePunching\x12\x19\n" +
"\blazy_p2p\x18% \x01(\bR\alazyP2p\x12\x19\n" +
"\bneed_p2p\x18& \x01(\bR\aneedP2p\x125\n" +
"\x17instance_recv_bps_limit\x18' \x01(\x04R\x14instanceRecvBpsLimit\x12!\n" +
"\fdisable_upnp\x18( \x01(\bR\vdisableUpnp\x12,\n" +
"\x12disable_relay_data\x18) \x01(\bR\x10disableRelayData\x12;\n" +
"\x1aenable_udp_broadcast_relay\x18* \x01(\bR\x17enableUdpBroadcastRelay\x12$\n" +
"\vsocket_mark\x18+ \x01(\rH\x00R\n" +
"socketMark\x88\x01\x01\x12*\n" +
"\x11prefer_peer_relay\x18, \x01(\bR\x0fpreferPeerRelayB\x0e\n" +
"accept_dns\x18\x16 \x01(\bB\x1f\x8a\xb5\x18\x1b\n" +
"\x05false\x12\x12\n" +
"\x10enable_magic_dnsR\tacceptDns\x12E\n" +
"\fprivate_mode\x18\x17 \x01(\bB\"\x8a\xb5\x18\x1e\n" +
"\x05false\x12\x15\n" +
"\x13enable_private_modeR\vprivateMode\x127\n" +
"\x11enable_quic_proxy\x18\x18 \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x0fenableQuicProxy\x129\n" +
"\x12disable_quic_input\x18\x19 \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x10disableQuicInput\x129\n" +
"\x12disable_relay_quic\x18# \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x10disableRelayQuic\x12>\n" +
"\x10quic_listen_port\x18! \x01(\rB\x14\x8a\xb5\x18\x0e\n" +
"\n" +
"4294967295 \x01\x18\x01R\x0equicListenPort\x12Q\n" +
"\x17foreign_relay_bps_limit\x18\x1a \x01(\x04B\x1a\x8a\xb5\x18\x16\n" +
"\x1418446744073709551615R\x14foreignRelayBpsLimit\x125\n" +
"\x12multi_thread_count\x18\x1b \x01(\rB\a\x8a\xb5\x18\x03\n" +
"\x012R\x10multiThreadCount\x12S\n" +
" enable_relay_foreign_network_kcp\x18\x1c \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x1cenableRelayForeignNetworkKcp\x12U\n" +
"!enable_relay_foreign_network_quic\x18$ \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x1denableRelayForeignNetworkQuic\x12B\n" +
"\x14encryption_algorithm\x18\x1d \x01(\tB\x0f\x8a\xb5\x18\v\n" +
"\aaes-gcm\x18\x01R\x13encryptionAlgorithm\x12F\n" +
"\x19disable_sym_hole_punching\x18\x1e \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x16disableSymHolePunching\x12/\n" +
"\ftld_dns_zone\x18\x1f \x01(\tB\r\x8a\xb5\x18\t\n" +
"\aet.net.R\n" +
"tldDnsZone\x12&\n" +
"\bp2p_only\x18 \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\ap2pOnly\x12F\n" +
"\x19disable_tcp_hole_punching\x18\" \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x16disableTcpHolePunching\x12&\n" +
"\blazy_p2p\x18% \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\alazyP2p\x12&\n" +
"\bneed_p2p\x18& \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\aneedP2p\x12S\n" +
"\x17instance_recv_bps_limit\x18' \x01(\x04B\x1c\x8a\xb5\x18\x18\n" +
"\x1418446744073709551615\x18\x01R\x14instanceRecvBpsLimit\x12.\n" +
"\fdisable_upnp\x18( \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\vdisableUpnp\x129\n" +
"\x12disable_relay_data\x18) \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x10disableRelayData\x12H\n" +
"\x1aenable_udp_broadcast_relay\x18* \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x17enableUdpBroadcastRelay\x12,\n" +
"\vsocket_mark\x18+ \x01(\rB\x06\x8a\xb5\x18\x02\x18\x01H\x00R\n" +
"socketMark\x88\x01\x01\x127\n" +
"\x11prefer_peer_relay\x18, \x01(\bB\v\x8a\xb5\x18\a\n" +
"\x05falseR\x0fpreferPeerRelayB\x0e\n" +
"\f_socket_mark\"\x95\x01\n" +
"\rRpcDescriptor\x12\x1f\n" +
"\vdomain_name\x18\x01 \x01(\tR\n" +
@@ -2371,7 +2421,7 @@ var file_common_proto_goTypes = []any{
(CompressionAlgoPb)(0), // 0: common.CompressionAlgoPb
(NatType)(0), // 1: common.NatType
(SocketType)(0), // 2: common.SocketType
(*FlagsInConfig)(nil), // 3: common.FlagsInConfig
(*Flags)(nil), // 3: common.Flags
(*RpcDescriptor)(nil), // 4: common.RpcDescriptor
(*RpcRequest)(nil), // 5: common.RpcRequest
(*DirectRpcRequest)(nil), // 6: common.DirectRpcRequest
@@ -2398,7 +2448,7 @@ var file_common_proto_goTypes = []any{
(*error1.Error)(nil), // 27: error.Error
}
var file_common_proto_depIdxs = []int32{
0, // 0: common.FlagsInConfig.data_compress_algo:type_name -> common.CompressionAlgoPb
0, // 0: common.Flags.data_compress_algo:type_name -> common.CompressionAlgoPb
4, // 1: common.RpcRequest.descriptor:type_name -> common.RpcDescriptor
6, // 2: common.HostManagementRequest.rpc:type_name -> common.DirectRpcRequest
12, // 3: common.HostManagementRequest.prepared_instance_id:type_name -> common.UUID
@@ -0,0 +1,242 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.36.1
// source: annotations.proto
package easytier
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
descriptorpb "google.golang.org/protobuf/types/descriptorpb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// The name the management API spells a flag with, where it differs from the
// flag's own, and whether that field is the flag's negation.
type ApiSpelling struct {
state protoimpl.MessageState `protogen:"open.v1"`
Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"`
Negate bool `protobuf:"varint,2,opt,name=negate,proto3" json:"negate,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ApiSpelling) Reset() {
*x = ApiSpelling{}
mi := &file_annotations_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ApiSpelling) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ApiSpelling) ProtoMessage() {}
func (x *ApiSpelling) ProtoReflect() protoreflect.Message {
mi := &file_annotations_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ApiSpelling.ProtoReflect.Descriptor instead.
func (*ApiSpelling) Descriptor() ([]byte, []int) {
return file_annotations_proto_rawDescGZIP(), []int{0}
}
func (x *ApiSpelling) GetField() string {
if x != nil {
return x.Field
}
return ""
}
func (x *ApiSpelling) GetNegate() bool {
if x != nil {
return x.Negate
}
return false
}
// What a flag is worth and how the layers around it name it.
//
// `default` holds the value the core runs for a flag a configuration does not
// state, written the way the flag's own type reads; an `optional` flag leaves
// it out, which is what "unset" means. A form has a non-boolean control for
// `form`; boolean flags need no marker, since the type says so.
type FlagMeta struct {
state protoimpl.MessageState `protogen:"open.v1"`
Default *string `protobuf:"bytes,1,opt,name=default,proto3,oneof" json:"default,omitempty"`
Api *ApiSpelling `protobuf:"bytes,2,opt,name=api,proto3" json:"api,omitempty"`
Form bool `protobuf:"varint,3,opt,name=form,proto3" json:"form,omitempty"`
Deprecated bool `protobuf:"varint,4,opt,name=deprecated,proto3" json:"deprecated,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *FlagMeta) Reset() {
*x = FlagMeta{}
mi := &file_annotations_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *FlagMeta) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FlagMeta) ProtoMessage() {}
func (x *FlagMeta) ProtoReflect() protoreflect.Message {
mi := &file_annotations_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FlagMeta.ProtoReflect.Descriptor instead.
func (*FlagMeta) Descriptor() ([]byte, []int) {
return file_annotations_proto_rawDescGZIP(), []int{1}
}
func (x *FlagMeta) GetDefault() string {
if x != nil && x.Default != nil {
return *x.Default
}
return ""
}
func (x *FlagMeta) GetApi() *ApiSpelling {
if x != nil {
return x.Api
}
return nil
}
func (x *FlagMeta) GetForm() bool {
if x != nil {
return x.Form
}
return false
}
func (x *FlagMeta) GetDeprecated() bool {
if x != nil {
return x.Deprecated
}
return false
}
var file_annotations_proto_extTypes = []protoimpl.ExtensionInfo{
{
ExtendedType: (*descriptorpb.FieldOptions)(nil),
ExtensionType: (*FlagMeta)(nil),
Field: 50001,
Name: "easytier.flag",
Tag: "bytes,50001,opt,name=flag",
Filename: "annotations.proto",
},
}
// Extension fields to descriptorpb.FieldOptions.
var (
// optional easytier.FlagMeta flag = 50001;
E_Flag = &file_annotations_proto_extTypes[0]
)
var File_annotations_proto protoreflect.FileDescriptor
const file_annotations_proto_rawDesc = "" +
"\n" +
"\x11annotations.proto\x12\beasytier\x1a google/protobuf/descriptor.proto\";\n" +
"\vApiSpelling\x12\x14\n" +
"\x05field\x18\x01 \x01(\tR\x05field\x12\x16\n" +
"\x06negate\x18\x02 \x01(\bR\x06negate\"\x92\x01\n" +
"\bFlagMeta\x12\x1d\n" +
"\adefault\x18\x01 \x01(\tH\x00R\adefault\x88\x01\x01\x12'\n" +
"\x03api\x18\x02 \x01(\v2\x15.easytier.ApiSpellingR\x03api\x12\x12\n" +
"\x04form\x18\x03 \x01(\bR\x04form\x12\x1e\n" +
"\n" +
"deprecated\x18\x04 \x01(\bR\n" +
"deprecatedB\n" +
"\n" +
"\b_default:G\n" +
"\x04flag\x12\x1d.google.protobuf.FieldOptions\x18ц\x03 \x01(\v2\x12.easytier.FlagMetaR\x04flagb\x06proto3"
var (
file_annotations_proto_rawDescOnce sync.Once
file_annotations_proto_rawDescData []byte
)
func file_annotations_proto_rawDescGZIP() []byte {
file_annotations_proto_rawDescOnce.Do(func() {
file_annotations_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_annotations_proto_rawDesc), len(file_annotations_proto_rawDesc)))
})
return file_annotations_proto_rawDescData
}
var file_annotations_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_annotations_proto_goTypes = []any{
(*ApiSpelling)(nil), // 0: easytier.ApiSpelling
(*FlagMeta)(nil), // 1: easytier.FlagMeta
(*descriptorpb.FieldOptions)(nil), // 2: google.protobuf.FieldOptions
}
var file_annotations_proto_depIdxs = []int32{
0, // 0: easytier.FlagMeta.api:type_name -> easytier.ApiSpelling
2, // 1: easytier.flag:extendee -> google.protobuf.FieldOptions
1, // 2: easytier.flag:type_name -> easytier.FlagMeta
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
2, // [2:3] is the sub-list for extension type_name
1, // [1:2] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_annotations_proto_init() }
func file_annotations_proto_init() {
if File_annotations_proto != nil {
return
}
file_annotations_proto_msgTypes[1].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_annotations_proto_rawDesc), len(file_annotations_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 1,
NumServices: 0,
},
GoTypes: file_annotations_proto_goTypes,
DependencyIndexes: file_annotations_proto_depIdxs,
MessageInfos: file_annotations_proto_msgTypes,
ExtensionInfos: file_annotations_proto_extTypes,
}.Build()
File_annotations_proto = out.File
file_annotations_proto_goTypes = nil
file_annotations_proto_depIdxs = nil
}
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: error.proto
package error
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: peer_rpc.proto
package peer_rpc
+2 -2
View File
@@ -3,6 +3,6 @@
package proto
const (
EasyTierCommit = "599e4eacaa9c9a6f84b8d6439418af9d860f9aa3"
SchemaSHA256 = "7de60ee229e6ee2f17e3673cff6f9d9d4d4bece13ed74e46b1999277b364db67"
EasyTierCommit = "9a2290f312dbdec6f40e8b13d8a6de5f66e4c570"
SchemaSHA256 = "c145983fee99e08f6207c425cea0952c1aaf50f9bfea12c834d1ec985e0b39df"
)
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// protoc v7.36.1
// source: web.proto
package web
+4 -2
View File
@@ -11,8 +11,8 @@ if [[ ! -f "${proto_root}/api_instance.proto" ]]; then
exit 1
fi
if [[ "$(protoc --version)" != "libprotoc 35.1" ]]; then
echo "protoc 35.1 is required" >&2
if [[ "$(protoc --version)" != "libprotoc 35.1" && "$(protoc --version)" != "libprotoc 36.1" ]]; then
echo "protoc 35.1 or 36.1 is required" >&2
exit 1
fi
if [[ "$(protoc-gen-go --version)" != "protoc-gen-go v1.36.11" ]]; then
@@ -24,6 +24,7 @@ protoc \
-I "${proto_root}" \
--go_out="${repository_root}" \
--go_opt=module=github.com/easytier/easytier/easytier-go \
--go_opt=Mannotations.proto=github.com/easytier/easytier/easytier-go/proto/easytier \
--go_opt=Mcommon.proto=github.com/easytier/easytier/easytier-go/proto/common \
--go_opt=Merror.proto=github.com/easytier/easytier/easytier-go/proto/error \
--go_opt=Macl.proto=github.com/easytier/easytier/easytier-go/proto/acl \
@@ -32,6 +33,7 @@ protoc \
--go_opt=Mapi_config.proto=github.com/easytier/easytier/easytier-go/proto/api/config \
--go_opt=Mapi_manage.proto=github.com/easytier/easytier/easytier-go/proto/api/manage \
--go_opt=Mweb.proto=github.com/easytier/easytier/easytier-go/proto/web \
"${proto_root}/annotations.proto" \
"${proto_root}/common.proto" \
"${proto_root}/error.proto" \
"${proto_root}/acl.proto" \
+10
View File
@@ -13,6 +13,7 @@ license-file = "../LICENSE"
build = "build/main.rs"
[dependencies]
optionize = { workspace = true, optional = true }
anyhow = { workspace = true, optional = true }
async-trait = { workspace = true, optional = true }
auto_impl = { workspace = true, optional = true }
@@ -28,6 +29,7 @@ pbjson = { version = "0.9.0", optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
serde_json = { workspace = true, optional = true }
sha2 = { workspace = true, optional = true }
strum = { workspace = true, features = ["derive"], optional = true }
thiserror = { workspace = true, optional = true }
tokio = { workspace = true, features = ["time"], optional = true }
url = { workspace = true, features = ["serde"], optional = true }
@@ -35,11 +37,16 @@ uuid = { workspace = true, features = ["serde"], optional = true }
x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true }
[build-dependencies]
anyhow.workspace = true
indoc.workspace = true
pbjson-build = "0.9.0"
proc-macro2 = "1"
prost.workspace = true
prost-build = "0.14.4"
prost-reflect = "0.16.4"
prost-types.workspace = true
quote = "1"
serde_json.workspace = true
[target.'cfg(windows)'.build-dependencies]
reqwest = { workspace = true, default-features = true, features = ["blocking"] }
@@ -71,6 +78,7 @@ full = [
]
api = ["core"]
core = [
"dep:optionize",
"dep:anyhow",
"dep:async-trait",
"dep:auto_impl",
@@ -85,6 +93,7 @@ core = [
"dep:serde",
"dep:serde_json",
"dep:sha2",
"dep:strum",
"dep:thiserror",
"dep:tokio",
"dep:url",
@@ -114,4 +123,5 @@ ignored = [
"auto_impl",
"pbjson",
"serde",
"strum",
]
+149
View File
@@ -0,0 +1,149 @@
//! Reads the `(easytier.flag)` annotations off `common.Flags` and writes them
//! out as Rust the crate compiles in.
//!
//! Each annotation carries what the core runs for a flag a configuration does
//! not state, the name the management API spells it with, and whether a config
//! form offers it. Nothing here is a second list: the field, its type and its
//! presence are the schema's, and an unannotated field fails the build.
use std::{
fmt::Write as _,
path::{Path, PathBuf},
};
use anyhow::{Context as _, bail, ensure};
use prost_reflect::{DescriptorPool, DynamicMessage, Kind, Value};
pub fn write(descriptor_set: &[u8], out: &Path) -> anyhow::Result<PathBuf> {
let pool = DescriptorPool::decode(descriptor_set)?;
let message = pool
.get_message_by_name("common.Flags")
.context("the compiled schema declares no common.Flags")?;
let extension = pool
.get_extension_by_name("easytier.flag")
.context("annotations.proto declares no easytier.flag")?;
// A form reads and writes the management API, so a flag it manages has to
// be one the API carries.
let api = pool
.get_message_by_name("api.manage.NetworkConfig")
.context("the compiled schema declares no api.manage.NetworkConfig")?;
let api_carries = |name: &str| api.get_field_by_name(name).is_some();
let mut defaults: Vec<String> = Vec::new();
let mut form = String::new();
for field in message.fields() {
let name = field.name();
let options = field.options();
ensure!(
options.has_extension(&extension),
"common.Flags.{name} carries no (easytier.flag) annotation"
);
let Value::Message(meta) = options.get_extension(&extension).into_owned() else {
bail!("easytier.flag on common.Flags.{name} is not a message");
};
// The value the flag runs with, written the way its type reads; JSON
// needs quotes around text, and an optional flag that states nothing
// is the one that runs unset. What the value *means* is protobuf's to
// say when the crate reads this document back.
let value = match text(&meta, "default", name)? {
Some(text) => match field.kind() {
Kind::String | Kind::Enum(_) => serde_json::to_string(&text)?,
_ => text,
},
None if field.supports_presence() => "null".to_owned(),
None => bail!("common.Flags.{name} states no default"),
};
defaults.push(format!(" {name:?}: {value}"));
// The name the management API uses, where the flag's own name is not it.
let spelling = match meta.get_field_by_name("api").as_deref() {
Some(Value::Message(api)) => match api.get_field_by_name("field").as_deref() {
Some(Value::String(field)) if !field.is_empty() => Some(field.clone()),
_ => None,
},
Some(_) => bail!("common.Flags.{name}: the api spelling is not a message"),
_ => None,
};
if let Some(spelling) = &spelling {
ensure!(
api_carries(spelling),
"common.Flags.{name} is spelled {spelling} by the management API, \
which carries no such field"
);
}
let deprecated = is_set(&meta, "deprecated");
ensure!(
deprecated
== matches!(
options.get_field_by_name("deprecated").as_deref(),
Some(Value::Bool(true))
),
"common.Flags.{name}: the annotation says deprecated = {deprecated}, \
but the field says the contrary"
);
// The schema field names a config form owns, used as TOML keys.
//
// Only what the API carries can be a key here: the merge drops a key
// the form produces nothing for, so a flag the API cannot express
// would be deleted from a stored configuration on every save.
let api_name = spelling.as_deref().unwrap_or(name);
if is_set(&meta, "form") {
ensure!(
api_carries(api_name),
"common.Flags.{name} says a form has a control for it, but \
api.manage.NetworkConfig carries no {api_name}"
);
}
if !deprecated
&& api_carries(api_name)
&& (matches!(field.kind(), Kind::Bool) || is_set(&meta, "form"))
{
writeln!(form, " {name:?},")?;
}
}
let defaults = defaults.join(",\n");
let generated = format!(
"\
// @generated by easytier-proto's build script from the (easytier.flag)
// annotations on proto/common.proto. Do not edit.
/// The values a network runs with when a configuration states no flag, as the
/// schema declares them in protobuf JSON.
pub const DEFAULTS: &str = r##\"{{
{defaults}}}\"##;
/// The schema field names of flags managed by the config form.
pub const FORM: &[&str] = &[
{form}];
"
);
let path = out.join("flags.rs");
std::fs::write(&path, generated)?;
Ok(path)
}
/// A text field of the annotation, absent when the annotation leaves it out.
fn text(meta: &DynamicMessage, field: &str, flag: &str) -> anyhow::Result<Option<String>> {
if !meta.has_field_by_name(field) {
return Ok(None);
}
match meta.get_field_by_name(field).as_deref() {
Some(Value::String(text)) => Ok(Some(text.clone())),
Some(other) => bail!("common.Flags.{flag}: {field} is {other:?}, expected text"),
None => Ok(None),
}
}
fn is_set(meta: &DynamicMessage, field: &str) -> bool {
matches!(
meta.get_field_by_name(field).as_deref(),
Some(Value::Bool(true))
)
}
+69 -7
View File
@@ -1,3 +1,4 @@
mod flags;
mod rpc;
use crate::rpc::ServiceGenerator;
@@ -81,7 +82,7 @@ fn ensure_protoc_for_windows() {
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn main() -> anyhow::Result<()> {
#[cfg(target_os = "windows")]
ensure_protoc_for_windows();
@@ -106,7 +107,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
let out = PathBuf::from(env::var("OUT_DIR")?);
let descriptor = out.join("descriptors.bin");
let mut config = prost_build::Config::new();
if env::var_os("CARGO_FEATURE_JSON_RPC").is_some() {
@@ -121,7 +121,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.extern_path(".google.protobuf.Value", "::prost_types::Value");
}
config
.file_descriptor_set_path(&descriptor)
.file_descriptor_set_path(out.join("descriptors.bin"))
.service_generator(Box::new(ServiceGenerator::default()))
.btree_map(["."])
.skip_debug([
@@ -130,16 +130,78 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".common.UUID",
".api.manage.ManagedCredentialConfig",
".api.manage.VpnPortalConfig",
]);
])
.type_attribute(
".common.CompressionAlgoPb",
"#[derive(strum::EnumString, strum::Display)]",
)
.type_attribute(
".common.CompressionAlgoPb",
"#[strum(ascii_case_insensitive)]",
)
.field_attribute(".common.CompressionAlgoPb.Invalid", "#[strum(disabled)]");
config.compile_protos(&proto_files, &["proto/"])?;
let mut descriptor_set = config.load_fds(&proto_files, &["proto/"])?;
// What protoc wrote (this set carries every file, imports included), read
// before anything else may touch it: the annotations are extension fields,
// which a prost-types round-trip drops.
let annotated = std::fs::read(out.join("descriptors.bin"))?;
{
let common = descriptor_set
.file
.iter_mut()
.find(|file| file.package.as_deref() == Some("common"))
.unwrap();
let flags = common
.message_type
.iter()
.find(|message| message.name.as_deref() == Some("Flags"))
.unwrap();
config
.type_attribute(".common.Flags", "#[optionize::optionized]")
.type_attribute(".common.Flags", "#[optionize(object = FlagsPatch)]");
let flattened = flags
.field
.iter()
.filter(|field| field.proto3_optional())
.map(|field| field.name().to_owned());
for name in flattened {
config.field_attribute(format!(".common.Flags.{name}"), "#[optionize(flatten)]");
}
let mut patch = flags.clone();
patch.name = Some("FlagsPatch".to_owned());
// Proto3 spells `optional` as one synthetic one-of per field.
let oneofs = patch
.field
.iter()
.map(|field| prost_types::OneofDescriptorProto {
name: Some(format!("_{}", field.name())),
..Default::default()
})
.collect::<Vec<_>>();
patch.oneof_decl = oneofs;
for (index, field) in patch.field.iter_mut().enumerate() {
field.proto3_optional = Some(true);
field.label = Some(prost_types::field_descriptor_proto::Label::Optional as i32);
field.oneof_index = Some(index as i32);
}
common.message_type.push(patch);
config.disable_comments([".common.FlagsPatch"]);
}
let descriptors = prost::Message::encode_to_vec(&descriptor_set);
config.file_descriptor_set_path(out.join("file_descriptor_set.bin"));
config.compile_protos(&proto_files_reflect, &["proto/"])?;
config.compile_fds(descriptor_set)?;
flags::write(&annotated, &out)?;
let descriptor = std::fs::read(descriptor)?;
pbjson_build::Builder::new()
.register_descriptors(&descriptor)?
.register_descriptors(&descriptors)?
.preserve_proto_field_names()
.btree_map(["."])
.build(&["."])?;
+29
View File
@@ -0,0 +1,29 @@
syntax = "proto3";
package easytier;
import "google/protobuf/descriptor.proto";
// The name the management API spells a flag with, where it differs from the
// flag's own, and whether that field is the flag's negation.
message ApiSpelling {
string field = 1;
bool negate = 2;
}
// What a flag is worth and how the layers around it name it.
//
// `default` holds the value the core runs for a flag a configuration does not
// state, written the way the flag's own type reads; an `optional` flag leaves
// it out, which is what "unset" means. A form has a non-boolean control for
// `form`; boolean flags need no marker, since the type says so.
message FlagMeta {
optional string default = 1;
ApiSpelling api = 2;
bool form = 3;
bool deprecated = 4;
}
extend google.protobuf.FieldOptions {
FlagMeta flag = 50001;
}
+46 -44
View File
@@ -1,95 +1,97 @@
syntax = "proto3";
import "annotations.proto";
import "error.proto";
package common;
message FlagsInConfig {
string default_protocol = 1;
string dev_name = 2;
bool enable_encryption = 3;
bool enable_ipv6 = 4;
uint32 mtu = 5;
bool latency_first = 6;
bool enable_exit_node = 7;
bool no_tun = 8;
bool use_smoltcp = 9;
string relay_network_whitelist = 10;
bool disable_p2p = 11;
bool relay_all_peer_rpc = 12;
bool disable_udp_hole_punching = 13;
message Flags {
string default_protocol = 1 [(easytier.flag) = { default: "tcp" }];
string dev_name = 2 [(easytier.flag) = { default: "", form: true }];
bool enable_encryption = 3 [(easytier.flag) = { default: "true", api: { field: "disable_encryption", negate: true } }];
bool enable_ipv6 = 4 [(easytier.flag) = { default: "true", api: { field: "disable_ipv6", negate: true } }];
uint32 mtu = 5 [(easytier.flag) = { default: "1380", form: true }];
bool latency_first = 6 [(easytier.flag) = { default: "false" }];
bool enable_exit_node = 7 [(easytier.flag) = { default: "false" }];
bool no_tun = 8 [(easytier.flag) = { default: "false" }];
bool use_smoltcp = 9 [(easytier.flag) = { default: "false" }];
string relay_network_whitelist = 10 [(easytier.flag) = { default: "*", form: true }];
bool disable_p2p = 11 [(easytier.flag) = { default: "false" }];
bool relay_all_peer_rpc = 12 [(easytier.flag) = { default: "false" }];
bool disable_udp_hole_punching = 13 [(easytier.flag) = { default: "false" }];
// string ipv6_listener = 14; [deprecated = true]; use -l udp://[::]:12345
// instead
bool multi_thread = 15;
CompressionAlgoPb data_compress_algo = 16;
bool bind_device = 17;
bool multi_thread = 15 [(easytier.flag) = { default: "true" }];
CompressionAlgoPb data_compress_algo = 16 [(easytier.flag) = { default: "None", form: true }];
bool bind_device = 17 [(easytier.flag) = { default: "true" }];
// should we convert all tcp streams into kcp streams
bool enable_kcp_proxy = 18;
bool enable_kcp_proxy = 18 [(easytier.flag) = { default: "false" }];
// does this peer allow kcp input
bool disable_kcp_input = 19;
bool disable_kcp_input = 19 [(easytier.flag) = { default: "false" }];
// disable relay local network kcp packets
bool disable_relay_kcp = 20;
bool proxy_forward_by_system = 21;
bool disable_relay_kcp = 20 [(easytier.flag) = { default: "false" }];
bool proxy_forward_by_system = 21 [(easytier.flag) = { default: "false" }];
// enable magic dns or not
bool accept_dns = 22;
bool accept_dns = 22 [(easytier.flag) = { default: "false", api: { field: "enable_magic_dns" } }];
// enable private mode
bool private_mode = 23;
bool private_mode = 23 [(easytier.flag) = { default: "false", api: { field: "enable_private_mode" } }];
// should we convert all tcp streams into quic streams
bool enable_quic_proxy = 24;
bool enable_quic_proxy = 24 [(easytier.flag) = { default: "false" }];
// does this peer allow quic input
bool disable_quic_input = 25;
bool disable_quic_input = 25 [(easytier.flag) = { default: "false" }];
// disable relay local network quic packets
bool disable_relay_quic = 35;
bool disable_relay_quic = 35 [(easytier.flag) = { default: "false" }];
// quic listen port
uint32 quic_listen_port = 33 [deprecated = true];
uint32 quic_listen_port = 33 [deprecated = true, (easytier.flag) = { default: "4294967295", deprecated: true }];
// a global relay limit, only work for foreign network
uint64 foreign_relay_bps_limit = 26;
uint64 foreign_relay_bps_limit = 26 [(easytier.flag) = { default: "18446744073709551615" }];
uint32 multi_thread_count = 27;
uint32 multi_thread_count = 27 [(easytier.flag) = { default: "2" }];
// enable relay foreign network kcp packets
bool enable_relay_foreign_network_kcp = 28;
bool enable_relay_foreign_network_kcp = 28 [(easytier.flag) = { default: "false" }];
// enable relay foreign network quic packets
bool enable_relay_foreign_network_quic = 36;
bool enable_relay_foreign_network_quic = 36 [(easytier.flag) = { default: "false" }];
// encryption algorithm to use, empty string means default (aes-gcm)
string encryption_algorithm = 29;
string encryption_algorithm = 29 [(easytier.flag) = { default: "aes-gcm", form: true }];
// disable symmetric nat hole punching, treat symmetric as cone when enabled
bool disable_sym_hole_punching = 30;
bool disable_sym_hole_punching = 30 [(easytier.flag) = { default: "false" }];
// tld dns zone for magic dns
string tld_dns_zone = 31;
string tld_dns_zone = 31 [(easytier.flag) = { default: "et.net." }];
bool p2p_only = 32;
bool p2p_only = 32 [(easytier.flag) = { default: "false" }];
bool disable_tcp_hole_punching = 34;
bool disable_tcp_hole_punching = 34 [(easytier.flag) = { default: "false" }];
bool lazy_p2p = 37;
bool need_p2p = 38;
uint64 instance_recv_bps_limit = 39;
bool disable_upnp = 40;
bool disable_relay_data = 41;
bool enable_udp_broadcast_relay = 42;
bool lazy_p2p = 37 [(easytier.flag) = { default: "false" }];
bool need_p2p = 38 [(easytier.flag) = { default: "false" }];
uint64 instance_recv_bps_limit = 39 [(easytier.flag) = { default: "18446744073709551615", form: true }];
bool disable_upnp = 40 [(easytier.flag) = { default: "false" }];
bool disable_relay_data = 41 [(easytier.flag) = { default: "false" }];
bool enable_udp_broadcast_relay = 42 [(easytier.flag) = { default: "false" }];
// Linux-only: SO_MARK (fwmark) value applied to every outbound underlay
// socket (TCP/UDP/QUIC/WS/WG connectors and listeners). Unset = leave
// SO_MARK untouched (kernel default 0). Any set value (including 0) is
// applied via setsockopt. Requires CAP_NET_ADMIN; silently ignored on
// non-Linux platforms.
optional uint32 socket_mark = 43;
optional uint32 socket_mark = 43 [(easytier.flag) = { form: true }];
// Prefer direct credential peers that already relay to a destination over
// advertising another direct edge to the same destination.
bool prefer_peer_relay = 44;
bool prefer_peer_relay = 44 [(easytier.flag) = { default: "false" }];
}
message RpcDescriptor {
// allow same service registered multiple times in different domain
string domain_name = 1;
+73
View File
@@ -1,9 +1,12 @@
use anyhow::Context;
use base64::{Engine as _, prelude::BASE64_STANDARD};
use optionize::Optionizable as _;
use prost::Message as _;
use std::time::SystemTime;
use std::{
fmt::{self, Display},
str::FromStr,
sync::OnceLock,
};
const IP_SCHEMES: &[&str] = &["tcp", "udp", "wg", "quic", "ws", "wss", "faketcp"];
@@ -11,6 +14,76 @@ const IP_SCHEMES: &[&str] = &["tcp", "udp", "wg", "quic", "ws", "wss", "faketcp"
include!(concat!(env!("OUT_DIR"), "/common.rs"));
include!(concat!(env!("OUT_DIR"), "/common.serde.rs"));
impl Flags {
/// The flags the message declares, in declaration order, less the ones it
/// has deprecated: what a user can still set.
///
/// Read back from the descriptor set this crate embeds, so a flag added to
/// the message shows up here without a second list to keep in step.
pub fn flags() -> &'static [prost_types::FieldDescriptorProto] {
static FLAGS: OnceLock<Vec<prost_types::FieldDescriptorProto>> = OnceLock::new();
FLAGS.get_or_init(|| {
prost_types::FileDescriptorSet::decode(crate::DESCRIPTOR_POOL_BYTES)
.unwrap()
.file
.into_iter()
.find(|file| file.package() == "common")
.unwrap()
.message_type
.into_iter()
.find(|message| message.name() == "Flags")
.unwrap()
.field
.into_iter()
.filter(|field| {
!field
.options
.as_ref()
.is_some_and(|options| options.deprecated())
})
.collect()
})
}
/// The values a network runs with when a configuration states no flag.
///
/// The build script reads them off the `(easytier.flag)` annotations on the
/// message, in protobuf JSON, and the config UI reads the same annotations
/// from its own bindings, so an unset flag means the same thing on both
/// sides.
pub fn defaults() -> Self {
static DEFAULTS: OnceLock<Flags> = OnceLock::new();
DEFAULTS
.get_or_init(|| {
serde_json::from_str(flags::DEFAULTS)
.expect("the build script writes the defaults from the schema")
})
.clone()
}
/// Applies `patch` over [`Flags::defaults`]. A field the user left out keeps
/// its default, so nothing downstream has to decide what "unset" means.
pub fn resolve(patch: FlagsPatch) -> Self {
let mut flags = Self::defaults();
flags.load(patch);
flags
}
/// The schema field names of flags managed by the config form.
///
/// These are TOML keys. Only flags the management API carries are included:
/// a merge drops a key the form produces nothing for.
pub fn form() -> &'static [&'static str] {
flags::FORM
}
}
/// What the `(easytier.flag)` annotations on the message declare, generated by
/// this crate's build script.
mod flags {
include!(concat!(env!("OUT_DIR"), "/flags.rs"));
}
pub trait TimestampExt {
fn now() -> Self;
}
+1
View File
@@ -6,6 +6,7 @@ rust-version.workspace = true
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
[dependencies]
optionize.workspace = true
easytier = { workspace = true, default-features = true }
easytier-core = { workspace = true, default-features = true }
tracing.workspace = true
@@ -15,6 +15,7 @@ const protocWrapper = require.resolve('@protobuf-ts/protoc/protoc.js')
const protobufTsPluginRoot = dirname(require.resolve('@protobuf-ts/plugin/package.json'))
const protoFiles = [
'annotations.proto',
'common.proto',
'acl.proto',
'api_instance.proto',
@@ -6,6 +6,8 @@ import InputGroupAddon from 'primevue/inputgroupaddon'
import {
addRow,
DEFAULT_NETWORK_CONFIG,
defaultFlagValue,
formFlags,
NetworkConfig,
normalizeNetworkConfig,
removeRow,
@@ -137,34 +139,28 @@ interface BoolFlag {
}
const bool_flags: BoolFlag[] = [
{ field: 'latency_first', help: 'latency_first_help' },
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
{ field: 'disable_ipv6', help: 'disable_ipv6_help' },
// Every boolean flag the schema declares, plus the one network option the
// form offers as a checkbox.
...formFlags(),
{ field: 'ipv6_public_addr_auto', help: 'ipv6_public_addr_auto_help' },
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
{ field: 'disable_quic_input', help: 'disable_quic_input_help' },
{ field: 'disable_p2p', help: 'disable_p2p_help' },
{ field: 'p2p_only', help: 'p2p_only_help' },
{ field: 'lazy_p2p', help: 'lazy_p2p_help' },
{ field: 'bind_device', help: 'bind_device_help' },
{ field: 'no_tun', help: 'no_tun_help' },
{ field: 'enable_exit_node', help: 'enable_exit_node_help' },
{ field: 'relay_all_peer_rpc', help: 'relay_all_peer_rpc_help' },
{ field: 'need_p2p', help: 'need_p2p_help' },
{ field: 'multi_thread', help: 'multi_thread_help' },
{ field: 'proxy_forward_by_system', help: 'proxy_forward_by_system_help' },
{ field: 'disable_encryption', help: 'disable_encryption_help' },
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
{ field: 'enable_udp_broadcast_relay', help: 'enable_udp_broadcast_relay_help' },
{ field: 'disable_upnp', help: 'disable_upnp_help' },
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
]
/**
* Whether a flag's checkbox is on: what the configuration states, or the value
* the network runs with when it states nothing. A flag this config leaves unset
* is not off, it is the default, and several default to on.
*/
function flagChecked(field: keyof NetworkConfig): boolean {
const value = curNetwork.value[field]
return typeof value === 'boolean' ? value : defaultFlagValue(field)
}
/** A checkbox states a flag; leaving it alone leaves the config silent. */
function setFlag(field: keyof NetworkConfig, value: boolean) {
Object.assign(curNetwork.value, { [field]: value })
}
const portForwardProtocolOptions = ref(["tcp", "udp"]);
const editingPortForward = ref(false);
@@ -394,7 +390,8 @@ function removeVpnPortalClient(index: number) {
<div class="flex flex-row flex-wrap">
<div class="basis-[20rem] flex items-center" v-for="flag in bool_flags">
<Checkbox v-model="curNetwork[flag.field]" :input-id="flag.field" :binary="true" />
<Checkbox :model-value="flagChecked(flag.field)" :input-id="flag.field" :binary="true"
@update:model-value="setFlag(flag.field, $event)" />
<label :for="flag.field" class="ml-2"> {{ t(flag.field) }} </label>
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t(flag.help)"></span>
</div>
@@ -800,3 +800,8 @@ acl:
name: 名称
type: 类型
match: 匹配
disable_relay_data: 不转发数据
disable_relay_data_help: 不转发其他节点的数据包,本节点不承载自身以外的流量。
prefer_peer_relay: 优先凭证节点转发
prefer_peer_relay_help: 优先通过已经转发到目标节点的凭证节点转发,而不是为同一目标再通告一条直连路径。
@@ -800,3 +800,8 @@ acl:
name: Name
type: Type
match: Match
disable_relay_data: Disable Data Relay
disable_relay_data_help: Do not relay data packets for other peers, so this node carries no traffic it did not originate.
prefer_peer_relay: Prefer Peer Relay
prefer_peer_relay_help: Prefer a credential peer that already relays to a destination over advertising another direct edge to the same destination.
+66 -25
View File
@@ -8,6 +8,8 @@ import {
type VpnPortalClientConfig,
type VpnPortalConfig,
} from '../generated/proto/api_manage'
import { ScalarType } from '@protobuf-ts/runtime'
import type { FlagMeta } from '../generated/proto/annotations'
import {
VpnPortalClientState,
VpnPortalInfo as VpnPortalInfoPb,
@@ -27,6 +29,7 @@ import {
} from '../generated/proto/acl'
import {
CompressionAlgoPb,
Flags as FlagsPb,
NatType,
type PeerFeatureFlag,
type SecureModeConfig,
@@ -87,6 +90,68 @@ function emptyAcl(): Acl {
}
}
/** A flag of `common.Flags` that carries an annotation. */
interface AnnotatedFlag {
/** The name the management API uses for the flag. */
field: string
/** Whether the schema types the flag as a boolean. */
boolean: boolean
meta: FlagMeta
}
const annotatedFlags: AnnotatedFlag[] = FlagsPb.fields.flatMap((flag) => {
const meta = flag.options?.['easytier.flag'] as FlagMeta | undefined
if (!meta) return []
return [{
field: meta.api?.field ?? flag.name,
boolean: flag.kind === 'scalar' && flag.T === ScalarType.BOOL,
meta,
}]
})
/** The value a flag's annotation declares, as the flag itself is typed. */
function declaredValue(flag: AnnotatedFlag): boolean {
const declared = flag.meta.default === 'true'
return flag.meta.api?.negate ? !declared : declared
}
/**
* The value a flag runs with when the configuration does not state one, so a
* form shows what an unset flag means instead of showing it as off.
*/
export function defaultFlagValue(field: keyof NetworkConfig): boolean {
const flag = annotatedFlags.find((flag) => flag.field === field)
return flag ? declaredValue(flag) : false
}
/** The flags the management API carries, so a form's value reaches the backend. */
export function apiCarries(field: string): boolean {
return NetworkConfigPb.fields.some((declared) => declared.name === field)
}
/**
* The flags a config form offers as checkboxes, in schema order: every boolean
* flag the message declares that the management API carries, less the deprecated ones.
*/
export function formFlags(): { field: keyof NetworkConfig; help: string }[] {
return annotatedFlags
.filter((flag) => flag.boolean && !flag.meta.deprecated && apiCarries(flag.field))
.map((flag) => ({ field: flag.field as keyof NetworkConfig, help: `${flag.field}_help` }))
}
/** The value the schema declares for every boolean flag, by form field. */
function newFlagValues(): Record<string, boolean> {
const values: Record<string, boolean> = {}
for (const flag of annotatedFlags) {
if (flag.boolean) values[flag.field] = declaredValue(flag)
}
return values
}
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
return {
...NetworkConfigPb.create(),
@@ -113,32 +178,10 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
'udp://0.0.0.0:11010',
'wg://0.0.0.0:11011',
],
latency_first: false,
...newFlagValues(),
dev_name: '',
use_smoltcp: false,
disable_ipv6: false,
ipv6_public_addr_auto: false,
enable_kcp_proxy: false,
disable_kcp_input: false,
enable_quic_proxy: false,
disable_quic_input: false,
disable_p2p: false,
p2p_only: false,
lazy_p2p: false,
bind_device: true,
no_tun: false,
enable_exit_node: false,
relay_all_peer_rpc: false,
need_p2p: false,
multi_thread: true,
proxy_forward_by_system: false,
disable_encryption: false,
disable_tcp_hole_punching: false,
disable_udp_hole_punching: false,
disable_upnp: false,
enable_udp_broadcast_relay: false,
disable_sym_hole_punching: false,
enable_relay_network_whitelist: false,
relay_network_whitelist: [],
enable_manual_routes: false,
@@ -149,8 +192,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
mtu: null,
instance_recv_bps_limit: null,
mapped_listeners: [],
enable_magic_dns: false,
enable_private_mode: false,
port_forwards: [],
acl: emptyAcl(),
}
@@ -2,6 +2,10 @@ import { mount, type VueWrapper } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { defineComponent, h, nextTick, reactive } from 'vue'
import Config from '../src/components/Config.vue'
import { ScalarType } from '@protobuf-ts/runtime'
import type { FlagMeta } from '../src/generated/proto/annotations'
import { NetworkConfig as NetworkConfigPb } from '../src/generated/proto/api_manage'
import { Flags as FlagsPb } from '../src/generated/proto/common'
import {
DEFAULT_NETWORK_CONFIG,
normalizeNetworkConfig,
@@ -9,33 +13,27 @@ import {
type NetworkConfig,
} from '../src/types/network'
/** The flags the management API carries, so a form's value reaches the backend. */
function apiCarries(field: string): boolean {
return NetworkConfigPb.fields.some((declared) => declared.name === field)
}
/**
* The checkboxes the form is expected to offer, read off the schema the way a
* config form reads it: every boolean flag it declares that the management API
* carries, less the deprecated ones, plus the one network option the form adds itself.
*/
const CONFIG_FLAG_FIELDS = [
'latency_first',
'use_smoltcp',
'disable_ipv6',
...FlagsPb.fields.flatMap((flag) => {
const meta = flag.options?.['easytier.flag'] as FlagMeta | undefined
if (meta?.deprecated) return []
if (flag.kind !== 'scalar' || flag.T !== ScalarType.BOOL) return []
const field = (meta.api?.field ?? flag.name) as keyof NetworkConfig
if (!apiCarries(field)) return []
return [field]
}),
'ipv6_public_addr_auto',
'enable_kcp_proxy',
'disable_kcp_input',
'enable_quic_proxy',
'disable_quic_input',
'disable_p2p',
'p2p_only',
'lazy_p2p',
'bind_device',
'no_tun',
'enable_exit_node',
'relay_all_peer_rpc',
'need_p2p',
'multi_thread',
'proxy_forward_by_system',
'disable_encryption',
'disable_tcp_hole_punching',
'disable_udp_hole_punching',
'enable_udp_broadcast_relay',
'disable_upnp',
'disable_sym_hole_punching',
'enable_magic_dns',
'enable_private_mode',
] as const satisfies readonly (keyof NetworkConfig)[]
const CONFIG_CHECKBOX_FIELDS = [
@@ -532,8 +530,28 @@ describe('Config.vue network config projection', () => {
})
})
it('shows a flag the configuration does not state as the value the core resolves', async () => {
const config = makeConfig()
delete config.bind_device
delete config.multi_thread
const { curNetwork, wrapper } = mountConfig(config)
await nextTick()
// Both default to on, so an unset flag must not read as off.
expect(input(wrapper, '#bind_device').checked).toBe(true)
expect(input(wrapper, '#multi_thread').checked).toBe(true)
// Toggling states the value, which is how a flag gets turned off.
await wrapper.find('#bind_device').setValue(false)
expect(curNetwork.bind_device).toBe(false)
})
it('round-trips every visible boolean config control into backend JSON', async () => {
const config = makeConfig()
// A checkbox for a flag the API has no field for states a value that stops
// at the form; the rest have to arrive at the backend.
const reachesBackend = CONFIG_UI_BOOLEAN_FIELDS.filter(apiCarries)
const originalFlagValues = new Map(
CONFIG_UI_BOOLEAN_FIELDS.map((field, index) => {
const value = index % 2 === 0
@@ -567,6 +585,10 @@ describe('Config.vue network config projection', () => {
for (const [field, value] of originalFlagValues) {
const expectedValue = !value
expect(curNetwork[field], `${field} should update config`).toBe(expectedValue)
if (!reachesBackend.includes(field)) {
expect(backend[field], `${field} has no api.manage.NetworkConfig field`).toBeUndefined()
continue
}
expect(backend[field], `${field} should be preserved in backend JSON`).toBe(expectedValue)
}
})
@@ -8,6 +8,7 @@
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
/* Bundler mode */
"moduleResolution": "Bundler",
@@ -20,10 +20,11 @@ use easytier::{
RunNetworkInstanceRequest,
},
},
common::{CompressionAlgoPb, Ipv4Inet as RpcIpv4Inet},
common::{CompressionAlgoPb, FlagsPatch, Ipv4Inet as RpcIpv4Inet},
rpc_types::controller::BaseController,
},
};
use optionize::Retain;
use super::session::{SessionConfigClient, SessionRpcClient};
@@ -53,7 +54,11 @@ fn instance_identifier(inst_id: &str) -> anyhow::Result<InstanceIdentifier> {
fn hot_patch_base(config: &NetworkConfig) -> anyhow::Result<NetworkConfig> {
let data_compress_algo = normalized_data_compress_algo(config.data_compress_algo);
let encryption_algorithm = normalized_encryption_algorithm(config.encryption_algorithm.clone());
let mut config = NetworkConfig::new_from_config(config.gen_config()?)?;
let config = config.gen_config()?;
// Runtime comparison intentionally resolves defaults. Configuration exports
// otherwise preserve whether each flag was supplied by the user.
config.set_flags(config.get_flags());
let mut config = NetworkConfig::new_from_config(config)?;
let is_credential_mode = config.network_secret.is_none()
&& config
.secure_mode
@@ -208,14 +213,6 @@ fn normalized_proxy_networks(config: &NetworkConfig) -> anyhow::Result<Vec<Runti
.collect())
}
fn normalized_disable_relay_data(config: &NetworkConfig) -> anyhow::Result<bool> {
Ok(config.gen_config()?.get_flags().disable_relay_data)
}
fn normalized_prefer_peer_relay(config: &NetworkConfig) -> anyhow::Result<bool> {
Ok(config.gen_config()?.get_flags().prefer_peer_relay)
}
fn normalized_vpn_portal(config: &NetworkConfig) -> anyhow::Result<Option<RuntimeVpnPortalConfig>> {
Ok(config.gen_config()?.get_vpn_portal_config())
}
@@ -335,17 +332,16 @@ fn web_source_runtime_patch(
diff_proxy_networks(&current_proxy_networks, &desired_proxy_networks)?;
}
let current_disable_relay_data = normalized_disable_relay_data(current)?;
let desired_disable_relay_data = normalized_disable_relay_data(desired)?;
if current_disable_relay_data != desired_disable_relay_data {
patch.disable_relay_data = Some(desired_disable_relay_data);
}
let current_prefer_peer_relay = normalized_prefer_peer_relay(current)?;
let desired_prefer_peer_relay = normalized_prefer_peer_relay(desired)?;
if current_prefer_peer_relay != desired_prefer_peer_relay {
patch.prefer_peer_relay = Some(desired_prefer_peer_relay);
}
let current_flags = current.gen_config()?.get_flags();
let desired_flags = desired.gen_config()?.get_flags();
let mut flags = FlagsPatch {
disable_relay_data: Some(desired_flags.disable_relay_data),
prefer_peer_relay: Some(desired_flags.prefer_peer_relay),
..Default::default()
};
flags.retain(&current_flags);
patch.disable_relay_data = flags.disable_relay_data;
patch.prefer_peer_relay = flags.prefer_peer_relay;
match (
normalized_vpn_portal(current)?,
+1
View File
@@ -136,6 +136,7 @@ once_cell.workspace = true
# for rpc
prost.workspace = true
prost-types.workspace = true
anyhow.workspace = true
url = { workspace = true, features = ["serde"] }
+8 -2
View File
@@ -207,8 +207,8 @@ core_clap:
en: "the url of the ipv6 listener, e.g.: tcp://[::]:11010, if not set, will listen on random udp port"
zh-CN: "IPv6 监听器的URL,例如:tcp://[::]:11010,如果未设置,将在随机UDP端口上监听"
compression:
en: "compression algorithm to use, support none, zstd. default is none"
zh-CN: "要使用的压缩算法,支持 none、zstd。默认为 none"
en: "compression algorithm to use, support None, Zstd. default is None"
zh-CN: "要使用的压缩算法,支持 None、Zstd。默认为 None"
mapped_listeners:
en: "manually specify the public address of the listener, other nodes can use this address to connect to this node. e.g.: tcp://123.123.123.123:11223, can specify multiple."
zh-CN: "手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。例如:tcp://123.123.123.123:11223,可以指定多个。"
@@ -218,6 +218,12 @@ core_clap:
socket_mark:
en: "Linux only: set SO_MARK (fwmark) on EasyTier's underlay sockets (TCP, UDP, QUIC, WebSocket, WireGuard, and the FakeTCP decoy socket) so the host can policy-route or filter them with 'ip rule fwmark ...', nftables ('meta mark'), or iptables ('-m mark'). Any value is applied verbatim (0 is a valid mark); omit the flag to leave SO_MARK untouched. Requires CAP_NET_ADMIN. Note: FakeTCP payload travels via raw TUN writes which the kernel does not tag — mark those separately on the TUN device if needed."
zh-CN: "仅 Linux: 在 EasyTier 的底层套接字 (TCP、UDP、QUIC、WebSocket、WireGuard 以及 FakeTCP 诱饵套接字) 上设置 SO_MARK (fwmark),使主机能用 'ip rule fwmark ...'、nftables ('meta mark') 或 iptables ('-m mark') 策略路由/过滤这些数据包。任何值都会原样应用 (0 也是合法的 mark);不传该参数即保持 SO_MARK 不变。需要 CAP_NET_ADMIN 权限。注意:FakeTCP 的实际载荷通过原始 TUN 写入,内核不会为其打标记;如有需要请在 TUN 设备上单独打标记。"
disable_relay_data:
en: "if true, do not relay data packets for other peers, so this node carries no traffic it did not originate. default is false"
zh-CN: "如果为true,则不转发其他对等节点的数据包,本节点不承载自身以外的流量。默认值为false"
prefer_peer_relay:
en: "prefer a credential peer that already relays to a destination over advertising another direct edge to the same destination"
zh-CN: "优先通过已经转发到目标节点的凭证节点转发,而不是为同一目标再通告一条直连路径"
enable_kcp_proxy:
en: "proxy tcp streams with kcp, improving the latency and throughput on the network with udp packet loss."
zh-CN: "使用 KCP 代理 TCP 流,提高在 UDP 丢包网络上的延迟和吞吐量。"
-6
View File
@@ -30,12 +30,6 @@ pub fn parse_mapped_listener_urls(
.parse_urls(mapped_listeners)
}
pub fn parse_encryption_algorithm(value: &str) -> Result<EncryptionAlgorithm, String> {
value
.parse()
.map_err(|_| format!("'{value}' is not a valid encryption algorithm"))
}
pub fn load_toml_config_from_path(path: &PathBuf) -> Result<TomlConfigLoader, anyhow::Error> {
let config = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config file: {}", path.display()))?;
+260 -440
View File
@@ -2,9 +2,9 @@ use crate::{
ShellType,
common::{
config::{
ConfigFileControl, ConfigLoader, ConsoleLoggerConfig, EncryptionAlgorithm,
FileLoggerConfig, LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig,
TomlConfigLoader, VpnPortalClientConfig, VpnPortalConfig, add_proxy_network_to_config,
ConfigFileControl, ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig,
LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader,
VpnPortalClientConfig, VpnPortalConfig, add_proxy_network_to_config,
load_config_from_file, load_toml_config_from_path,
load_toml_config_from_str_with_source, parse_mapped_listener_urls,
},
@@ -12,17 +12,20 @@ use crate::{
log,
},
instance::factory::native_cli_instance_manager,
proto::common::{CompressionAlgoPb, SecureModeConfig},
proto::common::{CompressionAlgoPb, Flags, FlagsPatch, SecureModeConfig},
rpc_service::ApiRpcServer,
utils::panic::setup_panic_handler,
web_client,
};
use anyhow::Context;
use cidr::IpCidr;
use clap::{CommandFactory, Parser};
use easytier_core::config::normalize_secure_mode_config;
use clap::{Arg, ArgMatches, CommandFactory, FromArgMatches, Parser};
use easytier_core::config::{EncryptionAlgorithm, normalize_secure_mode_config};
use guarden::defer;
use prost_types::field_descriptor_proto::Type;
use rust_i18n::t;
use serde_json::{Value, json};
use std::str::{FromStr, ParseBoolError};
use std::{
net::{IpAddr, SocketAddr},
path::PathBuf,
@@ -35,6 +38,7 @@ use tokio::io::AsyncReadExt;
use crate::tunnel::IpScheme;
#[cfg(feature = "jemalloc-prof")]
use jemalloc_ctl::{Access as _, AsName as _, epoch, stats};
use serde::Serialize;
#[cfg(target_os = "windows")]
windows_service::define_windows_service!(ffi_service_main, win_service_main);
@@ -149,6 +153,10 @@ struct Cli {
#[derive(Parser, Debug, Default, PartialEq, Eq)]
struct NetworkOptions {
/// Filled in from the flag arguments the schema implies.
#[arg(skip)]
flags: FlagsPatch,
#[arg(
long,
env = "ET_NETWORK_NAME",
@@ -312,79 +320,6 @@ struct NetworkOptions {
)]
vpn_portal_client_groups: Vec<String>,
#[arg(
long,
env = "ET_DEFAULT_PROTOCOL",
help = t!("core_clap.default_protocol").to_string()
)]
default_protocol: Option<String>,
#[arg(
short = 'u',
long,
env = "ET_DISABLE_ENCRYPTION",
help = t!("core_clap.disable_encryption").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_encryption: Option<bool>,
#[arg(
long,
env = "ET_ENCRYPTION_ALGORITHM",
help = t!("core_clap.encryption_algorithm").to_string(),
value_parser = crate::common::config::parse_encryption_algorithm,
)]
encryption_algorithm: Option<EncryptionAlgorithm>,
#[arg(
long,
env = "ET_MULTI_THREAD",
help = t!("core_clap.multi_thread").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
multi_thread: Option<bool>,
#[arg(
long,
env = "ET_MULTI_THREAD_COUNT",
help = t!("core_clap.multi_thread_count").to_string(),
)]
multi_thread_count: Option<u32>,
#[arg(
long,
env = "ET_DISABLE_IPV6",
help = t!("core_clap.disable_ipv6").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_ipv6: Option<bool>,
#[arg(
long,
env = "ET_DEV_NAME",
help = t!("core_clap.dev_name").to_string()
)]
dev_name: Option<String>,
#[arg(
long,
env = "ET_MTU",
help = t!("core_clap.mtu").to_string()
)]
mtu: Option<u16>,
#[arg(
long,
env = "ET_LATENCY_FIRST",
help = t!("core_clap.latency_first").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
latency_first: Option<bool>,
#[arg(
long,
env = "ET_EXIT_NODES",
@@ -394,42 +329,6 @@ struct NetworkOptions {
)]
exit_nodes: Vec<IpAddr>,
#[arg(
long,
env = "ET_ENABLE_EXIT_NODE",
help = t!("core_clap.enable_exit_node").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
enable_exit_node: Option<bool>,
#[arg(
long,
env = "ET_PROXY_FORWARD_BY_SYSTEM",
help = t!("core_clap.proxy_forward_by_system").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
proxy_forward_by_system: Option<bool>,
#[arg(
long,
env = "ET_NO_TUN",
help = t!("core_clap.no_tun").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
no_tun: Option<bool>,
#[arg(
long,
env = "ET_USE_SMOLTCP",
help = t!("core_clap.use_smoltcp").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
use_smoltcp: Option<bool>,
#[arg(
long,
env = "ET_MANUAL_ROUTES",
@@ -439,108 +338,6 @@ struct NetworkOptions {
)]
manual_routes: Option<Vec<String>>,
// if not in relay_network_whitelist:
// for foreign virtual network, will refuse the incoming connection
// for local virtual network, will refuse to relay tun packets
#[arg(
long,
env = "ET_RELAY_NETWORK_WHITELIST",
value_delimiter = ',',
help = t!("core_clap.relay_network_whitelist").to_string(),
num_args = 0..
)]
relay_network_whitelist: Option<Vec<String>>,
#[arg(
long,
env = "ET_P2P_ONLY",
help = t!("core_clap.p2p_only").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
p2p_only: Option<bool>,
#[arg(
long,
env = "ET_LAZY_P2P",
help = t!("core_clap.lazy_p2p").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
lazy_p2p: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_P2P",
help = t!("core_clap.disable_p2p").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_p2p: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_UDP_HOLE_PUNCHING",
help = t!("core_clap.disable_udp_hole_punching").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_udp_hole_punching: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_TCP_HOLE_PUNCHING",
help = t!("core_clap.disable_tcp_hole_punching").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_tcp_hole_punching: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_SYM_HOLE_PUNCHING",
help = t!("core_clap.disable_sym_hole_punching").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_sym_hole_punching: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_UPNP",
help = t!("core_clap.disable_upnp").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_upnp: Option<bool>,
#[arg(
long,
env = "ET_ENABLE_UDP_BROADCAST_RELAY",
help = t!("core_clap.enable_udp_broadcast_relay").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
enable_udp_broadcast_relay: Option<bool>,
#[arg(
long,
env = "ET_RELAY_ALL_PEER_RPC",
help = t!("core_clap.relay_all_peer_rpc").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
relay_all_peer_rpc: Option<bool>,
#[arg(
long,
env = "ET_NEED_P2P",
help = t!("core_clap.need_p2p").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
need_p2p: Option<bool>,
#[cfg(feature = "socks5")]
#[arg(
long,
@@ -549,67 +346,6 @@ struct NetworkOptions {
)]
socks5: Option<u16>,
#[arg(
long,
env = "ET_COMPRESSION",
help = t!("core_clap.compression").to_string(),
)]
compression: Option<String>,
#[arg(
long,
env = "ET_BIND_DEVICE",
help = t!("core_clap.bind_device").to_string()
)]
bind_device: Option<bool>,
// SO_MARK (fwmark) is a Linux-family kernel feature. Gate the flag out
// entirely on other targets so users on Windows/macOS/BSD don't see a
// `--socket-mark` they can't act on.
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
#[arg(
long,
env = "ET_SOCKET_MARK",
help = t!("core_clap.socket_mark").to_string()
)]
socket_mark: Option<u32>,
#[arg(
long,
env = "ET_ENABLE_KCP_PROXY",
help = t!("core_clap.enable_kcp_proxy").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
enable_kcp_proxy: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_KCP_INPUT",
help = t!("core_clap.disable_kcp_input").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_kcp_input: Option<bool>,
#[arg(
long,
env = "ET_ENABLE_QUIC_PROXY",
help = t!("core_clap.enable_quic_proxy").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
enable_quic_proxy: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_QUIC_INPUT",
help = t!("core_clap.disable_quic_input").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_quic_input: Option<bool>,
#[arg(
long,
env = "ET_PORT_FORWARD",
@@ -619,40 +355,6 @@ struct NetworkOptions {
)]
port_forward: Vec<url::Url>,
#[arg(
long,
env = "ET_ACCEPT_DNS",
help = t!("core_clap.accept_dns").to_string(),
)]
accept_dns: Option<bool>,
#[arg(
long = "tld-dns-zone",
env = "ET_TLD_DNS_ZONE",
help = t!("core_clap.tld_dns_zone").to_string())]
tld_dns_zone: Option<String>,
#[arg(
long,
env = "ET_PRIVATE_MODE",
help = t!("core_clap.private_mode").to_string(),
)]
private_mode: Option<bool>,
#[arg(
long,
env = "ET_FOREIGN_RELAY_BPS_LIMIT",
help = t!("core_clap.foreign_relay_bps_limit").to_string(),
)]
foreign_relay_bps_limit: Option<u64>,
#[arg(
long,
env = "ET_INSTANCE_RECV_BPS_LIMIT",
help = t!("core_clap.instance_recv_bps_limit").to_string(),
)]
instance_recv_bps_limit: Option<u64>,
#[arg(
long,
value_delimiter = ',',
@@ -669,42 +371,6 @@ struct NetworkOptions {
)]
udp_whitelist: Vec<String>,
#[arg(
long,
env = "ET_DISABLE_RELAY_KCP",
help = t!("core_clap.disable_relay_kcp").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_relay_kcp: Option<bool>,
#[arg(
long,
env = "ET_DISABLE_RELAY_QUIC",
help = t!("core_clap.disable_relay_quic").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
disable_relay_quic: Option<bool>,
#[arg(
long,
env = "ET_ENABLE_RELAY_FOREIGN_NETWORK_KCP",
help = t!("core_clap.enable_relay_foreign_network_kcp").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
enable_relay_foreign_network_kcp: Option<bool>,
#[arg(
long,
env = "ET_ENABLE_RELAY_FOREIGN_NETWORK_QUIC",
help = t!("core_clap.enable_relay_foreign_network_quic").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
enable_relay_foreign_network_quic: Option<bool>,
#[arg(
long,
env = "ET_STUN_SERVERS",
@@ -1237,96 +903,12 @@ impl NetworkOptions {
cfg.set_secure_mode(Some(normalize_secure_mode_config(c)?));
}
let mut f = cfg.get_flags();
if let Some(default_protocol) = &self.default_protocol {
f.default_protocol = default_protocol.clone()
};
if let Some(v) = self.disable_encryption {
f.enable_encryption = !v;
}
if let Some(algorithm) = &self.encryption_algorithm {
f.encryption_algorithm = algorithm.to_string();
}
if let Some(v) = self.disable_ipv6 {
f.enable_ipv6 = !v;
}
f.latency_first = self.latency_first.unwrap_or(f.latency_first);
if let Some(dev_name) = &self.dev_name {
f.dev_name = dev_name.clone()
}
if let Some(mtu) = self.mtu {
f.mtu = mtu as u32;
}
f.enable_exit_node = self.enable_exit_node.unwrap_or(f.enable_exit_node);
f.proxy_forward_by_system = self
.proxy_forward_by_system
.unwrap_or(f.proxy_forward_by_system);
f.no_tun = self.no_tun.unwrap_or(f.no_tun) || cfg!(not(feature = "tun"));
f.use_smoltcp = self.use_smoltcp.unwrap_or(f.use_smoltcp);
if let Some(wl) = self.relay_network_whitelist.as_ref() {
f.relay_network_whitelist = wl.join(" ");
}
f.disable_p2p = self.disable_p2p.unwrap_or(f.disable_p2p);
f.p2p_only = self.p2p_only.unwrap_or(f.p2p_only);
f.lazy_p2p = self.lazy_p2p.unwrap_or(f.lazy_p2p);
f.disable_tcp_hole_punching = self
.disable_tcp_hole_punching
.unwrap_or(f.disable_tcp_hole_punching);
f.disable_udp_hole_punching = self
.disable_udp_hole_punching
.unwrap_or(f.disable_udp_hole_punching);
f.relay_all_peer_rpc = self.relay_all_peer_rpc.unwrap_or(f.relay_all_peer_rpc);
f.need_p2p = self.need_p2p.unwrap_or(f.need_p2p);
f.multi_thread = self.multi_thread.unwrap_or(f.multi_thread);
if let Some(compression) = &self.compression {
f.data_compress_algo = match compression.as_str() {
"none" => CompressionAlgoPb::None,
"zstd" => CompressionAlgoPb::Zstd,
_ => panic!(
"unknown compression algorithm: {}, supported: none, zstd",
compression
),
}
.into();
}
f.bind_device = self.bind_device.unwrap_or(f.bind_device);
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
{
f.socket_mark = self.socket_mark.or(f.socket_mark);
}
f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy);
f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input);
f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy);
f.disable_quic_input = self.disable_quic_input.unwrap_or(f.disable_quic_input);
f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns);
f.private_mode = self.private_mode.unwrap_or(f.private_mode);
f.foreign_relay_bps_limit = self
.foreign_relay_bps_limit
.unwrap_or(f.foreign_relay_bps_limit);
f.instance_recv_bps_limit = self
.instance_recv_bps_limit
.unwrap_or(f.instance_recv_bps_limit);
f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count);
f.disable_relay_kcp = self.disable_relay_kcp.unwrap_or(f.disable_relay_kcp);
f.disable_relay_quic = self.disable_relay_quic.unwrap_or(f.disable_relay_quic);
f.enable_relay_foreign_network_kcp = self
.enable_relay_foreign_network_kcp
.unwrap_or(f.enable_relay_foreign_network_kcp);
f.enable_relay_foreign_network_quic = self
.enable_relay_foreign_network_quic
.unwrap_or(f.enable_relay_foreign_network_quic);
f.disable_sym_hole_punching = self
.disable_sym_hole_punching
.unwrap_or(f.disable_sym_hole_punching);
f.disable_upnp = self.disable_upnp.unwrap_or(f.disable_upnp);
f.enable_udp_broadcast_relay = self
.enable_udp_broadcast_relay
.unwrap_or(f.enable_udp_broadcast_relay);
// Configure tld_dns_zone: use provided value if set
if let Some(tld_dns_zone) = &self.tld_dns_zone {
f.tld_dns_zone = tld_dns_zone.clone();
}
cfg.set_flags(f);
cfg.patch_flags(self.flags.clone());
#[cfg(not(feature = "tun"))]
cfg.patch_flags(FlagsPatch {
no_tun: Some(true),
..Default::default()
});
if !self.exit_nodes.is_empty() {
cfg.set_exit_nodes(self.exit_nodes.clone());
@@ -1471,8 +1053,96 @@ fn win_service_event_loop(
});
}
fn flags_from(matches: &ArgMatches) -> Result<FlagsPatch, clap::Error> {
let given = Flags::flags()
.iter()
.filter_map(|field| {
let name = field.name();
let value = if let Ok(Some(values)) = matches.try_get_many::<String>(name) {
json!(values.map(|s| s.as_str()).collect::<Vec<_>>().join(" "))
} else if let Ok(Some(value)) = matches.try_get_one::<Value>(name) {
value.clone()
} else {
return None;
};
Some((name.to_owned(), value))
})
.collect::<serde_json::Map<_, _>>();
serde_json::from_value(Value::Object(given)).map_err(|error| {
clap::Error::raw(
clap::error::ErrorKind::ValueValidation,
format!("{error:#}"),
)
})
}
fn cli_command() -> clap::Command {
fn arg(flag: &str, spelling: &str) -> Arg {
let name = spelling.to_uppercase();
Arg::new(flag.to_owned())
.long(spelling.replace('_', "-"))
.env(format!("ET_{name}"))
.value_name(name)
.help(t!(format!("core_clap.{spelling}")).to_string())
}
fn parsed<T: FromStr + Serialize>(value: &str) -> Result<Value, T::Err> {
value.parse::<T>().map(|value| json!(value))
}
fn negated(value: &str) -> Result<Value, ParseBoolError> {
value.parse::<bool>().map(|value| json!(!value))
}
Cli::command().args(Flags::flags().iter().map(|field| {
let name = field.name();
match name {
// The two the command line states in the negative, which can be given
// bare like any other boolean flag.
"enable_encryption" => arg(name, "disable_encryption")
.short('u')
.num_args(0..=1)
.default_missing_value("true")
.value_parser(negated),
"enable_ipv6" => arg(name, "disable_ipv6")
.num_args(0..=1)
.default_missing_value("true")
.value_parser(negated),
// The command line names the algorithm instead of spelling the field.
// The values are the schema's own, which the patch reads.
"data_compress_algo" => {
arg(name, "compression").value_parser(parsed::<CompressionAlgoPb>)
}
"encryption_algorithm" => arg(name, name).value_parser(parsed::<EncryptionAlgorithm>),
"mtu" => arg(name, name).value_parser(parsed::<u16>),
"relay_network_whitelist" => arg(name, name)
.value_delimiter(',')
.num_args(0..)
.action(clap::ArgAction::Append),
_ => {
let arg = arg(name, name);
match field.r#type() {
// A boolean flag can be given bare, and bare means enabled.
Type::Bool => arg
.num_args(0..=1)
.default_missing_value("true")
.value_parser(parsed::<bool>),
Type::Uint32 => arg.value_parser(parsed::<u32>),
Type::Uint64 => arg.value_parser(parsed::<u64>),
// An enum flag takes the name the schema spells, and the patch's
// own serde rejects anything the enum does not declare.
Type::String | Type::Enum => arg.value_parser(parsed::<String>),
other => panic!("{name}: no command line for {other:?}"),
}
}
}
}))
}
fn parse_cli() -> Cli {
let mut cli = Cli::parse();
let matches = cli_command().get_matches();
let mut cli = Cli::from_arg_matches(&matches).unwrap_or_else(|error| error.exit());
cli.network_options.flags = flags_from(&matches).unwrap_or_else(|error| error.exit());
// for --stun-servers="", we want vec![], but clap will give vec![""], hack for that
if let Some(stun_servers) = &mut cli.network_options.stun_servers {
stun_servers.retain(|s| !s.trim().is_empty());
@@ -1778,7 +1448,7 @@ pub async fn main() -> ExitCode {
let cli = parse_cli();
if let Some(shell) = cli.gen_autocomplete {
let mut cmd = Cli::command();
let mut cmd = cli_command();
if let Some(shell) = shell.to_shell() {
crate::print_completions(shell, &mut cmd, "easytier-core");
} else {
@@ -1841,6 +1511,12 @@ async fn validate_config(cli: &Cli) -> anyhow::Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::common::CompressionAlgoPb;
/// Parses the flags the way the binary does.
fn parse_flags(argv: &[&str]) -> Result<FlagsPatch, clap::Error> {
flags_from(&cli_command().try_get_matches_from(argv)?)
}
#[test]
fn test_parse_listeners() {
@@ -1954,6 +1630,150 @@ enabled = true
assert_eq!(cfg.get_hostname(), "override-host");
}
#[test]
fn test_cli_flags_from_schema_preserves_presence() {
assert_eq!(parse_flags(&["easytier"]).unwrap(), FlagsPatch::default());
let patch = parse_flags(&[
"easytier",
"--disable-encryption",
"false",
"--latency-first",
"false",
"--mtu",
"0",
"--compression",
"Zstd",
"--socket-mark",
"42",
"--disable-relay-data",
])
.unwrap();
assert_eq!(patch.enable_encryption, Some(true));
assert_eq!(patch.latency_first, Some(false));
assert_eq!(patch.mtu, Some(0));
assert_eq!(
patch.data_compress_algo,
Some(CompressionAlgoPb::Zstd as i32)
);
assert_eq!(patch.socket_mark, Some(42));
assert_eq!(patch.disable_relay_data, Some(true));
assert_eq!(patch.enable_ipv6, None);
// A negated flag can be given bare, and bare means the value it states.
let patch = parse_flags(&["easytier", "--disable-ipv6"]).unwrap();
assert_eq!(patch.enable_ipv6, Some(false));
// Compression tests: case-insensitive, accepts None/none/zstd/Zstd/ZSTD, rejects invalid
let patch = parse_flags(&["easytier", "--compression", "none"]).unwrap();
assert_eq!(
patch.data_compress_algo,
Some(CompressionAlgoPb::None as i32)
);
let patch = parse_flags(&["easytier", "--compression", "zstd"]).unwrap();
assert_eq!(
patch.data_compress_algo,
Some(CompressionAlgoPb::Zstd as i32)
);
let patch = parse_flags(&["easytier", "--compression", "ZSTD"]).unwrap();
assert_eq!(
patch.data_compress_algo,
Some(CompressionAlgoPb::Zstd as i32)
);
assert!(parse_flags(&["easytier", "--compression", "invalid"]).is_err());
assert!(parse_flags(&["easytier", "--compression", "lz4"]).is_err());
// Encryption algorithm tests: case-insensitive, aliases, rejects invalid
let patch = parse_flags(&["easytier", "--encryption-algorithm", "aes-gcm"]).unwrap();
assert_eq!(patch.encryption_algorithm, Some("aes-gcm".to_string()));
let patch = parse_flags(&["easytier", "--encryption-algorithm", "AES-GCM"]).unwrap();
assert_eq!(patch.encryption_algorithm, Some("aes-gcm".to_string()));
let patch =
parse_flags(&["easytier", "--encryption-algorithm", "openssl-aes-gcm"]).unwrap();
assert_eq!(patch.encryption_algorithm, Some("aes-gcm".to_string()));
let patch =
parse_flags(&["easytier", "--encryption-algorithm", "chacha20-poly1305"]).unwrap();
assert_eq!(patch.encryption_algorithm, Some("chacha20".to_string()));
let patch = parse_flags(&["easytier", "--encryption-algorithm", "xor"]).unwrap();
assert_eq!(patch.encryption_algorithm, Some("xor".to_string()));
assert!(parse_flags(&["easytier", "--encryption-algorithm", "rot13"]).is_err());
assert!(parse_flags(&["easytier", "--encryption-algorithm", "des"]).is_err());
// Deprecated flag is not accepted.
assert!(parse_flags(&["easytier", "--quic-listen-port", "1234"]).is_err());
// MTU tests: u16 range validation
let patch = parse_flags(&["easytier", "--mtu", "1400"]).unwrap();
assert_eq!(patch.mtu, Some(1400));
let patch = parse_flags(&["easytier", "--mtu", "65535"]).unwrap();
assert_eq!(patch.mtu, Some(65535));
assert!(parse_flags(&["easytier", "--mtu", "65536"]).is_err());
assert!(parse_flags(&["easytier", "--mtu", "70000"]).is_err());
// Relay network whitelist tests: comma, multiple values, bare clearing
let patch = parse_flags(&["easytier", "--relay-network-whitelist", "net1,net2"]).unwrap();
assert_eq!(patch.relay_network_whitelist, Some("net1 net2".to_string()));
let patch =
parse_flags(&["easytier", "--relay-network-whitelist", "net1", "net2"]).unwrap();
assert_eq!(patch.relay_network_whitelist, Some("net1 net2".to_string()));
let patch = parse_flags(&[
"easytier",
"--relay-network-whitelist",
"net1",
"--relay-network-whitelist",
"net2",
])
.unwrap();
assert_eq!(patch.relay_network_whitelist, Some("net1 net2".to_string()));
let patch = parse_flags(&["easytier", "--relay-network-whitelist"]).unwrap();
assert_eq!(patch.relay_network_whitelist, Some("".to_string()));
}
#[test]
fn test_network_options_patch_preserves_omitted_flags_and_applies_zero_values() {
let cfg = TomlConfigLoader::new_from_str(
r#"[flags]
default_protocol = "udp"
latency_first = true
mtu = 1400
socket_mark = 42
"#,
)
.unwrap();
NetworkOptions::default().merge_into(&cfg).unwrap();
assert_eq!(cfg.get_flags().socket_mark, Some(42));
assert_eq!(cfg.get_flags_patch().enable_encryption, None);
assert_eq!(cfg.get_flags_patch().multi_thread, None);
NetworkOptions {
flags: FlagsPatch {
latency_first: Some(false),
mtu: Some(0),
relay_network_whitelist: Some(String::new()),
enable_encryption: Some(false),
data_compress_algo: Some(CompressionAlgoPb::Zstd.into()),
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
socket_mark: Some(0),
..Default::default()
},
..Default::default()
}
.merge_into(&cfg)
.unwrap();
let flags = cfg.get_flags();
assert_eq!(flags.default_protocol, "udp");
assert!(!flags.latency_first);
assert_eq!(flags.mtu, 0);
assert_eq!(flags.relay_network_whitelist, "");
assert!(!flags.enable_encryption);
assert_eq!(flags.data_compress_algo, CompressionAlgoPb::Zstd as i32);
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
assert_eq!(flags.socket_mark, Some(0));
#[cfg(not(feature = "tun"))]
assert!(flags.no_tun);
}
#[test]
fn secure_mode_cli_flag_preserves_config_file_keypair() {
use base64::{Engine as _, prelude::BASE64_STANDARD};