diff --git a/Cargo.lock b/Cargo.lock index 24187a03..ebff0a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 55ed4580..098d9683 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/easytier-contrib/easytier-ohrs/crates/easytier-ohos-features/src/config/services/schema_service.rs b/easytier-contrib/easytier-ohrs/crates/easytier-ohos-features/src/config/services/schema_service.rs index c6874c59..945df2a4 100644 --- a/easytier-contrib/easytier-ohrs/crates/easytier-ohos-features/src/config/services/schema_service.rs +++ b/easytier-contrib/easytier-ohrs/crates/easytier-ohos-features/src/config/services/schema_service.rs @@ -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; + +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 { +fn field_children(field: &FieldDescriptor, path: &mut ExpansionPath) -> Vec { 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 { - 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 { + 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 { 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 { 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(), ) } diff --git a/easytier-core/Cargo.toml b/easytier-core/Cargo.toml index fde05de2..0edba337 100644 --- a/easytier-core/Cargo.toml +++ b/easytier-core/Cargo.toml @@ -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"] } diff --git a/easytier-core/src/config/api.rs b/easytier-core/src/config/api.rs index 47b58b85..c5b574c1 100644 --- a/easytier-core/src/config/api.rs +++ b/easytier-core/src/config/api.rs @@ -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 } diff --git a/easytier-core/src/config/api_input.rs b/easytier-core/src/config/api_input.rs index 62dd96c0..688e9ee0 100644 --- a/easytier-core/src/config/api_input.rs +++ b/easytier-core/src/config/api_input.rs @@ -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::, _>>()?, ); 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()); } } diff --git a/easytier-core/src/config/encryption.rs b/easytier-core/src/config/encryption.rs index f865a03c..bcd5ed96 100644 --- a/easytier-core/src/config/encryption.rs +++ b/easytier-core/src/config/encryption.rs @@ -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 { - 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::*; diff --git a/easytier-core/src/config/mod.rs b/easytier-core/src/config/mod.rs index 4b3e39eb..ce008b73 100644 --- a/easytier-core/src/config/mod.rs +++ b/easytier-core/src/config/mod.rs @@ -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, } +#[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 for pb::ForeignNetworkConfig { impl From 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 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() } } diff --git a/easytier-core/src/config/peers.rs b/easytier-core/src/config/peers.rs index ee40002d..eafc61e1 100644 --- a/easytier-core/src/config/peers.rs +++ b/easytier-core/src/config/peers.rs @@ -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)>, pub peer_group_memberships: Vec, pub acl_group_declarations: Vec, @@ -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(), ) } } diff --git a/easytier-core/src/config/toml.rs b/easytier-core/src/config/toml.rs index 81692860..c0835fea 100644 --- a/easytier-core/src/config/toml.rs +++ b/easytier-core/src/config/toml.rs @@ -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 { - 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; fn set_exit_nodes(&self, nodes: Vec); @@ -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, #[serde(default)] + #[optionize(flatten)] pub allow_relay: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[optionize(flatten)] pub allowed_proxy_cidrs: Vec, + #[optionize(flatten)] pub expiry_unix: i64, #[serde(default = "default_true")] + // The mapped field is `Option` (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, - flags: Option>, - - #[serde(skip)] - flags_struct: Option, + #[serde(default)] + flags: FlagsPatch, acl: Option, @@ -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 { - 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, - ) -> serde_json::Result { - 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 { @@ -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::>(); - let diff_fields = FLAGS_DIFF_FIELDS - .iter() - .copied() - .collect::>(); - - 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::().is_some()) ); } } diff --git a/easytier-core/src/management/full/config_patch.rs b/easytier-core/src/management/full/config_patch.rs index ed261ce6..f57f30a6 100644 --- a/easytier-core/src/management/full/config_patch.rs +++ b/easytier-core/src/management/full/config_patch.rs @@ -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::>(); + .map(|credential| credential.clone().upgrade()) + .collect::, _>>()?; let replacement = credential_manager .validate_managed_credentials(&entries) .map_err(anyhow::Error::msg)?; diff --git a/easytier-core/src/peers/context.rs b/easytier-core/src/peers/context.rs index a259730e..d7b892e5 100644 --- a/easytier-core/src/peers/context.rs +++ b/easytier-core/src/peers/context.rs @@ -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, pub host_routing: HostRoutingPolicy, pub acl: Option, @@ -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) -> PeerRuntimeSnapshotInput { + fn host_snapshot_input(flags: Flags, acl: Option) -> 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>, ) -> 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>, acl: Option<&Acl>, ) -> CorePeerContext { @@ -1679,7 +1679,7 @@ pub(crate) mod tests { secure_mode: None, host_routing: HostRoutingPolicy::default(), }, - FlagsInConfig::default(), + Flags::default(), None, ) } diff --git a/easytier-core/src/peers/foreign_network/mod.rs b/easytier-core/src/peers/foreign_network/mod.rs index f3f790c2..34e703f7 100644 --- a/easytier-core/src/peers/foreign_network/mod.rs +++ b/easytier-core/src/peers/foreign_network/mod.rs @@ -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, relay_data: bool, - mut flags: FlagsInConfig, + mut flags: Flags, ) -> Arc { 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, parent_context: Arc, - foreign_context_default_flags: FlagsInConfig, + foreign_context_default_flags: Flags, relay_data: bool, peer_session_store: Arc, pm_packet_sender: PacketRecvChan, @@ -822,7 +822,7 @@ impl ForeignNetworkManagerData { relay_data: bool, rpc_registrar: Arc, parent_context: Arc, - foreign_context_default_flags: FlagsInConfig, + foreign_context_default_flags: Flags, peer_session_store: Arc, pm_packet_sender: &PacketRecvChan, ) -> Option<(Arc, bool)> { @@ -874,7 +874,7 @@ enum ForeignNetworkManagerState { pub(crate) struct ForeignNetworkManager { rpc_registrar: Arc, parent_context: Arc, - foreign_context_default_flags: FlagsInConfig, + foreign_context_default_flags: Flags, peer_session_store: Arc, packet_sender_to_mgr: PacketRecvChan, @@ -912,7 +912,7 @@ impl ForeignNetworkManager { pub fn new( rpc_registrar: Arc, parent_context: Arc, - foreign_context_default_flags: FlagsInConfig, + foreign_context_default_flags: Flags, peer_session_store: Arc, packet_sender_to_mgr: PacketRecvChan, global_peer_map: Weak, @@ -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); diff --git a/easytier-core/src/peers/peer_manager.rs b/easytier-core/src/peers/peer_manager.rs index e630126f..0ab7b081 100644 --- a/easytier-core/src/peers/peer_manager.rs +++ b/easytier-core/src/peers/peer_manager.rs @@ -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, - foreign_context_default_flags: FlagsInConfig, + foreign_context_default_flags: Flags, foreign_rpc_registrar: Arc, ) -> Self { let stats_manager = core_context.stats_manager(); diff --git a/easytier-core/src/peers/relay_peer_map.rs b/easytier-core/src/peers/relay_peer_map.rs index c6100376..dc835a49 100644 --- a/easytier-core/src/peers/relay_peer_map.rs +++ b/easytier-core/src/peers/relay_peer_map.rs @@ -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() }, diff --git a/easytier-core/src/peers/route/peer_ospf_route.rs b/easytier-core/src/peers/route/peer_ospf_route.rs index 9973c65e..b375c261 100644 --- a/easytier-core/src/peers/route/peer_ospf_route.rs +++ b/easytier-core/src/peers/route/peer_ospf_route.rs @@ -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() }; diff --git a/easytier-core/src/peers/test_support.rs b/easytier-core/src/peers/test_support.rs index 16a4662e..72e2f910 100644 --- a/easytier-core/src/peers/test_support.rs +++ b/easytier-core/src/peers/test_support.rs @@ -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, } @@ -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() } diff --git a/easytier-core/src/peers/whitelist.rs b/easytier-core/src/peers/whitelist.rs index a7cb7e6a..7ce089e5 100644 --- a/easytier-core/src/peers/whitelist.rs +++ b/easytier-core/src/peers/whitelist.rs @@ -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)) { diff --git a/easytier-core/src/tunnel/encrypt/mod.rs b/easytier-core/src/tunnel/encrypt/mod.rs index 804ab818..2e269451 100644 --- a/easytier-core/src/tunnel/encrypt/mod.rs +++ b/easytier-core/src/tunnel/encrypt/mod.rs @@ -277,7 +277,7 @@ fn maybe_offload_aead( pub(crate) fn validate_algorithm(algorithm: &str) -> Result<(), Error> { let parsed = algorithm .parse::() - .map_err(|()| Error::InvalidAlgorithm(algorithm.to_owned()))?; + .map_err(|_| Error::InvalidAlgorithm(algorithm.to_owned()))?; if algorithm_is_available(parsed) { Ok(()) } else { diff --git a/easytier-go/cmd/update-wasm/main.go b/easytier-go/cmd/update-wasm/main.go index 17c00a76..391628b0 100644 --- a/easytier-go/cmd/update-wasm/main.go +++ b/easytier-go/cmd/update-wasm/main.go @@ -19,6 +19,7 @@ const ( ) var managementProtoFiles = []string{ + "annotations.proto", "common.proto", "error.proto", "acl.proto", diff --git a/easytier-go/internal/artifact/easytier_core.wasm b/easytier-go/internal/artifact/easytier_core.wasm index af27a3bd..681b7c7e 100644 Binary files a/easytier-go/internal/artifact/easytier_core.wasm and b/easytier-go/internal/artifact/easytier_core.wasm differ diff --git a/easytier-go/internal/artifact/provenance.go b/easytier-go/internal/artifact/provenance.go index fa3154a8..16a7c5a2 100644 --- a/easytier-go/internal/artifact/provenance.go +++ b/easytier-go/internal/artifact/provenance.go @@ -3,6 +3,6 @@ package artifact const ( - Commit = "599e4eacaa9c9a6f84b8d6439418af9d860f9aa3" - SHA256 = "f5cc76a30d4c6128f7ce77d494dd4e8130aa7e3ce9d5b9ede7604745064bed8d" + Commit = "9a2290f312dbdec6f40e8b13d8a6de5f66e4c570" + SHA256 = "044f9ed79079f449741744d9dd0559d6844e1fff2f1a1994710811904f119986" ) diff --git a/easytier-go/proto/acl/acl.pb.go b/easytier-go/proto/acl/acl.pb.go index af9837b4..d3951429 100644 --- a/easytier-go/proto/acl/acl.pb.go +++ b/easytier-go/proto/acl/acl.pb.go @@ -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 diff --git a/easytier-go/proto/api/config/api_config.pb.go b/easytier-go/proto/api/config/api_config.pb.go index 909bfa07..a3510dab 100644 --- a/easytier-go/proto/api/config/api_config.pb.go +++ b/easytier-go/proto/api/config/api_config.pb.go @@ -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 diff --git a/easytier-go/proto/api/instance/api_instance.pb.go b/easytier-go/proto/api/instance/api_instance.pb.go index e22584ba..fef899ca 100644 --- a/easytier-go/proto/api/instance/api_instance.pb.go +++ b/easytier-go/proto/api/instance/api_instance.pb.go @@ -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 diff --git a/easytier-go/proto/api/manage/api_manage.pb.go b/easytier-go/proto/api/manage/api_manage.pb.go index 2a0d75d7..4d144664 100644 --- a/easytier-go/proto/api/manage/api_manage.pb.go +++ b/easytier-go/proto/api/manage/api_manage.pb.go @@ -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 diff --git a/easytier-go/proto/common/common.pb.go b/easytier-go/proto/common/common.pb.go index 3db9c4f8..81806daa 100644 --- a/easytier-go/proto/common/common.pb.go +++ b/easytier-go/proto/common/common.pb.go @@ -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 diff --git a/easytier-go/proto/easytier/annotations.pb.go b/easytier-go/proto/easytier/annotations.pb.go new file mode 100644 index 00000000..4eb36121 --- /dev/null +++ b/easytier-go/proto/easytier/annotations.pb.go @@ -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 +} diff --git a/easytier-go/proto/error/error.pb.go b/easytier-go/proto/error/error.pb.go index fbaff4b7..373a60c6 100644 --- a/easytier-go/proto/error/error.pb.go +++ b/easytier-go/proto/error/error.pb.go @@ -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 diff --git a/easytier-go/proto/peer_rpc/peer_rpc.pb.go b/easytier-go/proto/peer_rpc/peer_rpc.pb.go index 496897b9..4084e562 100644 --- a/easytier-go/proto/peer_rpc/peer_rpc.pb.go +++ b/easytier-go/proto/peer_rpc/peer_rpc.pb.go @@ -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 diff --git a/easytier-go/proto/provenance.go b/easytier-go/proto/provenance.go index 13ffccfa..c26dbcda 100644 --- a/easytier-go/proto/provenance.go +++ b/easytier-go/proto/provenance.go @@ -3,6 +3,6 @@ package proto const ( - EasyTierCommit = "599e4eacaa9c9a6f84b8d6439418af9d860f9aa3" - SchemaSHA256 = "7de60ee229e6ee2f17e3673cff6f9d9d4d4bece13ed74e46b1999277b364db67" + EasyTierCommit = "9a2290f312dbdec6f40e8b13d8a6de5f66e4c570" + SchemaSHA256 = "c145983fee99e08f6207c425cea0952c1aaf50f9bfea12c834d1ec985e0b39df" ) diff --git a/easytier-go/proto/web/web.pb.go b/easytier-go/proto/web/web.pb.go index ece7eb55..4b12bbfa 100644 --- a/easytier-go/proto/web/web.pb.go +++ b/easytier-go/proto/web/web.pb.go @@ -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 diff --git a/easytier-go/script/generate-proto.sh b/easytier-go/script/generate-proto.sh index 21af3eb9..c98ca74f 100755 --- a/easytier-go/script/generate-proto.sh +++ b/easytier-go/script/generate-proto.sh @@ -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" \ diff --git a/easytier-proto/Cargo.toml b/easytier-proto/Cargo.toml index 7f080828..3b53209c 100644 --- a/easytier-proto/Cargo.toml +++ b/easytier-proto/Cargo.toml @@ -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", ] diff --git a/easytier-proto/build/flags.rs b/easytier-proto/build/flags.rs new file mode 100644 index 00000000..b263bc23 --- /dev/null +++ b/easytier-proto/build/flags.rs @@ -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 { + 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 = 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> { + 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)) + ) +} diff --git a/easytier-proto/build/main.rs b/easytier-proto/build/main.rs index 2dcd1ec0..b7218fdd 100644 --- a/easytier-proto/build/main.rs +++ b/easytier-proto/build/main.rs @@ -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> { +fn main() -> anyhow::Result<()> { #[cfg(target_os = "windows")] ensure_protoc_for_windows(); @@ -106,7 +107,6 @@ fn main() -> Result<(), Box> { } 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> { .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> { ".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::>(); + 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(&["."])?; diff --git a/easytier-proto/proto/annotations.proto b/easytier-proto/proto/annotations.proto new file mode 100644 index 00000000..e4afcc59 --- /dev/null +++ b/easytier-proto/proto/annotations.proto @@ -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; +} diff --git a/easytier-proto/proto/common.proto b/easytier-proto/proto/common.proto index 1ce83183..90ffa119 100644 --- a/easytier-proto/proto/common.proto +++ b/easytier-proto/proto/common.proto @@ -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; diff --git a/easytier-proto/src/common.rs b/easytier-proto/src/common.rs index c2ad4d80..1db0d3e2 100644 --- a/easytier-proto/src/common.rs +++ b/easytier-proto/src/common.rs @@ -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> = 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 = 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; } diff --git a/easytier-web/Cargo.toml b/easytier-web/Cargo.toml index 14231dc3..5c6d97f5 100644 --- a/easytier-web/Cargo.toml +++ b/easytier-web/Cargo.toml @@ -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 diff --git a/easytier-web/frontend-lib/scripts/codegen-proto.mjs b/easytier-web/frontend-lib/scripts/codegen-proto.mjs index a5a446cc..cce8ebce 100644 --- a/easytier-web/frontend-lib/scripts/codegen-proto.mjs +++ b/easytier-web/frontend-lib/scripts/codegen-proto.mjs @@ -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', diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index 44f513cf..60bb960b 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -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) {
- +
diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index b25ad7ee..dfbf0dbd 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -800,3 +800,8 @@ acl: name: 名称 type: 类型 match: 匹配 + +disable_relay_data: 不转发数据 +disable_relay_data_help: 不转发其他节点的数据包,本节点不承载自身以外的流量。 +prefer_peer_relay: 优先凭证节点转发 +prefer_peer_relay_help: 优先通过已经转发到目标节点的凭证节点转发,而不是为同一目标再通告一条直连路径。 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index d95d5237..f28178ae 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -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. diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts index a0d9d346..b6b26422 100644 --- a/easytier-web/frontend-lib/src/types/network.ts +++ b/easytier-web/frontend-lib/src/types/network.ts @@ -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 { + const values: Record = {} + 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(), } diff --git a/easytier-web/frontend-lib/tests/config-ui.spec.ts b/easytier-web/frontend-lib/tests/config-ui.spec.ts index 106a8bc1..7318af4f 100644 --- a/easytier-web/frontend-lib/tests/config-ui.spec.ts +++ b/easytier-web/frontend-lib/tests/config-ui.spec.ts @@ -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) } }) diff --git a/easytier-web/frontend-lib/tsconfig.app.json b/easytier-web/frontend-lib/tsconfig.app.json index 8a954a8f..04d02364 100644 --- a/easytier-web/frontend-lib/tsconfig.app.json +++ b/easytier-web/frontend-lib/tsconfig.app.json @@ -8,6 +8,7 @@ "skipLibCheck": true, "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, /* Bundler mode */ "moduleResolution": "Bundler", diff --git a/easytier-web/src/client_manager/runtime_reconcile.rs b/easytier-web/src/client_manager/runtime_reconcile.rs index 582f00c2..1989db63 100644 --- a/easytier-web/src/client_manager/runtime_reconcile.rs +++ b/easytier-web/src/client_manager/runtime_reconcile.rs @@ -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 { fn hot_patch_base(config: &NetworkConfig) -> anyhow::Result { 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 anyhow::Result { - Ok(config.gen_config()?.get_flags().disable_relay_data) -} - -fn normalized_prefer_peer_relay(config: &NetworkConfig) -> anyhow::Result { - Ok(config.gen_config()?.get_flags().prefer_peer_relay) -} - fn normalized_vpn_portal(config: &NetworkConfig) -> anyhow::Result> { Ok(config.gen_config()?.get_vpn_portal_config()) } @@ -335,17 +332,16 @@ fn web_source_runtime_patch( diff_proxy_networks(¤t_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(¤t_flags); + patch.disable_relay_data = flags.disable_relay_data; + patch.prefer_peer_relay = flags.prefer_peer_relay; match ( normalized_vpn_portal(current)?, diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index f31c2110..02d8d68f 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -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"] } diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index f9bbf306..50f4a996 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -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 丢包网络上的延迟和吞吐量。" diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 50302660..8a5d50ef 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -30,12 +30,6 @@ pub fn parse_mapped_listener_urls( .parse_urls(mapped_listeners) } -pub fn parse_encryption_algorithm(value: &str) -> Result { - value - .parse() - .map_err(|_| format!("'{value}' is not a valid encryption algorithm")) -} - pub fn load_toml_config_from_path(path: &PathBuf) -> Result { let config = std::fs::read_to_string(path) .with_context(|| format!("failed to read config file: {}", path.display()))?; diff --git a/easytier/src/core.rs b/easytier/src/core.rs index b830f326..70bba2a1 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -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, - #[arg( - long, - env = "ET_DEFAULT_PROTOCOL", - help = t!("core_clap.default_protocol").to_string() - )] - default_protocol: Option, - - #[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, - - #[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, - - #[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, - - #[arg( - long, - env = "ET_MULTI_THREAD_COUNT", - help = t!("core_clap.multi_thread_count").to_string(), - )] - multi_thread_count: Option, - - #[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, - - #[arg( - long, - env = "ET_DEV_NAME", - help = t!("core_clap.dev_name").to_string() - )] - dev_name: Option, - - #[arg( - long, - env = "ET_MTU", - help = t!("core_clap.mtu").to_string() - )] - mtu: Option, - - #[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, - #[arg( long, env = "ET_EXIT_NODES", @@ -394,42 +329,6 @@ struct NetworkOptions { )] exit_nodes: Vec, - #[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, - - #[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, - - #[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, - - #[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, - #[arg( long, env = "ET_MANUAL_ROUTES", @@ -439,108 +338,6 @@ struct NetworkOptions { )] manual_routes: Option>, - // 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>, - - #[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, - - #[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, - - #[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, - - #[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, - - #[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, - - #[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, - - #[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, - - #[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, - - #[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, - - #[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, - #[cfg(feature = "socks5")] #[arg( long, @@ -549,67 +346,6 @@ struct NetworkOptions { )] socks5: Option, - #[arg( - long, - env = "ET_COMPRESSION", - help = t!("core_clap.compression").to_string(), - )] - compression: Option, - - #[arg( - long, - env = "ET_BIND_DEVICE", - help = t!("core_clap.bind_device").to_string() - )] - bind_device: Option, - - // 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, - - #[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, - - #[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, - - #[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, - - #[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, - #[arg( long, env = "ET_PORT_FORWARD", @@ -619,40 +355,6 @@ struct NetworkOptions { )] port_forward: Vec, - #[arg( - long, - env = "ET_ACCEPT_DNS", - help = t!("core_clap.accept_dns").to_string(), - )] - accept_dns: Option, - - #[arg( - long = "tld-dns-zone", - env = "ET_TLD_DNS_ZONE", - help = t!("core_clap.tld_dns_zone").to_string())] - tld_dns_zone: Option, - - #[arg( - long, - env = "ET_PRIVATE_MODE", - help = t!("core_clap.private_mode").to_string(), - )] - private_mode: Option, - - #[arg( - long, - env = "ET_FOREIGN_RELAY_BPS_LIMIT", - help = t!("core_clap.foreign_relay_bps_limit").to_string(), - )] - foreign_relay_bps_limit: Option, - - #[arg( - long, - env = "ET_INSTANCE_RECV_BPS_LIMIT", - help = t!("core_clap.instance_recv_bps_limit").to_string(), - )] - instance_recv_bps_limit: Option, - #[arg( long, value_delimiter = ',', @@ -669,42 +371,6 @@ struct NetworkOptions { )] udp_whitelist: Vec, - #[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, - - #[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, - - #[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, - - #[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, - #[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 { + let given = Flags::flags() + .iter() + .filter_map(|field| { + let name = field.name(); + let value = if let Ok(Some(values)) = matches.try_get_many::(name) { + json!(values.map(|s| s.as_str()).collect::>().join(" ")) + } else if let Ok(Some(value)) = matches.try_get_one::(name) { + value.clone() + } else { + return None; + }; + Some((name.to_owned(), value)) + }) + .collect::>(); + 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(value: &str) -> Result { + value.parse::().map(|value| json!(value)) + } + + fn negated(value: &str) -> Result { + value.parse::().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::) + } + "encryption_algorithm" => arg(name, name).value_parser(parsed::), + "mtu" => arg(name, name).value_parser(parsed::), + "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::), + Type::Uint32 => arg.value_parser(parsed::), + Type::Uint64 => arg.value_parser(parsed::), + // 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::), + 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 { + 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};