From 6ca074abae7eb1b91cd0baa32472ba43a23549f4 Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Sat, 18 Apr 2026 20:23:26 +0800 Subject: [PATCH 01/26] =?UTF-8?q?feat(nix):=20=E6=B7=BB=E5=8A=A0=20rustfmt?= =?UTF-8?q?=20=E5=92=8C=20clippy=20=E5=88=B0=20Rust=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E9=93=BE=E6=89=A9=E5=B1=95=20(#2126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flake.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake.nix b/flake.nix index 81650da5..c2e1a3e9 100644 --- a/flake.nix +++ b/flake.nix @@ -41,6 +41,8 @@ extensions = [ "rust-src" "rust-analyzer" + "rustfmt" + "clippy" ] ++ (if builtins.elem "android" features then android.rust.extensions else [ ]); From c49c56612b4ec02cd69f55fc4a5ef87f5f4353e3 Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Sat, 18 Apr 2026 20:23:53 +0800 Subject: [PATCH 02/26] feat(ui): add ACL graphical configuration interface (#1815) --- .../frontend-lib/src/components/Config.vue | 15 +- .../src/components/acl/AclChainEditor.vue | 218 ++++++++++++++++++ .../src/components/acl/AclGroupEditor.vue | 115 +++++++++ .../src/components/acl/AclManager.vue | 150 ++++++++++++ .../src/components/acl/AclRuleDialog.vue | 150 ++++++++++++ easytier-web/frontend-lib/src/locales/cn.yaml | 44 ++++ easytier-web/frontend-lib/src/locales/en.yaml | 44 ++++ .../frontend-lib/src/types/network.ts | 78 +++++++ easytier/src/launcher.rs | 8 + easytier/src/proto/acl.rs | 20 ++ easytier/src/proto/api_manage.proto | 3 +- 11 files changed, 843 insertions(+), 2 deletions(-) create mode 100644 easytier-web/frontend-lib/src/components/acl/AclChainEditor.vue create mode 100644 easytier-web/frontend-lib/src/components/acl/AclGroupEditor.vue create mode 100644 easytier-web/frontend-lib/src/components/acl/AclManager.vue create mode 100644 easytier-web/frontend-lib/src/components/acl/AclRuleDialog.vue diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index d054cc76..745724ee 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -1,7 +1,7 @@ + + diff --git a/easytier-web/frontend-lib/src/components/acl/AclGroupEditor.vue b/easytier-web/frontend-lib/src/components/acl/AclGroupEditor.vue new file mode 100644 index 00000000..bea7ac1b --- /dev/null +++ b/easytier-web/frontend-lib/src/components/acl/AclGroupEditor.vue @@ -0,0 +1,115 @@ + + + diff --git a/easytier-web/frontend-lib/src/components/acl/AclManager.vue b/easytier-web/frontend-lib/src/components/acl/AclManager.vue new file mode 100644 index 00000000..fa1aabbc --- /dev/null +++ b/easytier-web/frontend-lib/src/components/acl/AclManager.vue @@ -0,0 +1,150 @@ + + + diff --git a/easytier-web/frontend-lib/src/components/acl/AclRuleDialog.vue b/easytier-web/frontend-lib/src/components/acl/AclRuleDialog.vue new file mode 100644 index 00000000..e6f08857 --- /dev/null +++ b/easytier-web/frontend-lib/src/components/acl/AclRuleDialog.vue @@ -0,0 +1,150 @@ + + + diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index b9e03753..ef341c50 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -355,6 +355,7 @@ web: delete: 删除 edit: 编辑 refresh: 刷新 + add: 添加 loading: 加载中... error: 错误 success: 成功 @@ -422,3 +423,46 @@ config-server: client: not_running: 无法连接至远程客户端 retry: 重试 + +acl: + title: 访问控制 + help: 访问控制列表,用于限制节点间的通信。 + enabled: 启用 ACL + default_action: 默认动作 + chains: 规则链 + inbound: 入站 + outbound: 出站 + forward: 转发 + rules: 规则 + add_rule: 添加规则 + edit_rule: 编辑规则 + rule: + name: 规则名称 + description: 描述 + enabled: 启用 + protocol: 协议 + action: 动作 + src_ips: 来源 IP + dst_ips: 目的 IP + src_ports: 来源端口 + dst_ports: 目的端口 + rate_limit: 速率限制 (pps) + burst_limit: 爆发限制 + stateful: 状态追踪 + src_groups: 来源组 + dst_groups: 目的组 + groups: 组管理 + group: + declares: 声明组 + members: 加入组 + name: 组名 + secret: 密钥 + help: 在此处定义网络中的组身份,以便在规则中使用。 + any: 任意 + allow: 允许 + drop: 丢弃 + delete_chain_confirm: 确定要删除此规则链及其所有规则吗? + chain: + name: 名称 + type: 类型 + match: 匹配 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index c614d50c..3d8c847a 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -355,6 +355,7 @@ web: delete: Delete edit: Edit refresh: Refresh + add: Add loading: Loading... error: Error success: Success @@ -422,3 +423,46 @@ config-server: client: not_running: Unable to connect to remote client. retry: Retry + +acl: + title: Access Control (ACL) + help: Access control list to restrict communication between nodes. + enabled: Enable ACL + default_action: Default Action + chains: Rule Chains + inbound: Inbound + outbound: Outbound + forward: Forward + rules: Rules + add_rule: Add Rule + edit_rule: Edit Rule + rule: + name: Rule Name + description: Description + enabled: Enabled + protocol: Protocol + action: Action + src_ips: Source IPs + dst_ips: Destination IPs + src_ports: Source Ports + dst_ports: Destination Ports + rate_limit: Rate Limit (pps) + burst_limit: Burst Limit + stateful: Stateful + src_groups: Source Groups + dst_groups: Destination Groups + groups: Groups + group: + declares: Declared Groups + members: Node Memberships + name: Group Name + secret: Group Secret + help: Define group identities in the network to use them in rules. + any: Any + allow: Allow + drop: Drop + delete_chain_confirm: Are you sure you want to delete this rule chain and all its rules? + chain: + name: Name + type: Type + match: Match diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts index 4b3517eb..45247c3e 100644 --- a/easytier-web/frontend-lib/src/types/network.ts +++ b/easytier-web/frontend-lib/src/types/network.ts @@ -14,6 +14,74 @@ export interface SecureModeConfig { local_public_key?: string } +export enum AclProtocol { + Unspecified = 0, + TCP = 1, + UDP = 2, + ICMP = 3, + ICMPv6 = 4, + Any = 5, +} + +export enum AclAction { + Noop = 0, + Allow = 1, + Drop = 2, +} + +export enum AclChainType { + UnspecifiedChain = 0, + Inbound = 1, + Outbound = 2, + Forward = 3, +} + +export interface AclRule { + name: string + description: string + priority: number + enabled: boolean + protocol: AclProtocol + ports: string[] + source_ips: string[] + destination_ips: string[] + source_ports: string[] + action: AclAction + rate_limit: number + burst_limit: number + stateful: boolean + source_groups: string[] + destination_groups: string[] +} + +export interface AclChain { + name: string + chain_type: AclChainType + description: string + enabled: boolean + rules: AclRule[] + default_action: AclAction +} + +export interface GroupIdentity { + group_name: string + group_secret: string +} + +export interface GroupInfo { + declares: GroupIdentity[] + members: string[] +} + +export interface AclV1 { + chains: AclChain[] + group?: GroupInfo +} + +export interface Acl { + acl_v1?: AclV1 +} + export interface NetworkConfig { instance_id: string @@ -85,6 +153,7 @@ export interface NetworkConfig { enable_private_mode?: boolean port_forwards: PortForwardConfig[] + acl?: Acl } export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { @@ -152,6 +221,15 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { enable_magic_dns: false, enable_private_mode: false, port_forwards: [], + acl: { + acl_v1: { + group: { + declares: [], + members: [], + }, + chains: [], + }, + }, } } diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index a5dc001f..3ae57af4 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -825,6 +825,12 @@ impl NetworkConfig { 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; @@ -964,6 +970,8 @@ impl NetworkConfig { (flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit); result.enable_private_mode = Some(flags.private_mode); + result.acl = config.get_acl(); + if flags.relay_network_whitelist == "*" { result.enable_relay_network_whitelist = Some(false); } else { diff --git a/easytier/src/proto/acl.rs b/easytier/src/proto/acl.rs index 6948ba3f..03d11866 100644 --- a/easytier/src/proto/acl.rs +++ b/easytier/src/proto/acl.rs @@ -2,6 +2,26 @@ use std::fmt::Display; include!(concat!(env!("OUT_DIR"), "/acl.rs")); +impl Acl { + pub fn is_empty(&self) -> bool { + self.acl_v1.as_ref().map(|v1| v1.is_empty()).unwrap_or(true) + } +} + +impl AclV1 { + pub fn is_empty(&self) -> bool { + let has_chains = !self.chains.is_empty(); + let has_groups = self.group.as_ref().map(|g| !g.is_empty()).unwrap_or(false); + !has_chains && !has_groups + } +} + +impl GroupInfo { + pub fn is_empty(&self) -> bool { + self.declares.is_empty() && self.members.is_empty() + } +} + impl Display for ConnTrackEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let src = self diff --git a/easytier/src/proto/api_manage.proto b/easytier/src/proto/api_manage.proto index b79ff5d7..0641672f 100644 --- a/easytier/src/proto/api_manage.proto +++ b/easytier/src/proto/api_manage.proto @@ -3,6 +3,7 @@ syntax = "proto3"; import "common.proto"; import "peer_rpc.proto"; import "api_instance.proto"; +import "acl.proto"; package api.manage; @@ -83,7 +84,7 @@ message NetworkConfig { optional bool disable_tcp_hole_punching = 54; common.SecureModeConfig secure_mode = 55; - reserved 56; + optional acl.Acl acl = 56; optional string credential_file = 57; optional bool lazy_p2p = 58; optional bool need_p2p = 59; From 2db655bd6d8482fa94529aa9d88357e1f4149a56 Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Sun, 19 Apr 2026 10:37:39 +0800 Subject: [PATCH 03/26] fix: refresh ACL groups and enable TCP_NODELAY for WebSocket (#2118) * fix: refresh ACL groups and enable TCP_NODELAY for WebSocket * add remove_peers to remove list of peer id in ospf route * fix secure tunnel for unreliable udp tunnel * fix(web-client): timeout secure tunnel handshake * fix(web-server): tolerate delayed secure hello * fix quic endpoint panic * fix replay check --- Cargo.lock | 5890 +++++++++++++++++++- easytier-web/src/client_manager/mod.rs | 21 +- easytier-web/src/client_manager/session.rs | 2 +- easytier/src/gateway/quic_proxy.rs | 5 + easytier/src/instance/instance.rs | 4 + easytier/src/peers/mod.rs | 1 + easytier/src/peers/peer_ospf_route.rs | 734 ++- easytier/src/peers/peer_session.rs | 834 +-- easytier/src/peers/route_trait.rs | 2 + easytier/src/peers/secure_datagram.rs | 1020 ++++ easytier/src/peers/tests.rs | 103 +- easytier/src/tunnel/websocket.rs | 3 + easytier/src/web_client/mod.rs | 19 +- easytier/src/web_client/security.rs | 224 +- 14 files changed, 7824 insertions(+), 1038 deletions(-) create mode 100644 easytier/src/peers/secure_datagram.rs diff --git a/Cargo.lock b/Cargo.lock index 07268683..0e9a6104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "adler2" version = "2.0.0" @@ -43,6 +49,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -52,6 +69,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -64,6 +102,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" +dependencies = [ + "android_log-sys", + "env_logger", + "log", + "once_cell", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -128,6 +184,15 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.1" @@ -137,18 +202,149 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image 0.25.10", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.7", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.7", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -170,6 +366,24 @@ dependencies = [ "ringbuf", ] +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.7", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -192,6 +406,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -203,6 +423,38 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-shim" version = "0.2.0" @@ -248,24 +500,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", + "axum-macros 0.4.2", "bytes", "futures-util", "http", "http-body", "http-body-util", + "hyper", + "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", "pin-project-lite", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", + "tokio", "tower 0.5.2", "tower-layer", "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core 0.5.6", + "axum-macros 0.5.1", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", ] [[package]] @@ -286,8 +580,132 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-embed" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077959a7f8cf438676af90b483304528eb7e16eadadb7f44e9ada4f9dceb9e62" +dependencies = [ + "axum-core 0.4.5", + "chrono", + "http", + "mime_guess", + "rust-embed", + "tower-service", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum 0.8.9", + "axum-core 0.5.6", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "serde_html_form", + "serde_path_to_error", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-login" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5260ed0ecc8ace8e7e61a7406672faba598c8a86b8f4742fcdde0ddc979a318f" +dependencies = [ + "async-trait", + "axum 0.7.7", + "form_urlencoded", + "serde", + "subtle", + "thiserror 1.0.63", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions", + "tracing", + "urlencoding", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "axum-messages" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e85c86a8bd84f54833bca296a0204bd865958ade62bacadeae92dda34cfb8a" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "http", + "parking_lot", + "serde", + "serde_json", + "tower 0.4.13", + "tower-sessions-core", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base62" version = "2.0.2" @@ -312,6 +730,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bindgen" version = "0.72.1" @@ -341,6 +779,21 @@ name = "bitflags" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "blake2" @@ -360,6 +813,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bon" version = "3.9.1" @@ -376,7 +851,7 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ - "darling", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -410,6 +885,51 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "1.10.0" @@ -426,6 +946,28 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytecodec" version = "0.4.15" @@ -454,11 +996,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +dependencies = [ + "serde", +] [[package]] name = "bzip2" @@ -499,6 +1050,73 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.8.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.63", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.11", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + [[package]] name = "cc" version = "1.2.10" @@ -522,7 +1140,28 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", ] [[package]] @@ -604,7 +1243,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.5", ] [[package]] @@ -669,6 +1308,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "codepage" version = "0.1.2" @@ -678,6 +1326,12 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.2" @@ -694,6 +1348,35 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console-api" version = "0.8.1" @@ -733,12 +1416,33 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "conv" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" +dependencies = [ + "custom_derive", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.10.0" @@ -748,6 +1452,22 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "base64 0.22.1", + "hmac", + "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -774,6 +1494,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.0", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.0", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.13" @@ -869,6 +1613,24 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -880,6 +1642,43 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.87", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.87", +] + [[package]] name = "ctor" version = "0.8.0" @@ -914,6 +1713,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", + "digest", "fiat-crypto", "rustc_version", "subtle", @@ -931,14 +1731,30 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "custom_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" + [[package]] name = "darling" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -955,13 +1771,37 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.87", +] + [[package]] name = "darling_macro" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core", + "darling_core 0.20.10", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.87", ] @@ -1056,12 +1896,35 @@ dependencies = [ ] [[package]] -name = "deranged" -version = "0.4.0" +name = "delegate" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -1101,7 +1964,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", "syn 2.0.87", @@ -1117,6 +1980,19 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.87", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -1132,7 +2008,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", @@ -1147,6 +2023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1192,6 +2069,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.8.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1203,6 +2092,71 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "dtor" version = "0.3.0" @@ -1218,6 +2172,18 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "easytier" version = "2.6.0" @@ -1239,7 +2205,6 @@ dependencies = [ "bytecodec", "byteorder", "bytes", - "cfg-if", "cfg_aliases", "chrono", "cidr", @@ -1248,23 +2213,23 @@ dependencies = [ "clap_complete_nushell", "console-subscriber", "crossbeam", - "ctor", + "ctor 0.8.0", "dashmap", "dbus", "defguard_wireguard_rs", + "delegate", "derivative", "derive_builder", - "derive_more", + "derive_more 2.1.1", "easytier-rpc-build", "encoding", - "flume", + "flume 0.12.0", "forwarded-header-value", "futures", "futures-util", - "gethostname", + "gethostname 0.5.0", "git-version", "globwalk", - "hashbrown 0.15.3", "hickory-client", "hickory-proto", "hickory-resolver", @@ -1274,7 +2239,7 @@ dependencies = [ "http_req", "humansize", "humantime-serde", - "idna", + "idna 1.0.3", "indoc", "itertools 0.14.0", "kcp-sys", @@ -1310,7 +2275,7 @@ dependencies = [ "rand 0.8.5", "rcgen", "regex", - "reqwest", + "reqwest 0.12.12", "resolv-conf", "ring", "ringbuf", @@ -1326,7 +2291,7 @@ dependencies = [ "smoltcp", "snow", "socket2 0.5.10", - "strum", + "strum 0.27.2", "stun_codec", "sys-locale", "tabled", @@ -1363,11 +2328,69 @@ dependencies = [ "windows-sys 0.52.0", "winreg 0.52.0", "x25519-dalek", - "zerocopy", + "zerocopy 0.7.35", "zip", "zstd", ] +[[package]] +name = "easytier-android-jni" +version = "0.1.0" +dependencies = [ + "android_logger", + "easytier", + "jni", + "log", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "easytier-ffi" +version = "0.1.0" +dependencies = [ + "dashmap", + "easytier", + "once_cell", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "easytier-gui" +version = "2.6.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "dashmap", + "dunce", + "easytier", + "gethostname 1.1.0", + "libc", + "once_cell", + "security-framework-sys", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-clipboard-manager", + "tauri-plugin-os", + "tauri-plugin-positioner", + "tauri-plugin-process", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-vpnservice", + "thunk-rs", + "tokio", + "url", + "uuid", + "winapi", + "windows 0.52.0", +] + [[package]] name = "easytier-rpc-build" version = "0.1.0" @@ -1376,11 +2399,177 @@ dependencies = [ "prost-build", ] +[[package]] +name = "easytier-uptime" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.8.9", + "axum-extra", + "chrono", + "clap", + "dashmap", + "easytier", + "futures", + "jsonwebtoken", + "mimalloc", + "mockall", + "once_cell", + "parking_lot", + "reqwest 0.12.12", + "sea-orm", + "sea-orm-migration", + "serde", + "serde_json", + "serde_yaml", + "sqlx", + "tempfile", + "thiserror 1.0.63", + "tokio", + "tokio-test", + "tokio-util", + "toml 0.8.19", + "tower 0.5.2", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", + "validator", +] + +[[package]] +name = "easytier-web" +version = "2.6.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.7.7", + "axum-embed", + "axum-login", + "axum-messages", + "base64 0.22.1", + "chrono", + "clap", + "dashmap", + "easytier", + "image 0.24.9", + "imageproc", + "maxminddb", + "mimalloc", + "once_cell", + "openidconnect", + "password-auth", + "rand 0.8.5", + "reqwest 0.12.12", + "rust-embed", + "rust-i18n", + "rusttype", + "sea-orm", + "sea-orm-migration", + "serde", + "serde_json", + "sqlx", + "subtle", + "sys-locale", + "thiserror 1.0.63", + "thunk-rs", + "tokio", + "tower-http", + "tower-sessions", + "tower-sessions-sqlx-store", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embed-resource" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] name = "encoding" @@ -1466,6 +2655,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + [[package]] name = "endian-type" version = "0.1.2" @@ -1484,12 +2679,43 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "env_home" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1516,6 +2742,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "etherparse" version = "0.13.0" @@ -1525,6 +2768,27 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastbloom" version = "0.9.0" @@ -1533,7 +2797,7 @@ checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8" dependencies = [ "getrandom 0.3.2", "rand 0.9.1", - "siphasher", + "siphasher 1.0.1", "wide", ] @@ -1546,12 +2810,61 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1575,6 +2888,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "flume" version = "0.12.0" @@ -1605,7 +2929,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -1614,6 +2959,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1633,6 +2984,31 @@ dependencies = [ "thiserror 1.0.63", ] +[[package]] +name = "fragile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.30" @@ -1675,12 +3051,36 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -1728,6 +3128,114 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + [[package]] name = "generator" version = "0.8.4" @@ -1749,6 +3257,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1761,6 +3270,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.0.7", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1798,6 +3328,38 @@ dependencies = [ "polyval", ] +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.63", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "git-version" version = "0.3.9" @@ -1818,6 +3380,53 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.8.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.63", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.1" @@ -1848,6 +3457,80 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "h2" version = "0.4.7" @@ -1860,13 +3543,24 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.7.1", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy 0.8.48", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1881,6 +3575,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" @@ -1899,6 +3596,21 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.3", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -1908,7 +3620,7 @@ dependencies = [ "base64 0.21.7", "byteorder", "flate2", - "nom", + "nom 7.1.3", "num-traits", ] @@ -1940,6 +3652,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1978,7 +3696,7 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna", + "idna 1.0.3", "ipnet", "once_cell", "rand 0.9.1", @@ -2037,6 +3755,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2055,6 +3782,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + [[package]] name = "http" version = "1.1.0" @@ -2144,13 +3883,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -2178,6 +3918,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 0.26.3", ] [[package]] @@ -2211,18 +3952,22 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -2251,6 +3996,16 @@ dependencies = [ "cc", ] +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -2375,6 +4130,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "1.0.3" @@ -2412,6 +4177,51 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png 0.17.16", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + +[[package]] +name = "imageproc" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aee993351d466301a29655d628bfc6f5a35a0d062b6160ca0808f425805fd7" +dependencies = [ + "approx", + "conv", + "image 0.24.9", + "itertools 0.10.5", + "nalgebra", + "num", + "rand 0.7.3", + "rand_distr", + "rayon", + "rusttype", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2420,16 +4230,19 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] name = "indexmap" -version = "2.7.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -2441,6 +4254,26 @@ dependencies = [ "rustversion", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "inout" version = "0.1.3" @@ -2511,12 +4344,50 @@ dependencies = [ "serde", ] +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.11.0" @@ -2550,6 +4421,29 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "jni" version = "0.21.1" @@ -2583,14 +4477,53 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.63", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kcp-sys" version = "0.1.0" @@ -2610,7 +4543,30 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", - "zerocopy", + "zerocopy 0.7.35", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.8.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.14.0", + "selectors", ] [[package]] @@ -2618,6 +4574,33 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] [[package]] name = "libc" @@ -2635,6 +4618,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.5" @@ -2689,6 +4682,18 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.8.0", "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", ] [[package]] @@ -2732,6 +4737,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2759,6 +4765,23 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "serde", + "winapi", +] + [[package]] name = "machine-uid" version = "0.5.3" @@ -2781,6 +4804,31 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "matchers" version = "0.1.0" @@ -2790,12 +4838,56 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "matchit" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "maxminddb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6087e5d8ea14861bb7c7f573afbc7be3798d3ef0fae87ec4fd9a4de9a127c3c" +dependencies = [ + "ipnetwork", + "log", + "memchr", + "serde", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "md5" version = "0.7.0" @@ -2832,6 +4924,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2845,6 +4947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2853,12 +4956,39 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "moka" version = "0.12.10" @@ -2878,6 +5008,37 @@ dependencies = [ "uuid", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.17.16", + "serde", + "thiserror 2.0.11", + "windows-sys 0.60.2", +] + [[package]] name = "multimap" version = "0.10.1" @@ -2887,6 +5048,21 @@ dependencies = [ "serde", ] +[[package]] +name = "nalgebra" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb2d0de08694bed883320212c18ee3008576bfe8c306f4c3c4a58b4876998be" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -2904,6 +5080,36 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.8.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.63", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -3005,6 +5211,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nibble_vec" version = "0.1.0" @@ -3051,12 +5263,31 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "no-std-net" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -3067,6 +5298,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nonempty" version = "0.7.0" @@ -3093,10 +5333,90 @@ dependencies = [ ] [[package]] -name = "num-conv" -version = "0.1.0" +name = "num" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] [[package]] name = "num-traits" @@ -3105,6 +5425,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -3116,6 +5459,223 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.15", + "http", + "rand 0.8.5", + "reqwest 0.12.12", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.63", + "url", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.8.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.8.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.8.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -3132,6 +5692,49 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.63", + "url", +] + [[package]] name = "openssl" version = "0.10.66" @@ -3140,7 +5743,7 @@ checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.8.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -3192,6 +5795,34 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "ordered_hash_map" version = "0.5.0" @@ -3201,12 +5832,120 @@ dependencies = [ "hashbrown 0.15.3", ] +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix 0.30.1", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.87", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owned_ttf_parser" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e6affeb1632d6ff6a23d2cd40ffed138e82f1532571a26f527c8a284bb2fbb" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "papergrid" version = "0.12.0" @@ -3218,6 +5957,12 @@ dependencies = [ "unicode-width 0.1.11", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -3241,12 +5986,41 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-auth" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524" +dependencies = [ + "argon2", + "getrandom 0.2.15", + "password-hash", + "rand_core 0.6.4", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -3267,6 +6041,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3280,7 +6063,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap 2.7.1", + "indexmap 2.14.0", ] [[package]] @@ -3291,10 +6074,153 @@ checksum = "7a98c6720655620a521dcc722d0ad66cd8afd5d86e34a89ef691c50b7b24de06" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.3", - "indexmap 2.7.1", + "indexmap 2.14.0", "serde", ] +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -3327,6 +6253,38 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -3340,8 +6298,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.7.1", - "quick-xml", + "indexmap 2.14.0", + "quick-xml 0.32.0", "serde", "time", ] @@ -3439,6 +6397,46 @@ dependencies = [ "pnet_sys", ] +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.8.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.0.7", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -3480,7 +6478,39 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", ] [[package]] @@ -3504,6 +6534,34 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -3537,6 +6595,34 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -3546,6 +6632,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "version_check", + "yansi", +] + [[package]] name = "prost" version = "0.13.5" @@ -3677,6 +6776,38 @@ dependencies = [ "serde_json", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.32.0" @@ -3686,6 +6817,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.8" @@ -3769,6 +6909,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "radix_trie" version = "0.2.1" @@ -3779,6 +6925,20 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + [[package]] name = "rand" version = "0.8.5" @@ -3800,6 +6960,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3820,6 +6990,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -3838,6 +7017,65 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "rand_distr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" +dependencies = [ + "rand 0.7.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rcgen" version = "0.12.1" @@ -3881,6 +7119,26 @@ dependencies = [ "thiserror 2.0.11", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "regex" version = "1.12.3" @@ -3931,6 +7189,15 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.12.12" @@ -3959,7 +7226,10 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -3967,21 +7237,67 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tower 0.5.2", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 0.26.3", "windows-registry", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "resolv-conf" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -4005,6 +7321,74 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rstest" version = "0.25.0" @@ -4025,7 +7409,7 @@ checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" dependencies = [ "cfg-if", "glob", - "proc-macro-crate", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", "regex", @@ -4035,6 +7419,41 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.87", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + [[package]] name = "rust-i18n" version = "3.1.2" @@ -4084,11 +7503,28 @@ dependencies = [ "serde", "serde_json", "serde_yml", - "siphasher", + "siphasher 1.0.1", "toml 0.7.8", "triomphe", ] +[[package]] +name = "rust_decimal" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + [[package]] name = "rustc-hash" version = "2.1.0" @@ -4212,6 +7648,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rusttype" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff8374aa04134254b7995b63ad3dc41c7f7236f69528b28553da7d72efaa967" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -4260,6 +7706,57 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.87", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -4278,12 +7775,186 @@ version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0495e4577c672de8254beb68d01a9b62d0e8a13c099edecdbedccce3223cd29f" +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "sea-orm" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more 2.1.1", + "futures-util", + "log", + "mac_address", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum 0.26.3", + "thiserror 2.0.11", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da80ebcdb44571e86f03a2bdcb5532136a87397f366f38bbce64673fc5e6a450" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "regex", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9a3f90e336ec74803e8eb98c61bc98754c1adfba3b4f84d946237b752b1c88" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.87", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c577f2959277e936c1d08109acd1e08fc36a95ef29ec028190ba82cad8f96e" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float 4.6.0", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" +dependencies = [ + "darling 0.20.10", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.87", + "thiserror 2.0.11", +] + +[[package]] +name = "sea-schema" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338" +dependencies = [ + "futures", + "sea-query", + "sea-schema-derive", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -4320,11 +7991,32 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -4336,6 +8028,28 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float 2.10.1", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4357,15 +8071,71 @@ dependencies = [ ] [[package]] -name = "serde_json" -version = "1.0.125" +name = "serde_derive_internals" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap 2.14.0", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -4377,6 +8147,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4389,13 +8168,57 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serde_yml" version = "0.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e76bab63c3fd98d27c17f9cbce177f64a91f5e69ac04cafe04e1bb25d1dc3c" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.14.0", "itoa", "libyml", "log", @@ -4431,6 +8254,28 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "service-manager" version = "0.8.0" @@ -4446,6 +8291,16 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4477,6 +8332,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shellexpand" version = "3.1.1" @@ -4492,6 +8358,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4501,6 +8388,29 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simba" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3fd720c48c53cace224ae62bef1bbff363a70c68c4802a78b5cc6159618176" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -4513,6 +8423,24 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.11", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "siphasher" version = "1.0.1" @@ -4533,6 +8461,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "smoltcp" @@ -4584,6 +8515,54 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "spin" version = "0.9.8" @@ -4593,18 +8572,285 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bigdecimal", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.3", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.11", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.3", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.87", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.87", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bigdecimal", + "bitflags 2.8.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.11", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bigdecimal", + "bitflags 2.8.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.11", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume 0.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.11", + "time", + "tracing", + "url", + "uuid", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "strum" version = "0.27.2" @@ -4647,6 +8893,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "syn" version = "1.0.109" @@ -4729,6 +8986,19 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.19", + "version-compare", +] + [[package]] name = "tabled" version = "0.16.0" @@ -4758,6 +9028,403 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tao" +version = "0.34.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +dependencies = [ + "bitflags 2.8.0", + "block2", + "core-foundation 0.10.0", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.2", + "glob", + "gtk", + "heck 0.5.0", + "http", + "image 0.25.10", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.2", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.11", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.87", + "tauri-utils", + "thiserror 2.0.11", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" +dependencies = [ + "gethostname 1.1.0", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", +] + +[[package]] +name = "tauri-plugin-positioner" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dcd56dd4797bd4d6c4c658daed40ce563176f92df90fbd2c904ce145de17ef" +dependencies = [ + "log", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", + "tokio", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33a5b7d78f0dec4406b003ea87c40bf928d801b6fd9323a556172c91d8712c1" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.11", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-vpnservice" +version = "0.0.0" +dependencies = [ + "serde", + "tauri", + "tauri-plugin", + "thiserror 1.0.63", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.11", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor 0.2.9", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.11", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.12+spec-1.1.0", +] + [[package]] name = "tempfile" version = "3.22.0" @@ -4771,6 +9438,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "terminal_size" version = "0.4.1" @@ -4781,6 +9459,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.63" @@ -4836,6 +9520,20 @@ name = "thunk-rs" version = "0.3.5" source = "git+https://github.com/easytier/thunk.git#cbbeec75a66b7b3cf0824ae890d9d06bcfb9d1f3" +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "tikv-jemalloc-ctl" version = "0.6.0" @@ -4869,9 +9567,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -4879,22 +9577,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -5004,6 +9702,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -5012,8 +9721,12 @@ checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", + "hashbrown 0.14.5", "pin-project-lite", + "slab", "tokio", ] @@ -5046,8 +9759,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.7", + "toml_datetime 0.6.8", "toml_edit 0.19.15", ] @@ -5058,11 +9771,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.7", + "toml_datetime 0.6.8", "toml_edit 0.22.20", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -5072,16 +9800,36 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.14.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.7", + "toml_datetime 0.6.8", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.8", "winnow 0.5.40", ] @@ -5091,13 +9839,28 @@ version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.14.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.7", + "toml_datetime 0.6.8", "winnow 0.6.18", ] +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.1", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tonic" version = "0.12.3" @@ -5106,7 +9869,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.7.7", "base64 0.22.1", "bytes", "h2", @@ -5174,6 +9937,46 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", +] + +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "async-compression", + "bitflags 2.8.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tokio", + "tokio-util", + "tower 0.5.2", + "tower-layer", + "tower-service", ] [[package]] @@ -5188,6 +9991,71 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-sessions" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65856c81ee244e0f8a55ab0f7b769b72fbde387c235f0a73cd97c579818d05eb" +dependencies = [ + "async-trait", + "http", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6abbfcaf6436ec5a772cd9f965401da12db793e404ae6134eac066fa5a04f3" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "base64 0.22.1", + "futures", + "http", + "parking_lot", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror 1.0.63", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fad75660c8afbe74f4e7cbbe8e9090171a056b57370ea4d7d5e9eb3e4af3092" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + +[[package]] +name = "tower-sessions-sqlx-store" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd38eba51214e99accab78f6b7c8e273e90a9cb57575e86b592c60074e182d7" +dependencies = [ + "async-trait", + "rmp-serde", + "sqlx", + "thiserror 1.0.63", + "time", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.41" @@ -5280,6 +10148,39 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.17.16", + "serde", + "thiserror 2.0.11", + "windows-sys 0.60.2", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom 8.0.0", + "petgraph 0.8.1", +] + [[package]] name = "triomphe" version = "0.1.13" @@ -5297,6 +10198,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" + [[package]] name = "tun-easytier" version = "1.1.1" @@ -5307,7 +10214,7 @@ dependencies = [ "futures-core", "ipnet", "libc", - "libloading", + "libloading 0.8.5", "log", "nix 0.29.0", "thiserror 1.0.63", @@ -5353,18 +10260,91 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -5399,6 +10379,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -5412,11 +10398,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna", + "idna 1.0.3", "percent-encoding", "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -5460,6 +10470,36 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna 0.5.0", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" +dependencies = [ + "darling 0.20.10", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "valuable" version = "0.1.0" @@ -5484,6 +10524,26 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -5503,6 +10563,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5519,48 +10585,40 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.87", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", "js-sys", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5568,31 +10626,114 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.87", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] [[package]] -name = "web-sys" -version = "0.3.70" +name = "wasm-streams" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.7", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.8.0", + "rustix 1.0.7", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5608,6 +10749,50 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + [[package]] name = "webpki" version = "0.22.4" @@ -5654,6 +10839,48 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement 0.60.2", + "windows-interface 0.59.3", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.11", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "which" version = "4.4.2" @@ -5678,6 +10905,16 @@ dependencies = [ "winsafe", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "wide" version = "0.7.28" @@ -5752,6 +10989,21 @@ dependencies = [ "windows 0.48.0", ] +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + [[package]] name = "windows" version = "0.48.0" @@ -5781,6 +11033,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -5796,13 +11070,37 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -5814,6 +11112,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -5825,20 +11134,47 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows-result", - "windows-strings", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] @@ -5851,6 +11187,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-service" version = "0.7.0" @@ -5868,10 +11213,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -5923,7 +11277,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -5988,6 +11342,24 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6186,6 +11558,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" + [[package]] name = "winreg" version = "0.50.0" @@ -6206,6 +11593,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "winsafe" version = "0.0.19" @@ -6219,7 +11616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b196f9328341b035820c54beebca487823e2e20a5977f284f2af2a0ee8f04400" dependencies = [ "c2rust-bitfields", - "libloading", + "libloading 0.8.5", "log", "thiserror 1.0.63", "windows-sys 0.52.0", @@ -6234,6 +11631,24 @@ dependencies = [ "bitflags 2.8.0", ] +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.0.7", + "thiserror 2.0.11", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "write16" version = "1.0.0" @@ -6246,6 +11661,98 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wry" +version = "0.54.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.11", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname 1.1.0", + "rustix 1.0.7", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -6264,6 +11771,12 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yasna" version = "0.5.2" @@ -6297,6 +11810,66 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -6304,7 +11877,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive 0.8.48", ] [[package]] @@ -6318,6 +11900,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "zerofrom" version = "0.1.6" @@ -6396,7 +11989,7 @@ dependencies = [ "flate2", "getrandom 0.3.2", "hmac", - "indexmap 2.7.1", + "indexmap 2.14.0", "liblzma", "memchr", "pbkdf2", @@ -6413,6 +12006,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zopfli" version = "0.8.2" @@ -6452,3 +12051,58 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.87", + "winnow 0.7.15", +] diff --git a/easytier-web/src/client_manager/mod.rs b/easytier-web/src/client_manager/mod.rs index d18c2ab0..abc53ed4 100644 --- a/easytier-web/src/client_manager/mod.rs +++ b/easytier-web/src/client_manager/mod.rs @@ -379,19 +379,26 @@ mod tests { let req = tokio::time::timeout(Duration::from_secs(12), async { loop { - let session = mgr + let sessions = mgr .client_sessions .iter() - .next() - .map(|item| item.value().clone()); - let Some(session) = session else { + .map(|item| item.value().clone()) + .collect::>(); + if sessions.is_empty() { tokio::time::sleep(Duration::from_millis(100)).await; continue; - }; - let mut waiter = session.data().read().await.heartbeat_waiter(); - if let Ok(req) = waiter.recv().await { + } + let mut found_req = None; + for session in sessions { + if let Some(req) = session.data().read().await.req() { + found_req = Some(req); + break; + } + } + if let Some(req) = found_req { break req; } + tokio::time::sleep(Duration::from_millis(100)).await; } }) .await diff --git a/easytier-web/src/client_manager/session.rs b/easytier-web/src/client_manager/session.rs index ab0f4fb6..3867ca4d 100644 --- a/easytier-web/src/client_manager/session.rs +++ b/easytier-web/src/client_manager/session.rs @@ -386,7 +386,7 @@ impl WebServerService for SessionRpcService { _: easytier::proto::web::GetFeatureRequest, ) -> rpc_types::error::Result { Ok(easytier::proto::web::GetFeatureResponse { - support_encryption: true, + support_encryption: easytier::web_client::security::web_secure_tunnel_supported(), }) } } diff --git a/easytier/src/gateway/quic_proxy.rs b/easytier/src/gateway/quic_proxy.rs index dfd88d95..eb468037 100644 --- a/easytier/src/gateway/quic_proxy.rs +++ b/easytier/src/gateway/quic_proxy.rs @@ -611,6 +611,11 @@ impl QuicStreamReceiver { } _ = self.tasks.join_next(), if !self.tasks.is_empty() => {} + + else => { + info!("quic stream receiver endpoint closed, exiting"); + break; + } } } } diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index 7a5966ce..8d30f5fe 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -383,6 +383,10 @@ impl InstanceConfigPatcher { global_ctx .get_acl_filter() .reload_rules(AclRuleBuilder::build(&global_ctx)?.as_ref()); + weak_upgrade(&self.peer_manager)? + .get_route() + .refresh_acl_groups() + .await; Ok(()) } diff --git a/easytier/src/peers/mod.rs b/easytier/src/peers/mod.rs index 523d6718..24720e71 100644 --- a/easytier/src/peers/mod.rs +++ b/easytier/src/peers/mod.rs @@ -20,6 +20,7 @@ pub mod foreign_network_client; pub mod foreign_network_manager; pub mod encrypt; +pub(crate) mod secure_datagram; pub mod peer_task; diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index 0f3d4992..f260e601 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -418,6 +418,32 @@ impl Debug for SyncedRouteInfo { } impl SyncedRouteInfo { + fn set_peer_groups(&self, peer_id: PeerId, groups: HashMap>) { + if groups.is_empty() { + self.group_trust_map.remove(&peer_id); + self.group_trust_map_cache.remove(&peer_id); + return; + } + + let group_names = groups.keys().cloned().collect(); + self.group_trust_map.insert(peer_id, groups); + self.group_trust_map_cache + .insert(peer_id, Arc::new(group_names)); + } + + fn get_proof_groups(&self, peer_id: PeerId) -> HashMap> { + self.group_trust_map + .get(&peer_id) + .map(|groups| { + groups + .iter() + .filter(|(_, proof)| !proof.is_empty()) + .map(|(group, proof)| (group.clone(), proof.clone())) + .collect() + }) + .unwrap_or_default() + } + fn mark_credential_peer(info: &mut RoutePeerInfo, is_credential_peer: bool) { let mut feature_flag = info.feature_flag.unwrap_or_default(); feature_flag.is_credential_peer = is_credential_peer; @@ -439,13 +465,38 @@ impl SyncedRouteInfo { } fn remove_peer(&self, peer_id: PeerId) { - tracing::warn!(?peer_id, "remove_peer from synced_route_info"); - self.peer_infos.write().remove(&peer_id); - self.raw_peer_infos.remove(&peer_id); - self.conn_map.write().remove(&peer_id); - self.foreign_network.retain(|k, _| k.peer_id != peer_id); - self.group_trust_map.remove(&peer_id); - self.group_trust_map_cache.remove(&peer_id); + self.remove_peers([peer_id]); + } + + fn remove_peers(&self, peer_ids: I) + where + I: IntoIterator, + { + let peer_ids: HashSet<_> = peer_ids.into_iter().collect(); + if peer_ids.is_empty() { + return; + } + + for peer_id in &peer_ids { + tracing::warn!(?peer_id, "remove_peer from synced_route_info"); + } + + { + let mut peer_infos = self.peer_infos.write(); + let mut conn_map = self.conn_map.write(); + for peer_id in &peer_ids { + peer_infos.remove(peer_id); + conn_map.remove(peer_id); + } + } + + for peer_id in &peer_ids { + self.raw_peer_infos.remove(peer_id); + self.group_trust_map.remove(peer_id); + self.group_trust_map_cache.remove(peer_id); + } + self.foreign_network + .retain(|k, _| !peer_ids.contains(&k.peer_id)); shrink_dashmap(&self.raw_peer_infos, None); shrink_dashmap(&self.foreign_network, None); @@ -827,31 +878,20 @@ impl SyncedRouteInfo { &self, peer_infos: &[RoutePeerInfo], local_group_declarations: &[GroupIdentity], + trust_admin_groups_without_proof: bool, ) { let local_group_declarations = local_group_declarations .iter() .map(|g| (g.group_name.as_str(), g.group_secret.as_str())) .collect::>(); - let verify_groups = |old_trusted_groups: Option<&HashMap>>, - info: &RoutePeerInfo| - -> HashMap> { + let verify_groups = |info: &RoutePeerInfo| -> HashMap> { let mut trusted_groups_for_peer: HashMap> = HashMap::new(); for group_proof in &info.groups { let name = &group_proof.group_name; let proof_bytes = group_proof.group_proof.clone(); - // If we already trusted this group and the proof hasn't changed, reuse it. - if old_trusted_groups - .and_then(|g| g.get(name)) - .map(|old| old == &proof_bytes) - .unwrap_or(false) - { - trusted_groups_for_peer.insert(name.clone(), proof_bytes); - continue; - } - if let Some(&local_secret) = local_group_declarations.get(group_proof.group_name.as_str()) { @@ -867,34 +907,39 @@ impl SyncedRouteInfo { } } + if trust_admin_groups_without_proof && self.is_admin_peer(info) { + for group_proof in &info.groups { + trusted_groups_for_peer + .entry(group_proof.group_name.clone()) + .or_default(); + } + } + trusted_groups_for_peer }; for info in peer_infos { match self.group_trust_map.entry(info.peer_id) { dashmap::mapref::entry::Entry::Occupied(mut entry) => { - let old_trusted_groups = entry.get().clone(); - let trusted_groups_for_peer = verify_groups(Some(&old_trusted_groups), info); + let trusted_groups_for_peer = verify_groups(info); if trusted_groups_for_peer.is_empty() { entry.remove(); self.group_trust_map_cache.remove(&info.peer_id); } else { - self.group_trust_map_cache.insert( - info.peer_id, - Arc::new(trusted_groups_for_peer.keys().cloned().collect()), - ); + let group_names = trusted_groups_for_peer.keys().cloned().collect(); + self.group_trust_map_cache + .insert(info.peer_id, Arc::new(group_names)); *entry.get_mut() = trusted_groups_for_peer; } } dashmap::mapref::entry::Entry::Vacant(entry) => { - let trusted_groups_for_peer = verify_groups(None, info); + let trusted_groups_for_peer = verify_groups(info); if !trusted_groups_for_peer.is_empty() { - self.group_trust_map_cache.insert( - info.peer_id, - Arc::new(trusted_groups_for_peer.keys().cloned().collect()), - ); + let group_names = trusted_groups_for_peer.keys().cloned().collect(); + self.group_trust_map_cache + .insert(info.peer_id, Arc::new(group_names)); entry.insert(trusted_groups_for_peer); } } @@ -904,16 +949,12 @@ impl SyncedRouteInfo { fn update_my_group_trusts(&self, my_peer_id: PeerId, groups: &[PeerGroupInfo]) { let mut my_group_map = HashMap::new(); - let mut my_group_names = Vec::new(); for group in groups.iter() { my_group_map.insert(group.group_name.clone(), group.group_proof.clone()); - my_group_names.push(group.group_name.clone()); } - self.group_trust_map.insert(my_peer_id, my_group_map); - self.group_trust_map_cache - .insert(my_peer_id, Arc::new(my_group_names)); + self.set_peer_groups(my_peer_id, my_group_map); } /// Collect trusted credential pubkeys from admin nodes (network_secret holders) @@ -1004,18 +1045,13 @@ impl SyncedRouteInfo { continue; } if let Some(tc) = all_trusted.get(&info.noise_static_pubkey) { - // This peer is a credential peer, assign groups from credential declaration - if !tc.groups.is_empty() { - let mut group_map = HashMap::new(); - let mut group_names = Vec::new(); - for g in &tc.groups { - group_map.insert(g.clone(), Vec::new()); // no proof needed, admin-declared - group_names.push(g.clone()); - } - self.group_trust_map.insert(info.peer_id, group_map); - self.group_trust_map_cache - .insert(info.peer_id, Arc::new(group_names)); + // Start from proof-backed groups so credential-declared groups can coexist + // without leaving stale credential-only entries behind after refreshes. + let mut group_map = self.get_proof_groups(info.peer_id); + for g in &tc.groups { + group_map.entry(g.clone()).or_default(); } + self.set_peer_groups(info.peer_id, group_map); } } @@ -1039,19 +1075,10 @@ impl SyncedRouteInfo { // Remove untrusted peers from peer_infos so they won't appear in route graph if !untrusted_peers.is_empty() { drop(peer_infos); // release read lock before writing - let mut peer_infos_write = self.peer_infos.write(); for peer_id in &untrusted_peers { tracing::warn!(?peer_id, "removing untrusted peer from route info"); - peer_infos_write.remove(peer_id); - self.raw_peer_infos.remove(peer_id); } - drop(peer_infos_write); - // Also remove from conn_map - let mut conn_map = self.conn_map.write(); - for peer_id in &untrusted_peers { - conn_map.remove(peer_id); - } - self.version.inc(); + self.remove_peers(untrusted_peers.iter().copied()); } (untrusted_peers, global_trusted_keys) @@ -2444,14 +2471,53 @@ impl PeerRouteServiceImpl { my_peer_info_updated || my_conn_info_updated || my_foreign_network_updated } - async fn refresh_credential_trusts_and_disconnect(&self) -> bool { + async fn refresh_acl_groups(&self) -> bool { + let my_peer_info_updated = self.update_my_peer_info(); + let trust_admin_groups_without_proof = self + .global_ctx + .get_network_identity() + .network_secret + .is_none(); + + let peer_infos: Vec<_> = self + .synced_route_info + .peer_infos + .read() + .iter() + .map(|(_, info)| info.clone()) + .collect(); + self.synced_route_info.verify_and_update_group_trusts( + &peer_infos, + &self.global_ctx.get_acl_group_declarations(), + trust_admin_groups_without_proof, + ); + + let untrusted = self.refresh_credential_trusts(); + self.disconnect_untrusted_peers(&untrusted).await; + + if my_peer_info_updated || !untrusted.is_empty() { + self.update_route_table_and_cached_local_conn_bitmap(); + self.update_foreign_network_owner_map(); + } + if my_peer_info_updated { + self.update_peer_info_last_update(); + } + + my_peer_info_updated || !untrusted.is_empty() + } + + fn refresh_credential_trusts(&self) -> Vec { let network_identity = self.global_ctx.get_network_identity(); - let network_secret = network_identity.network_secret.as_deref(); let (untrusted, global_trusted_keys) = self .synced_route_info - .verify_and_update_credential_trusts(network_secret); + .verify_and_update_credential_trusts(network_identity.network_secret.as_deref()); self.global_ctx .update_trusted_keys(global_trusted_keys, &network_identity.network_name); + untrusted + } + + async fn refresh_credential_trusts_and_disconnect(&self) -> bool { + let untrusted = self.refresh_credential_trusts(); self.disconnect_untrusted_peers(&untrusted).await; !untrusted.is_empty() } @@ -2529,9 +2595,8 @@ impl PeerRouteServiceImpl { } } - for p in to_remove.iter() { - self.synced_route_info.remove_peer(*p); - } + self.synced_route_info + .remove_peers(to_remove.iter().copied()); // clear expired foreign network info let mut to_remove = Vec::new(); @@ -3197,6 +3262,11 @@ impl RouteSessionManager { (peer_infos, raw_peer_infos.as_ref().unwrap()) }; if !pi.is_empty() { + let trust_admin_groups_without_proof = service_impl + .global_ctx + .get_network_identity() + .network_secret + .is_none(); service_impl.synced_route_info.update_peer_infos( my_peer_id, service_impl.my_peer_route_id, @@ -3209,6 +3279,7 @@ impl RouteSessionManager { .verify_and_update_group_trusts( pi, &service_impl.global_ctx.get_acl_group_declarations(), + trust_admin_groups_without_proof, ); session.update_dst_saved_peer_info_version(pi, from_peer_id); need_update_route_table = true; @@ -3228,15 +3299,7 @@ impl RouteSessionManager { if need_update_route_table { // Run credential verification and update route table - let network_identity = service_impl.global_ctx.get_network_identity(); - let (untrusted, global_trusted_keys) = service_impl - .synced_route_info - .verify_and_update_credential_trusts(network_identity.network_secret.as_deref()); - untrusted_peers = untrusted; - // Sync trusted keys to GlobalCtx for handshake verification - service_impl - .global_ctx - .update_trusted_keys(global_trusted_keys, &network_identity.network_name); + untrusted_peers = service_impl.refresh_credential_trusts(); service_impl.update_route_table_and_cached_local_conn_bitmap(); } @@ -3644,6 +3707,12 @@ impl Route for PeerRoute { fn get_peer_groups(&self, peer_id: PeerId) -> Arc> { self.service_impl.get_peer_groups(peer_id) } + + async fn refresh_acl_groups(&self) { + if self.service_impl.refresh_acl_groups().await { + self.session_mgr.sync_now("refresh_acl_groups"); + } + } } impl PeerPacketFilter for Arc {} @@ -3665,11 +3734,14 @@ mod tests { time::{Duration, SystemTime}, }; - use super::{PeerRoute, REMOVE_DEAD_PEER_INFO_AFTER}; + use super::{PeerRoute, REMOVE_DEAD_PEER_INFO_AFTER, RouteConnInfo}; use crate::{ common::{ PeerId, - global_ctx::{GlobalCtxEvent, TrustedKeySource, tests::get_mock_global_ctx}, + global_ctx::{ + GlobalCtxEvent, TrustedKeySource, + tests::{get_mock_global_ctx, get_mock_global_ctx_with_network}, + }, }, connector::udp_hole_punch::tests::replace_stun_info_collector, peers::{ @@ -3680,8 +3752,10 @@ mod tests { tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear}, }, proto::{ + acl::{Acl, AclV1, GroupIdentity, GroupInfo}, common::{NatType, PeerFeatureFlag}, peer_rpc::{ + ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, PeerGroupInfo, PeerIdentityType, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoRequest, TrustedCredentialPubkey, TrustedCredentialPubkeyProof, }, @@ -4040,6 +4114,219 @@ mod tests { ); } + #[tokio::test] + async fn credential_groups_merge_with_proof_groups_and_recompute_cleanly() { + let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); + let network_secret = "sec1"; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let credential_peer_id = 31; + let credential_pubkey = vec![7; 32]; + + let mut credential_info = RoutePeerInfo::new(); + credential_info.peer_id = credential_peer_id; + credential_info.version = 1; + credential_info.noise_static_pubkey = credential_pubkey.clone(); + credential_info.groups = vec![PeerGroupInfo::generate_with_proof( + "proof-group".to_string(), + "proof-secret".to_string(), + credential_peer_id, + )]; + + let mut admin_info = RoutePeerInfo::new(); + admin_info.peer_id = 32; + admin_info.version = 1; + admin_info.feature_flag = Some(PeerFeatureFlag { + is_credential_peer: false, + ..Default::default() + }); + admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( + TrustedCredentialPubkey { + pubkey: credential_pubkey.clone(), + groups: vec!["cred-group".to_string()], + expiry_unix: now + 600, + ..Default::default() + }, + network_secret, + )]; + + { + let mut guard = service_impl.synced_route_info.peer_infos.write(); + guard.insert(admin_info.peer_id, admin_info.clone()); + guard.insert(credential_peer_id, credential_info.clone()); + } + + service_impl + .synced_route_info + .verify_and_update_group_trusts( + &[credential_info], + &[GroupIdentity { + group_name: "proof-group".to_string(), + group_secret: "proof-secret".to_string(), + }], + false, + ); + service_impl + .synced_route_info + .verify_and_update_credential_trusts(Some(network_secret)); + + let groups = service_impl.get_peer_groups(credential_peer_id); + assert!(groups.contains(&"proof-group".to_string())); + assert!(groups.contains(&"cred-group".to_string())); + + let guard = service_impl.synced_route_info.peer_infos.write(); + let admin_info = guard.get(&32).unwrap().clone(); + drop(guard); + + let mut updated_admin = admin_info; + updated_admin.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof::new_signed( + TrustedCredentialPubkey { + pubkey: credential_pubkey.clone(), + groups: vec!["replacement-group".to_string()], + expiry_unix: now + 600, + ..Default::default() + }, + network_secret, + )]; + service_impl + .synced_route_info + .peer_infos + .write() + .insert(updated_admin.peer_id, updated_admin); + + service_impl + .synced_route_info + .verify_and_update_credential_trusts(Some(network_secret)); + + let groups = service_impl.get_peer_groups(credential_peer_id); + assert!(groups.contains(&"proof-group".to_string())); + assert!(groups.contains(&"replacement-group".to_string())); + assert!(!groups.contains(&"cred-group".to_string())); + } + + #[tokio::test] + async fn remove_peers_batches_cleanup_and_version_increment() { + let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); + let removed_peer_ids = [41, 42]; + let retained_peer_id = 43; + + { + let mut peer_infos = service_impl.synced_route_info.peer_infos.write(); + let mut conn_map = service_impl.synced_route_info.conn_map.write(); + for peer_id in removed_peer_ids { + let mut info = RoutePeerInfo::new(); + info.peer_id = peer_id; + info.version = 1; + peer_infos.insert(peer_id, info); + conn_map.insert(peer_id, RouteConnInfo::default()); + } + + let mut retained_info = RoutePeerInfo::new(); + retained_info.peer_id = retained_peer_id; + retained_info.version = 1; + peer_infos.insert(retained_peer_id, retained_info); + conn_map.insert(retained_peer_id, RouteConnInfo::default()); + } + + for peer_id in removed_peer_ids { + service_impl.synced_route_info.raw_peer_infos.insert( + peer_id, + DynamicMessage::new(RoutePeerInfo::default().descriptor()), + ); + service_impl.synced_route_info.group_trust_map.insert( + peer_id, + HashMap::from([("guest".to_string(), vec![1, 2, 3])]), + ); + service_impl + .synced_route_info + .group_trust_map_cache + .insert(peer_id, Arc::new(vec!["guest".to_string()])); + service_impl.synced_route_info.foreign_network.insert( + ForeignNetworkRouteInfoKey { + peer_id, + ..Default::default() + }, + ForeignNetworkRouteInfoEntry::default(), + ); + } + + service_impl.synced_route_info.foreign_network.insert( + ForeignNetworkRouteInfoKey { + peer_id: retained_peer_id, + ..Default::default() + }, + ForeignNetworkRouteInfoEntry::default(), + ); + + let initial_version = service_impl.synced_route_info.version.get(); + service_impl + .synced_route_info + .remove_peers(removed_peer_ids); + + assert_eq!( + service_impl.synced_route_info.version.get(), + initial_version + 1 + ); + for peer_id in removed_peer_ids { + assert!( + !service_impl + .synced_route_info + .peer_infos + .read() + .contains_key(&peer_id) + ); + assert!( + !service_impl + .synced_route_info + .conn_map + .read() + .contains_key(&peer_id) + ); + assert!( + !service_impl + .synced_route_info + .raw_peer_infos + .contains_key(&peer_id) + ); + assert!( + !service_impl + .synced_route_info + .group_trust_map + .contains_key(&peer_id) + ); + assert!( + !service_impl + .synced_route_info + .group_trust_map_cache + .contains_key(&peer_id) + ); + assert!( + !service_impl.synced_route_info.foreign_network.contains_key( + &ForeignNetworkRouteInfoKey { + peer_id, + ..Default::default() + } + ) + ); + } + + assert!( + service_impl + .synced_route_info + .peer_infos + .read() + .contains_key(&retained_peer_id) + ); + assert!(service_impl.synced_route_info.foreign_network.contains_key( + &ForeignNetworkRouteInfoKey { + peer_id: retained_peer_id, + ..Default::default() + } + )); + } + #[tokio::test] async fn sync_route_info_marks_credential_sender_and_filters_entries() { let peer_mgr = create_mock_pmgr().await; @@ -4208,6 +4495,7 @@ mod tests { admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof { credential: Some(TrustedCredentialPubkey { pubkey: credential_pubkey.clone(), + groups: vec!["guest".to_string()], expiry_unix: i64::MAX, ..Default::default() }), @@ -4237,6 +4525,11 @@ mod tests { .trusted_credential_pubkeys .contains_key(&credential_pubkey) ); + assert!( + service_impl + .get_peer_groups(credential_peer_id) + .contains(&"guest".to_string()) + ); service_impl.clear_expired_peer().await; @@ -4260,6 +4553,299 @@ mod tests { .read() .contains_key(&credential_peer_id) ); + assert!( + !service_impl + .synced_route_info + .group_trust_map_cache + .contains_key(&credential_peer_id) + ); + } + + #[tokio::test] + async fn refresh_acl_groups_returns_true_when_untrusted_peers_are_disconnected() { + let service_impl = PeerRouteServiceImpl::new(1, get_mock_global_ctx()); + let credential_peer_id: PeerId = 10061; + let credential_pubkey = vec![8u8; 32]; + let closed_peers = Arc::new(Mutex::new(Vec::new())); + + *service_impl.interface.lock().await = Some(Box::new(TrackingInterface { + my_peer_id: service_impl.my_peer_id, + closed_peers: closed_peers.clone(), + })); + + let mut credential_info = RoutePeerInfo::new(); + credential_info.peer_id = credential_peer_id; + credential_info.version = 1; + credential_info.noise_static_pubkey = credential_pubkey.clone(); + credential_info.feature_flag = Some(PeerFeatureFlag { + is_credential_peer: true, + ..Default::default() + }); + let self_info = RoutePeerInfo::new_updated_self( + service_impl.my_peer_id, + service_impl.my_peer_route_id, + &service_impl.global_ctx, + ); + let mut self_info = self_info; + self_info.version = 1; + self_info.last_update = Some(SystemTime::now().into()); + { + let mut guard = service_impl.synced_route_info.peer_infos.write(); + guard.insert(service_impl.my_peer_id, self_info); + guard.insert(credential_peer_id, credential_info); + } + service_impl + .synced_route_info + .trusted_credential_pubkeys + .insert( + credential_pubkey.clone(), + TrustedCredentialPubkey { + pubkey: credential_pubkey.clone(), + expiry_unix: i64::MAX, + ..Default::default() + }, + ); + + assert!(service_impl.refresh_acl_groups().await); + assert!(closed_peers.lock().contains(&credential_peer_id)); + assert!( + !service_impl + .synced_route_info + .peer_infos + .read() + .contains_key(&credential_peer_id) + ); + assert!( + !service_impl + .synced_route_info + .trusted_credential_pubkeys + .contains_key(&credential_pubkey) + ); + } + + #[tokio::test] + async fn refresh_acl_groups_updates_local_membership_immediately() { + let peer_mgr = create_mock_pmgr().await; + let route = create_mock_route(peer_mgr.clone()).await; + let my_peer_id = peer_mgr.my_peer_id(); + + assert!(route.service_impl.get_peer_groups(my_peer_id).is_empty()); + + peer_mgr.get_global_ctx().config.set_acl(Some(Acl { + acl_v1: Some(AclV1 { + group: Some(GroupInfo { + declares: vec![GroupIdentity { + group_name: "admin".to_string(), + group_secret: "admin-secret".to_string(), + }], + members: vec!["admin".to_string()], + }), + ..Default::default() + }), + })); + + route.refresh_acl_groups().await; + + let groups = route.service_impl.get_peer_groups(my_peer_id); + assert!(groups.contains(&"admin".to_string())); + assert_eq!(groups.len(), 1); + } + + #[tokio::test] + async fn refresh_acl_groups_revalidates_cached_remote_groups() { + let peer_mgr = create_mock_pmgr().await; + let route = create_mock_route(peer_mgr.clone()).await; + let remote_peer_id = 200; + let remote_group = PeerGroupInfo::generate_with_proof( + "ops".to_string(), + "secret-v1".to_string(), + remote_peer_id, + ); + + peer_mgr.get_global_ctx().config.set_acl(Some(Acl { + acl_v1: Some(AclV1 { + group: Some(GroupInfo { + declares: vec![GroupIdentity { + group_name: "ops".to_string(), + group_secret: "secret-v1".to_string(), + }], + members: vec![], + }), + ..Default::default() + }), + })); + + let mut remote_info = RoutePeerInfo::new(); + remote_info.peer_id = remote_peer_id; + remote_info.version = 1; + remote_info.groups = vec![remote_group]; + route + .service_impl + .synced_route_info + .peer_infos + .write() + .insert(remote_peer_id, remote_info.clone()); + route + .service_impl + .synced_route_info + .verify_and_update_group_trusts( + &[remote_info], + &peer_mgr.get_global_ctx().get_acl_group_declarations(), + false, + ); + + assert!( + route + .service_impl + .get_peer_groups(remote_peer_id) + .contains(&"ops".to_string()) + ); + + peer_mgr.get_global_ctx().config.set_acl(Some(Acl { + acl_v1: Some(AclV1 { + group: Some(GroupInfo { + declares: vec![GroupIdentity { + group_name: "ops".to_string(), + group_secret: "secret-v2".to_string(), + }], + members: vec![], + }), + ..Default::default() + }), + })); + + route.refresh_acl_groups().await; + + assert!( + route + .service_impl + .get_peer_groups(remote_peer_id) + .is_empty() + ); + } + + #[tokio::test] + async fn credential_verifier_trusts_admin_self_groups_from_multiple_admins() { + let service_impl = PeerRouteServiceImpl::new( + 1, + get_mock_global_ctx_with_network(Some( + crate::common::config::NetworkIdentity::new_credential("net1".to_string()), + )), + ); + + let mut admin_a = RoutePeerInfo::new(); + admin_a.peer_id = 501; + admin_a.version = 1; + admin_a.groups = vec![ + PeerGroupInfo { + group_name: "ops".to_string(), + group_proof: vec![1; 32], + }, + PeerGroupInfo { + group_name: "core-admin".to_string(), + group_proof: vec![2; 32], + }, + ]; + + let mut admin_b = RoutePeerInfo::new(); + admin_b.peer_id = 502; + admin_b.version = 1; + admin_b.groups = vec![PeerGroupInfo { + group_name: "audit".to_string(), + group_proof: vec![3; 32], + }]; + + service_impl + .synced_route_info + .verify_and_update_group_trusts(&[admin_a.clone(), admin_b.clone()], &[], true); + + let admin_a_groups = service_impl.get_peer_groups(admin_a.peer_id); + assert!(admin_a_groups.contains(&"ops".to_string())); + assert!(admin_a_groups.contains(&"core-admin".to_string())); + + let admin_b_groups = service_impl.get_peer_groups(admin_b.peer_id); + assert!(admin_b_groups.contains(&"audit".to_string())); + } + + #[tokio::test] + async fn credential_verifier_still_checks_credential_self_declared_groups() { + let service_impl = PeerRouteServiceImpl::new( + 1, + get_mock_global_ctx_with_network(Some( + crate::common::config::NetworkIdentity::new_credential("net1".to_string()), + )), + ); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let credential_peer_id = 601; + let credential_pubkey = vec![9; 32]; + + let mut admin_info = RoutePeerInfo::new(); + admin_info.peer_id = 600; + admin_info.version = 1; + admin_info.trusted_credential_pubkeys = vec![TrustedCredentialPubkeyProof { + credential: Some(TrustedCredentialPubkey { + pubkey: credential_pubkey.clone(), + groups: vec!["cred-acl".to_string()], + expiry_unix: now + 600, + ..Default::default() + }), + credential_hmac: vec![7; 32], + }]; + + let mut credential_info = RoutePeerInfo::new(); + credential_info.peer_id = credential_peer_id; + credential_info.version = 1; + credential_info.noise_static_pubkey = credential_pubkey.clone(); + credential_info.feature_flag = Some(PeerFeatureFlag { + is_credential_peer: true, + ..Default::default() + }); + credential_info.groups = vec![ + PeerGroupInfo::generate_with_proof( + "proof-group".to_string(), + "proof-secret".to_string(), + credential_peer_id, + ), + PeerGroupInfo::generate_with_proof( + "invalid-group".to_string(), + "wrong-secret".to_string(), + credential_peer_id, + ), + ]; + + { + let mut guard = service_impl.synced_route_info.peer_infos.write(); + guard.insert(admin_info.peer_id, admin_info.clone()); + guard.insert(credential_info.peer_id, credential_info.clone()); + } + + service_impl + .synced_route_info + .verify_and_update_group_trusts( + &[admin_info, credential_info], + &[ + GroupIdentity { + group_name: "proof-group".to_string(), + group_secret: "proof-secret".to_string(), + }, + GroupIdentity { + group_name: "invalid-group".to_string(), + group_secret: "actual-secret".to_string(), + }, + ], + true, + ); + service_impl + .synced_route_info + .verify_and_update_credential_trusts(None); + + let groups = service_impl.get_peer_groups(credential_peer_id); + assert!(groups.contains(&"proof-group".to_string())); + assert!(groups.contains(&"cred-acl".to_string())); + assert!(!groups.contains(&"invalid-group".to_string())); } #[rstest::rstest] diff --git a/easytier/src/peers/peer_session.rs b/easytier/src/peers/peer_session.rs index 1fe813f6..ee6a947a 100644 --- a/easytier/src/peers/peer_session.rs +++ b/easytier/src/peers/peer_session.rs @@ -1,26 +1,14 @@ -use std::{ - sync::{ - Arc, Mutex, RwLock, - atomic::{AtomicBool, AtomicU32, Ordering}, - }, - time::{SystemTime, UNIX_EPOCH}, +use std::sync::{ + Arc, RwLock, + atomic::{AtomicBool, Ordering}, }; -use atomic_shim::AtomicU64; - -use crate::{ - common::PeerId, - peers::encrypt::{Encryptor, create_encryptor}, - tunnel::packet_def::{StandardAeadTail, ZCPacket}, -}; use anyhow::anyhow; use dashmap::DashMap; -use hmac::{Hmac, Mac as _}; -use rand::RngCore as _; -use sha2::Sha256; -use zerocopy::FromBytes; -type HmacSha256 = Hmac; +use super::secure_datagram::{SecureDatagramDirection, SecureDatagramSession}; +use crate::{common::PeerId, tunnel::packet_def::ZCPacket}; + pub struct UpsertResponderSessionReturn { pub session: Arc, pub action: PeerSessionAction, @@ -87,9 +75,6 @@ impl PeerSessionStore { self.sessions.insert(key, session); } - /// Remove sessions that are no longer referenced by any PeerConn or RelayPeerMap. - /// A session with strong_count == 1 means only the store holds it — no active - /// connection is using it, so it can be safely cleaned up. pub fn evict_unused_sessions(&self) { self.sessions .retain(|_key, session| Arc::strong_count(session) > 1); @@ -188,7 +173,6 @@ impl PeerSessionStore { } PeerSessionAction::Sync | PeerSessionAction::Create => { let root_key = root_key_32.ok_or_else(|| anyhow!("missing root_key"))?; - // If the existing session is invalidated, remove it so we create a fresh one if let Some(existing) = self.sessions.get(key) && !existing.is_valid() { @@ -224,253 +208,25 @@ impl PeerSessionStore { } } -#[derive(Clone, Default)] -struct EpochKeySlot { - epoch: u32, - generation: u32, - valid: bool, - send_cipher: Option>, - recv_cipher: Option>, -} - -impl std::fmt::Debug for EpochKeySlot { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EpochKeySlot") - .field("epoch", &self.epoch) - .field("generation", &self.generation) - .field("valid", &self.valid) - .finish() - } -} - -impl EpochKeySlot { - fn get_encryptor(&self, is_send: bool) -> Arc { - if is_send { - self.send_cipher.as_ref().unwrap().clone() - } else { - self.recv_cipher.as_ref().unwrap().clone() - } - } -} - -#[derive(Debug, Clone, Copy, Default)] -struct ReplayWindow256 { - max_seq: u64, - bitmap: [u8; 32], - valid: bool, -} - -impl ReplayWindow256 { - fn clear(&mut self) { - self.max_seq = 0; - self.bitmap.fill(0); - self.valid = false; - } - - fn test_bit(&self, idx: usize) -> bool { - let byte = idx / 8; - let bit = idx % 8; - (self.bitmap[byte] >> bit) & 1 == 1 - } - - fn set_bit(&mut self, idx: usize) { - let byte = idx / 8; - let bit = idx % 8; - self.bitmap[byte] |= 1u8 << bit; - } - - fn shift_right(&mut self, shift: usize) { - if shift == 0 { - return; - } - let total_bits = 256usize; - if shift >= total_bits { - self.bitmap.fill(0); - return; - } - - let byte_shift = shift / 8; - let bit_shift = shift % 8; - - if byte_shift > 0 { - for i in (0..self.bitmap.len()).rev() { - self.bitmap[i] = if i >= byte_shift { - self.bitmap[i - byte_shift] - } else { - 0 - }; - } - } - - if bit_shift > 0 { - let mut carry = 0u8; - for b in self.bitmap.iter_mut() { - let new_carry = *b >> (8 - bit_shift); - *b = (*b << bit_shift) | carry; - carry = new_carry; - } - } - } - - fn accept(&mut self, seq: u64) -> bool { - if !self.valid { - self.valid = true; - self.max_seq = seq; - self.set_bit(0); - return true; - } - - if seq > self.max_seq { - let shift = (seq - self.max_seq) as usize; - self.shift_right(shift); - self.max_seq = seq; - self.set_bit(0); - return true; - } - - let delta = (self.max_seq - seq) as usize; - if delta >= 256 { - return false; - } - if self.test_bit(delta) { - return false; - } - self.set_bit(delta); - true - } -} - -#[derive(Debug, Clone, Copy, Default)] -struct EpochRxSlot { - epoch: u32, - window: ReplayWindow256, - last_rx_ms: u64, - valid: bool, -} - -impl EpochRxSlot { - fn clear(&mut self) { - self.epoch = 0; - self.window.clear(); - self.last_rx_ms = 0; - self.valid = false; - } -} - -#[derive(Debug, Clone, Copy, Default)] -struct SyncRxGrace { - slots: [[EpochRxSlot; 2]; 2], - expires_at_ms: u64, - valid: bool, -} - -impl SyncRxGrace { - fn clear(&mut self) { - self.slots = [[EpochRxSlot::default(), EpochRxSlot::default()]; 2]; - self.expires_at_ms = 0; - self.valid = false; - } - - fn refresh(&mut self, slots: [[EpochRxSlot; 2]; 2], expires_at_ms: u64) { - self.slots = slots; - self.expires_at_ms = expires_at_ms; - self.valid = true; - } - - fn maybe_expire(&mut self, now_ms: u64) { - if self.valid && now_ms >= self.expires_at_ms { - self.clear(); - } - } -} - pub struct PeerSession { peer_id: PeerId, - root_key: RwLock<[u8; 32]>, - session_generation: AtomicU32, peer_static_pubkey: RwLock>, - - send_epoch: AtomicU32, - send_seq: [AtomicU64; 2], - send_epoch_started_ms: AtomicU64, - send_packets_since_epoch: AtomicU64, - - rx_slots: Mutex<[[EpochRxSlot; 2]; 2]>, - key_cache: Mutex<[[EpochKeySlot; 2]; 2]>, - sync_rx_grace: Mutex, - sync_rx_grace_expires_at_ms: AtomicU64, - - send_cipher_algorithm: String, - recv_cipher_algorithm: String, - - /// Set to true when the session is detected as corrupted (persistent decrypt failures). - /// Holders of Arc can check this to know the session should be discarded. + datagram: SecureDatagramSession, invalidated: AtomicBool, - /// Consecutive decrypt failure counter. Auto-invalidates when threshold is reached. - decrypt_fail_count: AtomicU32, } impl std::fmt::Debug for PeerSession { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PeerSession") .field("peer_id", &self.peer_id) - .field("root_key", &self.root_key) - .field("session_generation", &self.session_generation) .field("peer_static_pubkey", &self.peer_static_pubkey) - .field("send_epoch", &self.send_epoch) - .field("send_seq", &self.send_seq) - .field("send_epoch_started_ms", &self.send_epoch_started_ms) - .field("send_packets_since_epoch", &self.send_packets_since_epoch) - .field("rx_slots", &self.rx_slots) - .field("key_cache", &self.key_cache) - .field("sync_rx_grace", &self.sync_rx_grace) - .field( - "sync_rx_grace_expires_at_ms", - &self.sync_rx_grace_expires_at_ms, - ) - .field("send_cipher_algorithm", &self.send_cipher_algorithm) - .field("recv_cipher_algorithm", &self.recv_cipher_algorithm) + .field("datagram", &self.datagram) .finish() } } impl PeerSession { - /// Idle-eviction timeout for receive slots, in milliseconds. - /// - /// If no packets are received for this period (~30 seconds), the - /// corresponding RX slot is considered idle and may be cleared/reused. - /// This helps reclaim state for dead peers or paths while still tolerating - /// short network stalls. Environments with very bursty or high-latency - /// traffic may want to increase this value; low-latency or tightly - /// resource-constrained deployments may lower it. - const EVICT_IDLE_AFTER_MS: u64 = 30_000; - /// Keep the pre-sync receive windows alive briefly so in-flight packets - /// from the previous epochs are still accepted after a shared session is - /// synced in place by another connection. - const SYNC_RX_GRACE_AFTER_MS: u64 = 5_000; - - /// Maximum number of packets to send in a single epoch before forcing - /// a key/epoch rotation. - /// - /// This bounds the amount of traffic protected under a single set of - /// derived keys, which is a common best practice for long-lived secure - /// channels. The current value (~1 million packets) is a conservative - /// default chosen to balance security (more frequent rotation) and - /// performance (avoiding excessive rekeying). Deployments with very high - /// or very low packet rates may tune this threshold accordingly. - const ROTATE_AFTER_PACKETS: u64 = 1_000_000; - - /// Maximum wall-clock lifetime of a send epoch, in milliseconds. - /// - /// Even if the packet-based limit is not reached, epochs are rotated - /// after this duration (~10 minutes) to avoid long-lived keys and keep - /// replay windows bounded in time. This also limits the impact of a - /// compromised key. Installations that prioritize lower overhead over - /// more aggressive key rotation may increase this value; those with - /// stricter security requirements may decrease it. - const ROTATE_AFTER_MS: u64 = 10 * 60 * 1000; - const MAX_ACCEPTED_RX_EPOCH_AHEAD: u32 = 3; - const DECRYPT_FAIL_THRESHOLD: u32 = 10; + const SYNC_RX_GRACE_AFTER_MS: u64 = SecureDatagramSession::SYNC_RX_GRACE_AFTER_MS; pub fn new( peer_id: PeerId, @@ -481,32 +237,17 @@ impl PeerSession { recv_cipher_algorithm: String, peer_static_pubkey: Option<[u8; 32]>, ) -> Self { - let rx_slots = [ - [EpochRxSlot::default(), EpochRxSlot::default()], - [EpochRxSlot::default(), EpochRxSlot::default()], - ]; - let key_cache = [ - [EpochKeySlot::default(), EpochKeySlot::default()], - [EpochKeySlot::default(), EpochKeySlot::default()], - ]; - let now_ms = now_ms(); Self { peer_id, - root_key: RwLock::new(root_key), - session_generation: AtomicU32::new(session_generation), peer_static_pubkey: RwLock::new(peer_static_pubkey), - send_epoch: AtomicU32::new(initial_epoch), - send_seq: [AtomicU64::new(0), AtomicU64::new(0)], - send_epoch_started_ms: AtomicU64::new(now_ms), - send_packets_since_epoch: AtomicU64::new(0), - rx_slots: Mutex::new(rx_slots), - key_cache: Mutex::new(key_cache), - sync_rx_grace: Mutex::new(SyncRxGrace::default()), - sync_rx_grace_expires_at_ms: AtomicU64::new(0), - send_cipher_algorithm, - recv_cipher_algorithm, + datagram: SecureDatagramSession::new( + root_key, + session_generation, + initial_epoch, + send_cipher_algorithm, + recv_cipher_algorithm, + ), invalidated: AtomicBool::new(false), - decrypt_fail_count: AtomicU32::new(0), } } @@ -514,44 +255,29 @@ impl PeerSession { self.peer_id } - /// Mark this session as invalid. All holders of Arc will see this. pub fn invalidate(&self) { self.invalidated.store(true, Ordering::Relaxed); + self.datagram.invalidate(); } pub fn is_valid(&self) -> bool { - !self.invalidated.load(Ordering::Relaxed) + !self.invalidated.load(Ordering::Relaxed) && self.datagram.is_valid() } pub fn session_generation(&self) -> u32 { - self.session_generation.load(Ordering::Relaxed) + self.datagram.session_generation() } pub fn root_key(&self) -> [u8; 32] { - *self.root_key.read().unwrap() + self.datagram.root_key() } pub fn new_root_key() -> [u8; 32] { - let mut out = [0u8; 32]; - rand::rngs::OsRng.fill_bytes(&mut out); - out + SecureDatagramSession::new_root_key() } pub fn next_sync_epoch(&self) -> u32 { - let send_epoch = self.send_epoch.load(Ordering::Relaxed); - let rx = self.rx_slots.lock().unwrap(); - let mut max_epoch = send_epoch; - for dir in 0..2 { - let cur = rx[dir][0]; - if cur.valid { - max_epoch = max_epoch.max(cur.epoch); - } - let prev = rx[dir][1]; - if prev.valid { - max_epoch = max_epoch.max(prev.epoch); - } - } - max_epoch.wrapping_add(1) + self.datagram.next_sync_epoch() } pub fn check_encrypt_algo_same( @@ -559,12 +285,8 @@ impl PeerSession { send_algorithm: &str, recv_algorithm: &str, ) -> Result<(), anyhow::Error> { - if self.send_cipher_algorithm != send_algorithm - || self.recv_cipher_algorithm != recv_algorithm - { - return Err(anyhow!("encrypt algorithm not same")); - } - Ok(()) + self.datagram + .check_encrypt_algo_same(send_algorithm, recv_algorithm) } pub fn check_or_set_peer_static_pubkey( @@ -592,277 +314,25 @@ impl PeerSession { initial_epoch: u32, preserve_rx_grace: bool, ) { - let old_root_key = self.root_key(); - let can_preserve_rx_grace = preserve_rx_grace && old_root_key == root_key; - { - let mut g = self.root_key.write().unwrap(); - *g = root_key; - } - self.session_generation - .store(session_generation, Ordering::Relaxed); - - self.send_epoch.store(initial_epoch, Ordering::Relaxed); - self.send_seq[0].store(0, Ordering::Relaxed); - self.send_seq[1].store(0, Ordering::Relaxed); - self.send_epoch_started_ms - .store(now_ms(), Ordering::Relaxed); - self.send_packets_since_epoch.store(0, Ordering::Relaxed); - - { - let mut rx = self.rx_slots.lock().unwrap(); - let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap(); - if can_preserve_rx_grace { - let expires_at_ms = now_ms().saturating_add(Self::SYNC_RX_GRACE_AFTER_MS); - sync_rx_grace.refresh(*rx, expires_at_ms); - self.sync_rx_grace_expires_at_ms - .store(expires_at_ms, Ordering::Relaxed); - } else { - sync_rx_grace.clear(); - self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed); - } - for dir in 0..2 { - rx[dir][0].clear(); - rx[dir][1].clear(); - } - } - - self.key_cache - .lock() - .unwrap() - .fill([EpochKeySlot::default(), EpochKeySlot::default()]); + self.datagram.sync_root_key( + root_key, + session_generation, + initial_epoch, + preserve_rx_grace, + ); } - pub fn dir_for_sender(sender_peer_id: PeerId, receiver_peer_id: PeerId) -> usize { + pub fn dir_for_sender( + sender_peer_id: PeerId, + receiver_peer_id: PeerId, + ) -> SecureDatagramDirection { if sender_peer_id < receiver_peer_id { - 0 + SecureDatagramDirection::AToB } else { - 1 + SecureDatagramDirection::BToA } } - fn hkdf_traffic_key(&self, epoch: u32, dir: usize) -> [u8; 32] { - let root_key = self.root_key(); - let salt = [0u8; 32]; - let mut extract = HmacSha256::new_from_slice(&salt).unwrap(); - extract.update(&root_key); - let prk = extract.finalize().into_bytes(); - - let mut info = Vec::with_capacity(9 + 4 + 1); - info.extend_from_slice(b"et-traffic"); - info.extend_from_slice(&epoch.to_be_bytes()); - info.push(dir as u8); - - let mut expand = HmacSha256::new_from_slice(&prk).unwrap(); - expand.update(&info); - expand.update(&[1u8]); - let okm = expand.finalize().into_bytes(); - let mut key = [0u8; 32]; - key.copy_from_slice(&okm[..32]); - key - } - - fn get_or_create_encryptor( - &self, - epoch: u32, - dir: usize, - generation: u32, - is_send: bool, - ) -> Arc { - let mut guard = self.key_cache.lock().unwrap(); - for slot in guard[dir].iter_mut() { - if slot.valid && slot.epoch == epoch && slot.generation == generation { - return slot.get_encryptor(is_send); - } - } - - let key = self.hkdf_traffic_key(epoch, dir); - let mut key_128 = [0u8; 16]; - key_128.copy_from_slice(&key[..16]); - - let slot = EpochKeySlot { - epoch, - generation, - valid: true, - send_cipher: Some(create_encryptor(&self.send_cipher_algorithm, key_128, key)), - recv_cipher: Some(create_encryptor(&self.recv_cipher_algorithm, key_128, key)), - }; - let ret = slot.get_encryptor(is_send); - - if !guard[dir][0].valid || guard[dir][0].epoch == epoch { - guard[dir][0] = slot; - } else { - guard[dir][1] = slot; - } - - ret - } - - fn maybe_rotate_epoch(&self, now_ms: u64) { - let packets = self - .send_packets_since_epoch - .fetch_add(1, Ordering::Relaxed) - + 1; - let started = self.send_epoch_started_ms.load(Ordering::Relaxed); - if packets < Self::ROTATE_AFTER_PACKETS - && now_ms.saturating_sub(started) < Self::ROTATE_AFTER_MS - { - return; - } - - let cur = self.send_epoch.load(Ordering::Relaxed); - let next = cur.wrapping_add(1); - if self - .send_epoch - .compare_exchange(cur, next, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - self.send_epoch_started_ms.store(now_ms, Ordering::Relaxed); - self.send_packets_since_epoch.store(0, Ordering::Relaxed); - } - } - - fn next_nonce(&self, dir: usize) -> (u32, u64, [u8; 12]) { - let now_ms = now_ms(); - self.maybe_rotate_epoch(now_ms); - let epoch = self.send_epoch.load(Ordering::Relaxed); - let seq = self.send_seq[dir].fetch_add(1, Ordering::Relaxed); - let mut nonce = [0u8; 12]; - nonce[..4].copy_from_slice(&epoch.to_be_bytes()); - nonce[4..].copy_from_slice(&seq.to_be_bytes()); - (epoch, seq, nonce) - } - - fn parse_tail(payload: &[u8]) -> Option<[u8; 12]> { - let tail = StandardAeadTail::ref_from_suffix(payload)?; - Some(tail.nonce) - } - - fn evict_old_rx_slots(rx: &mut [[EpochRxSlot; 2]; 2], now_ms: u64) { - for dir_slots in rx.iter_mut() { - for slot in dir_slots.iter_mut() { - if !slot.valid { - continue; - } - let last = slot.last_rx_ms; - if last != 0 && now_ms.saturating_sub(last) > Self::EVICT_IDLE_AFTER_MS { - slot.clear(); - } - } - } - } - - fn epoch_in_slots(slots: &[EpochRxSlot; 2], epoch: u32) -> bool { - slots[0].valid && slots[0].epoch == epoch || slots[1].valid && slots[1].epoch == epoch - } - - fn sync_rx_grace_active(&self, now_ms: u64) -> bool { - let expires_at_ms = self.sync_rx_grace_expires_at_ms.load(Ordering::Relaxed); - if expires_at_ms == 0 { - return false; - } - if now_ms < expires_at_ms { - return true; - } - self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed); - false - } - - fn check_replay(&self, epoch: u32, seq: u64, dir: usize, now_ms: u64) -> bool { - let mut rx = self.rx_slots.lock().unwrap(); - Self::evict_old_rx_slots(&mut rx, now_ms); - let mut sync_rx_grace = if self.sync_rx_grace_active(now_ms) { - let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap(); - sync_rx_grace.maybe_expire(now_ms); - if sync_rx_grace.valid { - Self::evict_old_rx_slots(&mut sync_rx_grace.slots, now_ms); - Some(sync_rx_grace) - } else { - self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed); - None - } - } else { - None - }; - let send_epoch = self.send_epoch.load(Ordering::Relaxed); - { - let mut key_cache = self.key_cache.lock().unwrap(); - for d in 0..2 { - for s in 0..2 { - if !key_cache[d][s].valid { - continue; - } - let e = key_cache[d][s].epoch; - let allowed = e == send_epoch - || rx[d][0].valid && rx[d][0].epoch == e - || rx[d][1].valid && rx[d][1].epoch == e - || sync_rx_grace - .as_ref() - .is_some_and(|g| Self::epoch_in_slots(&g.slots[d], e)); - if !allowed { - key_cache[d][s].valid = false; - } - } - } - } - - if sync_rx_grace - .as_ref() - .is_some_and(|g| Self::epoch_in_slots(&g.slots[dir], epoch)) - { - for slot in sync_rx_grace.as_mut().unwrap().slots[dir].iter_mut() { - if slot.valid && slot.epoch == epoch { - slot.last_rx_ms = now_ms; - return slot.window.accept(seq); - } - } - } - - if !rx[dir][0].valid { - rx[dir][0] = EpochRxSlot { - epoch, - window: ReplayWindow256::default(), - last_rx_ms: now_ms, - valid: true, - }; - } - - if rx[dir][0].valid && epoch == rx[dir][0].epoch { - rx[dir][0].last_rx_ms = now_ms; - return rx[dir][0].window.accept(seq); - } - - if rx[dir][1].valid && epoch == rx[dir][1].epoch { - rx[dir][1].last_rx_ms = now_ms; - return rx[dir][1].window.accept(seq); - } - - if rx[dir][0].valid && epoch > rx[dir][0].epoch { - let mut baseline_epoch = send_epoch; - if rx[dir][0].valid { - baseline_epoch = baseline_epoch.max(rx[dir][0].epoch); - } - if rx[dir][1].valid { - baseline_epoch = baseline_epoch.max(rx[dir][1].epoch); - } - let max_allowed_epoch = - baseline_epoch.saturating_add(Self::MAX_ACCEPTED_RX_EPOCH_AHEAD); - if epoch > max_allowed_epoch { - return false; - } - - rx[dir][1] = rx[dir][0]; - rx[dir][0] = EpochRxSlot { - epoch, - window: ReplayWindow256::default(), - last_rx_ms: now_ms, - valid: true, - }; - return rx[dir][0].window.accept(seq); - } - - false - } - pub fn encrypt_payload( &self, sender_peer_id: PeerId, @@ -872,19 +342,8 @@ impl PeerSession { if !self.is_valid() { return Err(anyhow!("session invalidated")); } - let dir = Self::dir_for_sender(sender_peer_id, receiver_peer_id); - let (epoch, _seq, nonce_bytes) = self.next_nonce(dir); - let encryptor = self.get_or_create_encryptor(epoch, dir, self.session_generation(), true); - if let Err(e) = encryptor.encrypt_with_nonce(pkt, Some(nonce_bytes.as_slice())) { - tracing::warn!( - peer_id = ?self.peer_id, - ?e, - "session encrypt failed, invalidating" - ); - self.invalidate(); - return Err(e.into()); - } - Ok(()) + self.datagram + .encrypt_payload(Self::dir_for_sender(sender_peer_id, receiver_peer_id), pkt) } pub fn decrypt_payload( @@ -896,47 +355,13 @@ impl PeerSession { if !self.is_valid() { return Err(anyhow!("session invalidated")); } - let dir = Self::dir_for_sender(sender_peer_id, receiver_peer_id); - let nonce_bytes = - Self::parse_tail(ciphertext_with_tail.payload()).ok_or_else(|| anyhow!("no tail"))?; - let epoch = u32::from_be_bytes(nonce_bytes[..4].try_into().unwrap()); - let seq = u64::from_be_bytes(nonce_bytes[4..].try_into().unwrap()); - - let now_ms = now_ms(); - if !self.check_replay(epoch, seq, dir, now_ms) { - return Err(anyhow!( - "replay rejected, sender_peer_id: {:?}, receiver_peer_id: {:?}", - sender_peer_id, - receiver_peer_id - )); - } - - let encryptor = self.get_or_create_encryptor(epoch, dir, self.session_generation(), false); - if let Err(e) = encryptor.decrypt(ciphertext_with_tail) { - let count = self.decrypt_fail_count.fetch_add(1, Ordering::Relaxed) + 1; - if count >= Self::DECRYPT_FAIL_THRESHOLD { - self.invalidate(); - tracing::warn!( - peer_id = ?self.peer_id, - count, - "session auto-invalidated after consecutive decrypt failures" - ); - } - return Err(e.into()); - } - self.decrypt_fail_count.store(0, Ordering::Relaxed); - - Ok(()) + self.datagram.decrypt_payload( + Self::dir_for_sender(sender_peer_id, receiver_peer_id), + ciphertext_with_tail, + ) } } -fn now_ms() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64 -} - #[cfg(test)] mod tests { use super::*; @@ -984,177 +409,10 @@ mod tests { } #[test] - fn replay_rejects_far_future_epoch_without_poisoning_window() { - let peer_id: PeerId = 10; - let root_key = PeerSession::new_root_key(); - let generation = 1u32; - let initial_epoch = 0u32; - let s = PeerSession::new( - peer_id, - root_key, - generation, - initial_epoch, - "aes-256-gcm".to_string(), - "aes-256-gcm".to_string(), - None, - ); - - let now = now_ms(); - - assert!(s.check_replay(0, 1, 0, now)); - assert!(s.check_replay(0, 2, 0, now)); - - assert!(!s.check_replay(1000, 1, 0, now)); - - assert!(s.check_replay(1, 1, 0, now + 1)); - assert!(s.check_replay(1, 2, 0, now + 2)); - } - - #[test] - fn replay_window_shift_preserves_bits() { - let mut w = ReplayWindow256::default(); - // Accept seqs 0..10 - for i in 0..10u64 { - assert!(w.accept(i), "seq {i} should be accepted"); - } - assert_eq!(w.max_seq, 9); - - // All seqs 0..10 should be marked as seen (replay) - for i in 0..10u64 { - assert!(!w.accept(i), "seq {i} should be rejected as replay"); - } - - // Seq 10 should still be accepted - assert!(w.accept(10)); - } - - #[test] - fn replay_window_out_of_order_within_window() { - let mut w = ReplayWindow256::default(); - // Accept even seqs 0,2,4,...,20 - for i in (0..=20u64).step_by(2) { - assert!(w.accept(i), "seq {i} should be accepted"); - } - // Now accept odd seqs 1,3,5,...,19 (out of order, within window) - for i in (1..=19u64).step_by(2) { - assert!(w.accept(i), "seq {i} should be accepted (out of order)"); - } - // All seqs 0..=20 should now be marked as seen - for i in 0..=20u64 { - assert!(!w.accept(i), "seq {i} should be rejected as replay"); - } - } - - #[test] - fn sync_root_key_allows_any_epoch_from_remote() { - // After sync_root_key, the remote peer may still be sending at an - // old epoch. The receiver should accept those packets. - let peer_id: PeerId = 10; - let root_key = PeerSession::new_root_key(); - let s = PeerSession::new( - peer_id, - root_key, - 1, - 0, - "aes-256-gcm".to_string(), - "aes-256-gcm".to_string(), - None, - ); - - // Simulate receiving some packets at epoch 0 - let now = now_ms(); - assert!(s.check_replay(0, 0, 0, now)); - assert!(s.check_replay(0, 1, 0, now)); - - // Sync with initial_epoch=2 (simulating a Sync action) - s.sync_root_key(root_key, 2, 2, true); - - // Remote peer is still sending at epoch 0 — should be accepted - // (rx_slots were cleared, so the first packet establishes the epoch) - assert!( - s.check_replay(0, 10, 0, now + 1), - "packets at old epoch should be accepted after sync" - ); - } - - #[test] - fn sync_root_key_keeps_previous_epochs_during_grace_window() { - let peer_id: PeerId = 10; - let root_key = PeerSession::new_root_key(); - let s = PeerSession::new( - peer_id, - root_key, - 1, - 0, - "aes-256-gcm".to_string(), - "aes-256-gcm".to_string(), - None, - ); - - let now = now_ms(); - assert!(s.check_replay(0, 0, 0, now)); - assert!(s.check_replay(1, 0, 0, now + 1)); - - s.sync_root_key(root_key, 2, 2, true); - - // The first packet after sync may already use the new epoch. - assert!(s.check_replay(2, 0, 0, now + 2)); - // Older in-flight packets from pre-sync epochs should still be accepted - // during the grace period, regardless of arrival order. - assert!(s.check_replay(1, 1, 0, now + 3)); - assert!(s.check_replay(0, 1, 0, now + 4)); - } - - #[test] - fn sync_root_key_expires_previous_epochs_after_grace_window() { - let peer_id: PeerId = 10; - let root_key = PeerSession::new_root_key(); - let s = PeerSession::new( - peer_id, - root_key, - 1, - 0, - "aes-256-gcm".to_string(), - "aes-256-gcm".to_string(), - None, - ); - - let now = now_ms(); - assert!(s.check_replay(0, 0, 0, now)); - assert!(s.check_replay(1, 0, 0, now + 1)); - - s.sync_root_key(root_key, 2, 2, true); - assert!(s.check_replay(2, 0, 0, now + 2)); - - assert!( - !s.check_replay(0, 1, 0, now + PeerSession::SYNC_RX_GRACE_AFTER_MS + 3), - "old epochs should stop being accepted once the sync grace window expires" - ); - } - - #[test] - fn sync_root_key_does_not_preserve_previous_epochs_when_root_key_changes() { - let peer_id: PeerId = 10; - let root_key = PeerSession::new_root_key(); - let s = PeerSession::new( - peer_id, - root_key, - 1, - 0, - "aes-256-gcm".to_string(), - "aes-256-gcm".to_string(), - None, - ); - - let now = now_ms(); - assert!(s.check_replay(0, 0, 0, now)); - assert!(s.check_replay(1, 0, 0, now + 1)); - - s.sync_root_key(PeerSession::new_root_key(), 2, 2, true); - assert!(s.check_replay(2, 0, 0, now + 2)); - assert!( - !s.check_replay(1, 1, 0, now + 3), - "old epochs should not be preserved when sync replaces the root key" + fn sync_root_key_preserves_generic_grace_window_constant() { + assert_eq!( + PeerSession::SYNC_RX_GRACE_AFTER_MS, + SecureDatagramSession::SYNC_RX_GRACE_AFTER_MS ); } } diff --git a/easytier/src/peers/route_trait.rs b/easytier/src/peers/route_trait.rs index fdddc6bf..34080133 100644 --- a/easytier/src/peers/route_trait.rs +++ b/easytier/src/peers/route_trait.rs @@ -141,6 +141,8 @@ pub trait Route { fn get_peer_groups(&self, peer_id: PeerId) -> Arc>; + async fn refresh_acl_groups(&self) {} + async fn get_peer_groups_by_ip(&self, ip: &std::net::IpAddr) -> Arc> { match self.get_peer_id_by_ip(ip).await { Some(peer_id) => self.get_peer_groups(peer_id), diff --git a/easytier/src/peers/secure_datagram.rs b/easytier/src/peers/secure_datagram.rs new file mode 100644 index 00000000..f4722e44 --- /dev/null +++ b/easytier/src/peers/secure_datagram.rs @@ -0,0 +1,1020 @@ +use std::{ + sync::{ + Arc, Mutex, RwLock, + atomic::{AtomicBool, AtomicU32, Ordering}, + }, + time::{SystemTime, UNIX_EPOCH}, +}; + +use anyhow::anyhow; +use atomic_shim::AtomicU64; +use hmac::{Hmac, Mac as _}; +use rand::RngCore as _; +use sha2::Sha256; +use zerocopy::FromBytes; + +use crate::{ + peers::encrypt::{Encryptor, create_encryptor}, + tunnel::packet_def::{StandardAeadTail, ZCPacket}, +}; + +type HmacSha256 = Hmac; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SecureDatagramDirection { + AToB, + BToA, +} + +impl SecureDatagramDirection { + fn idx(self) -> usize { + match self { + Self::AToB => 0, + Self::BToA => 1, + } + } +} + +#[derive(Clone, Default)] +struct EpochKeySlot { + epoch: u32, + generation: u32, + valid: bool, + send_cipher: Option>, + recv_cipher: Option>, +} + +impl std::fmt::Debug for EpochKeySlot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EpochKeySlot") + .field("epoch", &self.epoch) + .field("generation", &self.generation) + .field("valid", &self.valid) + .finish() + } +} + +impl EpochKeySlot { + fn get_encryptor(&self, is_send: bool) -> Arc { + if is_send { + self.send_cipher.as_ref().unwrap().clone() + } else { + self.recv_cipher.as_ref().unwrap().clone() + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +struct ReplayWindow256 { + max_seq: u64, + bitmap: [u8; 32], + valid: bool, +} + +impl ReplayWindow256 { + fn clear(&mut self) { + self.max_seq = 0; + self.bitmap.fill(0); + self.valid = false; + } + + fn test_bit(&self, idx: usize) -> bool { + let byte = idx / 8; + let bit = idx % 8; + (self.bitmap[byte] >> bit) & 1 == 1 + } + + fn set_bit(&mut self, idx: usize) { + let byte = idx / 8; + let bit = idx % 8; + self.bitmap[byte] |= 1u8 << bit; + } + + fn shift_right(&mut self, shift: usize) { + if shift == 0 { + return; + } + let total_bits = 256usize; + if shift >= total_bits { + self.bitmap.fill(0); + return; + } + + let byte_shift = shift / 8; + let bit_shift = shift % 8; + + if byte_shift > 0 { + for i in (0..self.bitmap.len()).rev() { + self.bitmap[i] = if i >= byte_shift { + self.bitmap[i - byte_shift] + } else { + 0 + }; + } + } + + if bit_shift > 0 { + let mut carry = 0u8; + for b in self.bitmap.iter_mut() { + let new_carry = *b >> (8 - bit_shift); + *b = (*b << bit_shift) | carry; + carry = new_carry; + } + } + } + + fn accept(&mut self, seq: u64) -> bool { + if !self.valid { + self.valid = true; + self.max_seq = seq; + self.set_bit(0); + return true; + } + + if seq > self.max_seq { + let shift = (seq - self.max_seq) as usize; + self.shift_right(shift); + self.max_seq = seq; + self.set_bit(0); + return true; + } + + let delta = (self.max_seq - seq) as usize; + if delta >= 256 { + return false; + } + if self.test_bit(delta) { + return false; + } + self.set_bit(delta); + true + } + + fn can_accept(&self, seq: u64) -> bool { + if !self.valid || seq > self.max_seq { + return true; + } + + let delta = (self.max_seq - seq) as usize; + delta < 256 && !self.test_bit(delta) + } +} + +#[derive(Debug, Clone, Copy, Default)] +struct EpochRxSlot { + epoch: u32, + window: ReplayWindow256, + last_rx_ms: u64, + valid: bool, +} + +impl EpochRxSlot { + fn clear(&mut self) { + self.epoch = 0; + self.window.clear(); + self.last_rx_ms = 0; + self.valid = false; + } +} + +#[derive(Debug, Clone, Copy, Default)] +struct SyncRxGrace { + slots: [[EpochRxSlot; 2]; 2], + expires_at_ms: u64, + valid: bool, +} + +impl SyncRxGrace { + fn clear(&mut self) { + self.slots = [[EpochRxSlot::default(), EpochRxSlot::default()]; 2]; + self.expires_at_ms = 0; + self.valid = false; + } + + fn refresh(&mut self, slots: [[EpochRxSlot; 2]; 2], expires_at_ms: u64) { + self.slots = slots; + self.expires_at_ms = expires_at_ms; + self.valid = true; + } + + fn maybe_expire(&mut self, now_ms: u64) { + if self.valid && now_ms >= self.expires_at_ms { + self.clear(); + } + } +} + +pub struct SecureDatagramSession { + root_key: RwLock<[u8; 32]>, + session_generation: AtomicU32, + + send_epoch: AtomicU32, + send_seq: [AtomicU64; 2], + send_epoch_started_ms: AtomicU64, + send_packets_since_epoch: AtomicU64, + + rx_slots: Mutex<[[EpochRxSlot; 2]; 2]>, + key_cache: Mutex<[[EpochKeySlot; 2]; 2]>, + sync_rx_grace: Mutex, + sync_rx_grace_expires_at_ms: AtomicU64, + + send_cipher_algorithm: String, + recv_cipher_algorithm: String, + + invalidated: AtomicBool, + decrypt_fail_count: AtomicU32, +} + +impl std::fmt::Debug for SecureDatagramSession { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SecureDatagramSession") + .field("root_key", &self.root_key) + .field("session_generation", &self.session_generation) + .field("send_epoch", &self.send_epoch) + .field("send_seq", &self.send_seq) + .field("send_epoch_started_ms", &self.send_epoch_started_ms) + .field("send_packets_since_epoch", &self.send_packets_since_epoch) + .field("rx_slots", &self.rx_slots) + .field("key_cache", &self.key_cache) + .field("sync_rx_grace", &self.sync_rx_grace) + .field( + "sync_rx_grace_expires_at_ms", + &self.sync_rx_grace_expires_at_ms, + ) + .field("send_cipher_algorithm", &self.send_cipher_algorithm) + .field("recv_cipher_algorithm", &self.recv_cipher_algorithm) + .finish() + } +} + +impl SecureDatagramSession { + const EVICT_IDLE_AFTER_MS: u64 = 30_000; + pub(crate) const SYNC_RX_GRACE_AFTER_MS: u64 = 5_000; + const ROTATE_AFTER_PACKETS: u64 = 1_000_000; + const ROTATE_AFTER_MS: u64 = 10 * 60 * 1000; + const MAX_ACCEPTED_RX_EPOCH_AHEAD: u32 = 3; + const DECRYPT_FAIL_THRESHOLD: u32 = 10; + + pub fn new( + root_key: [u8; 32], + session_generation: u32, + initial_epoch: u32, + send_cipher_algorithm: String, + recv_cipher_algorithm: String, + ) -> Self { + let rx_slots = [ + [EpochRxSlot::default(), EpochRxSlot::default()], + [EpochRxSlot::default(), EpochRxSlot::default()], + ]; + let key_cache = [ + [EpochKeySlot::default(), EpochKeySlot::default()], + [EpochKeySlot::default(), EpochKeySlot::default()], + ]; + let now_ms = now_ms(); + Self { + root_key: RwLock::new(root_key), + session_generation: AtomicU32::new(session_generation), + send_epoch: AtomicU32::new(initial_epoch), + send_seq: [AtomicU64::new(0), AtomicU64::new(0)], + send_epoch_started_ms: AtomicU64::new(now_ms), + send_packets_since_epoch: AtomicU64::new(0), + rx_slots: Mutex::new(rx_slots), + key_cache: Mutex::new(key_cache), + sync_rx_grace: Mutex::new(SyncRxGrace::default()), + sync_rx_grace_expires_at_ms: AtomicU64::new(0), + send_cipher_algorithm, + recv_cipher_algorithm, + invalidated: AtomicBool::new(false), + decrypt_fail_count: AtomicU32::new(0), + } + } + + pub fn invalidate(&self) { + self.invalidated.store(true, Ordering::Relaxed); + } + + pub fn is_valid(&self) -> bool { + !self.invalidated.load(Ordering::Relaxed) + } + + pub fn session_generation(&self) -> u32 { + self.session_generation.load(Ordering::Relaxed) + } + + pub fn root_key(&self) -> [u8; 32] { + *self.root_key.read().unwrap() + } + + pub fn new_root_key() -> [u8; 32] { + let mut out = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut out); + out + } + + pub fn next_sync_epoch(&self) -> u32 { + let send_epoch = self.send_epoch.load(Ordering::Relaxed); + let rx = self.rx_slots.lock().unwrap(); + let mut max_epoch = send_epoch; + for dir in 0..2 { + let cur = rx[dir][0]; + if cur.valid { + max_epoch = max_epoch.max(cur.epoch); + } + let prev = rx[dir][1]; + if prev.valid { + max_epoch = max_epoch.max(prev.epoch); + } + } + max_epoch.wrapping_add(1) + } + + pub fn check_encrypt_algo_same( + &self, + send_algorithm: &str, + recv_algorithm: &str, + ) -> Result<(), anyhow::Error> { + if self.send_cipher_algorithm != send_algorithm + || self.recv_cipher_algorithm != recv_algorithm + { + return Err(anyhow!("encrypt algorithm not same")); + } + Ok(()) + } + + pub fn sync_root_key( + &self, + root_key: [u8; 32], + session_generation: u32, + initial_epoch: u32, + preserve_rx_grace: bool, + ) { + let old_root_key = self.root_key(); + let can_preserve_rx_grace = preserve_rx_grace && old_root_key == root_key; + { + let mut g = self.root_key.write().unwrap(); + *g = root_key; + } + self.session_generation + .store(session_generation, Ordering::Relaxed); + + self.send_epoch.store(initial_epoch, Ordering::Relaxed); + self.send_seq[0].store(0, Ordering::Relaxed); + self.send_seq[1].store(0, Ordering::Relaxed); + self.send_epoch_started_ms + .store(now_ms(), Ordering::Relaxed); + self.send_packets_since_epoch.store(0, Ordering::Relaxed); + + { + let mut rx = self.rx_slots.lock().unwrap(); + let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap(); + if can_preserve_rx_grace { + let expires_at_ms = now_ms().saturating_add(Self::SYNC_RX_GRACE_AFTER_MS); + sync_rx_grace.refresh(*rx, expires_at_ms); + self.sync_rx_grace_expires_at_ms + .store(expires_at_ms, Ordering::Relaxed); + } else { + sync_rx_grace.clear(); + self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed); + } + for dir in 0..2 { + rx[dir][0].clear(); + rx[dir][1].clear(); + } + } + + self.key_cache + .lock() + .unwrap() + .fill([EpochKeySlot::default(), EpochKeySlot::default()]); + } + + fn hkdf_traffic_key(&self, epoch: u32, dir: SecureDatagramDirection) -> [u8; 32] { + let root_key = self.root_key(); + let salt = [0u8; 32]; + let mut extract = HmacSha256::new_from_slice(&salt).unwrap(); + extract.update(&root_key); + let prk = extract.finalize().into_bytes(); + + let mut info = Vec::with_capacity(9 + 4 + 1); + info.extend_from_slice(b"et-traffic"); + info.extend_from_slice(&epoch.to_be_bytes()); + info.push(dir.idx() as u8); + + let mut expand = HmacSha256::new_from_slice(&prk).unwrap(); + expand.update(&info); + expand.update(&[1u8]); + let okm = expand.finalize().into_bytes(); + let mut key = [0u8; 32]; + key.copy_from_slice(&okm[..32]); + key + } + + fn get_or_create_encryptor( + &self, + epoch: u32, + dir: SecureDatagramDirection, + generation: u32, + is_send: bool, + ) -> Arc { + let dir_idx = dir.idx(); + let mut guard = self.key_cache.lock().unwrap(); + for slot in guard[dir_idx].iter_mut() { + if slot.valid && slot.epoch == epoch && slot.generation == generation { + return slot.get_encryptor(is_send); + } + } + + let key = self.hkdf_traffic_key(epoch, dir); + let mut key_128 = [0u8; 16]; + key_128.copy_from_slice(&key[..16]); + + let slot = EpochKeySlot { + epoch, + generation, + valid: true, + send_cipher: Some(create_encryptor(&self.send_cipher_algorithm, key_128, key)), + recv_cipher: Some(create_encryptor(&self.recv_cipher_algorithm, key_128, key)), + }; + let ret = slot.get_encryptor(is_send); + + if !guard[dir_idx][0].valid || guard[dir_idx][0].epoch == epoch { + guard[dir_idx][0] = slot; + } else { + guard[dir_idx][1] = slot; + } + + ret + } + + fn maybe_rotate_epoch(&self, now_ms: u64) { + let packets = self + .send_packets_since_epoch + .fetch_add(1, Ordering::Relaxed) + + 1; + let started = self.send_epoch_started_ms.load(Ordering::Relaxed); + if packets < Self::ROTATE_AFTER_PACKETS + && now_ms.saturating_sub(started) < Self::ROTATE_AFTER_MS + { + return; + } + + let cur = self.send_epoch.load(Ordering::Relaxed); + let next = cur.wrapping_add(1); + if self + .send_epoch + .compare_exchange(cur, next, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.send_epoch_started_ms.store(now_ms, Ordering::Relaxed); + self.send_packets_since_epoch.store(0, Ordering::Relaxed); + } + } + + fn next_nonce(&self, dir: SecureDatagramDirection) -> (u32, u64, [u8; 12]) { + let now_ms = now_ms(); + self.maybe_rotate_epoch(now_ms); + let epoch = self.send_epoch.load(Ordering::Relaxed); + let seq = self.send_seq[dir.idx()].fetch_add(1, Ordering::Relaxed); + let mut nonce = [0u8; 12]; + nonce[..4].copy_from_slice(&epoch.to_be_bytes()); + nonce[4..].copy_from_slice(&seq.to_be_bytes()); + (epoch, seq, nonce) + } + + fn parse_tail(payload: &[u8]) -> Option<[u8; 12]> { + let tail = StandardAeadTail::ref_from_suffix(payload)?; + Some(tail.nonce) + } + + fn evict_old_rx_slots(rx: &mut [[EpochRxSlot; 2]; 2], now_ms: u64) { + for dir_slots in rx.iter_mut() { + for slot in dir_slots.iter_mut() { + if !slot.valid { + continue; + } + let last = slot.last_rx_ms; + if last != 0 && now_ms.saturating_sub(last) > Self::EVICT_IDLE_AFTER_MS { + slot.clear(); + } + } + } + } + + fn epoch_in_slots(slots: &[EpochRxSlot; 2], epoch: u32) -> bool { + slots[0].valid && slots[0].epoch == epoch || slots[1].valid && slots[1].epoch == epoch + } + + fn sync_rx_grace_active(&self, now_ms: u64) -> bool { + let expires_at_ms = self.sync_rx_grace_expires_at_ms.load(Ordering::Relaxed); + if expires_at_ms == 0 { + return false; + } + if now_ms < expires_at_ms { + return true; + } + self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed); + false + } + + fn prune_key_cache(&self, rx: &[[EpochRxSlot; 2]; 2], sync_rx_grace: Option<&SyncRxGrace>) { + let send_epoch = self.send_epoch.load(Ordering::Relaxed); + let mut key_cache = self.key_cache.lock().unwrap(); + for d in 0..2 { + for s in 0..2 { + if !key_cache[d][s].valid { + continue; + } + let e = key_cache[d][s].epoch; + let allowed = e == send_epoch + || rx[d][0].valid && rx[d][0].epoch == e + || rx[d][1].valid && rx[d][1].epoch == e + || sync_rx_grace.is_some_and(|g| Self::epoch_in_slots(&g.slots[d], e)); + if !allowed { + key_cache[d][s].valid = false; + } + } + } + } + + fn precheck_replay( + &self, + epoch: u32, + seq: u64, + dir: SecureDatagramDirection, + now_ms: u64, + ) -> bool { + let dir_idx = dir.idx(); + let mut rx = self.rx_slots.lock().unwrap(); + Self::evict_old_rx_slots(&mut rx, now_ms); + let sync_rx_grace = if self.sync_rx_grace_active(now_ms) { + let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap(); + sync_rx_grace.maybe_expire(now_ms); + if sync_rx_grace.valid { + Self::evict_old_rx_slots(&mut sync_rx_grace.slots, now_ms); + Some(sync_rx_grace) + } else { + self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed); + None + } + } else { + None + }; + + if sync_rx_grace + .as_ref() + .is_some_and(|g| Self::epoch_in_slots(&g.slots[dir_idx], epoch)) + { + for slot in sync_rx_grace.as_ref().unwrap().slots[dir_idx].iter() { + if slot.valid && slot.epoch == epoch { + return slot.window.can_accept(seq); + } + } + } + + if !rx[dir_idx][0].valid { + return true; + } + + if rx[dir_idx][0].valid && epoch == rx[dir_idx][0].epoch { + return rx[dir_idx][0].window.can_accept(seq); + } + + if rx[dir_idx][1].valid && epoch == rx[dir_idx][1].epoch { + return rx[dir_idx][1].window.can_accept(seq); + } + + if rx[dir_idx][0].valid && epoch > rx[dir_idx][0].epoch { + let mut baseline_epoch = self.send_epoch.load(Ordering::Relaxed); + if rx[dir_idx][0].valid { + baseline_epoch = baseline_epoch.max(rx[dir_idx][0].epoch); + } + if rx[dir_idx][1].valid { + baseline_epoch = baseline_epoch.max(rx[dir_idx][1].epoch); + } + let max_allowed_epoch = + baseline_epoch.saturating_add(Self::MAX_ACCEPTED_RX_EPOCH_AHEAD); + if epoch > max_allowed_epoch { + return false; + } + + return true; + } + + false + } + + fn commit_replay( + &self, + epoch: u32, + seq: u64, + dir: SecureDatagramDirection, + now_ms: u64, + ) -> bool { + let dir_idx = dir.idx(); + let mut rx = self.rx_slots.lock().unwrap(); + Self::evict_old_rx_slots(&mut rx, now_ms); + let mut sync_rx_grace = if self.sync_rx_grace_active(now_ms) { + let mut sync_rx_grace = self.sync_rx_grace.lock().unwrap(); + sync_rx_grace.maybe_expire(now_ms); + if sync_rx_grace.valid { + Self::evict_old_rx_slots(&mut sync_rx_grace.slots, now_ms); + Some(sync_rx_grace) + } else { + self.sync_rx_grace_expires_at_ms.store(0, Ordering::Relaxed); + None + } + } else { + None + }; + + let accepted = if sync_rx_grace + .as_ref() + .is_some_and(|g| Self::epoch_in_slots(&g.slots[dir_idx], epoch)) + { + let mut accepted = false; + for slot in sync_rx_grace.as_mut().unwrap().slots[dir_idx].iter_mut() { + if slot.valid && slot.epoch == epoch { + slot.last_rx_ms = now_ms; + accepted = slot.window.accept(seq); + break; + } + } + accepted + } else { + if !rx[dir_idx][0].valid { + rx[dir_idx][0] = EpochRxSlot { + epoch, + window: ReplayWindow256::default(), + last_rx_ms: now_ms, + valid: true, + }; + } + + if rx[dir_idx][0].valid && epoch == rx[dir_idx][0].epoch { + rx[dir_idx][0].last_rx_ms = now_ms; + rx[dir_idx][0].window.accept(seq) + } else if rx[dir_idx][1].valid && epoch == rx[dir_idx][1].epoch { + rx[dir_idx][1].last_rx_ms = now_ms; + rx[dir_idx][1].window.accept(seq) + } else if rx[dir_idx][0].valid && epoch > rx[dir_idx][0].epoch { + let mut baseline_epoch = self.send_epoch.load(Ordering::Relaxed); + if rx[dir_idx][0].valid { + baseline_epoch = baseline_epoch.max(rx[dir_idx][0].epoch); + } + if rx[dir_idx][1].valid { + baseline_epoch = baseline_epoch.max(rx[dir_idx][1].epoch); + } + let max_allowed_epoch = + baseline_epoch.saturating_add(Self::MAX_ACCEPTED_RX_EPOCH_AHEAD); + if epoch > max_allowed_epoch { + false + } else { + rx[dir_idx][1] = rx[dir_idx][0]; + rx[dir_idx][0] = EpochRxSlot { + epoch, + window: ReplayWindow256::default(), + last_rx_ms: now_ms, + valid: true, + }; + rx[dir_idx][0].window.accept(seq) + } + } else { + false + } + }; + + self.prune_key_cache(&rx, sync_rx_grace.as_deref()); + accepted + } + + fn check_replay( + &self, + epoch: u32, + seq: u64, + dir: SecureDatagramDirection, + now_ms: u64, + ) -> bool { + if self.precheck_replay(epoch, seq, dir, now_ms) { + return self.commit_replay(epoch, seq, dir, now_ms); + } + + false + } + + pub fn encrypt_payload( + &self, + dir: SecureDatagramDirection, + pkt: &mut ZCPacket, + ) -> Result<(), anyhow::Error> { + if !self.is_valid() { + return Err(anyhow!("session invalidated")); + } + let (epoch, _seq, nonce_bytes) = self.next_nonce(dir); + let encryptor = self.get_or_create_encryptor(epoch, dir, self.session_generation(), true); + if let Err(e) = encryptor.encrypt_with_nonce(pkt, Some(nonce_bytes.as_slice())) { + tracing::warn!(?e, "secure datagram session encrypt failed, invalidating"); + self.invalidate(); + return Err(e.into()); + } + Ok(()) + } + + pub fn decrypt_payload( + &self, + dir: SecureDatagramDirection, + ciphertext_with_tail: &mut ZCPacket, + ) -> Result<(), anyhow::Error> { + if !self.is_valid() { + return Err(anyhow!("session invalidated")); + } + let nonce_bytes = + Self::parse_tail(ciphertext_with_tail.payload()).ok_or_else(|| anyhow!("no tail"))?; + let epoch = u32::from_be_bytes(nonce_bytes[..4].try_into().unwrap()); + let seq = u64::from_be_bytes(nonce_bytes[4..].try_into().unwrap()); + + let now_ms = now_ms(); + if !self.precheck_replay(epoch, seq, dir, now_ms) { + return Err(anyhow!("replay rejected")); + } + + let encryptor = self.get_or_create_encryptor(epoch, dir, self.session_generation(), false); + if let Err(e) = encryptor.decrypt(ciphertext_with_tail) { + let count = self.decrypt_fail_count.fetch_add(1, Ordering::Relaxed) + 1; + if count >= Self::DECRYPT_FAIL_THRESHOLD { + self.invalidate(); + tracing::warn!( + count, + "secure datagram session auto-invalidated after consecutive decrypt failures" + ); + } + return Err(e.into()); + } + self.decrypt_fail_count.store(0, Ordering::Relaxed); + + if !self.commit_replay(epoch, seq, dir, now_ms) { + return Err(anyhow!("replay rejected")); + } + + Ok(()) + } + + #[cfg(test)] + fn check_replay_for_test( + &self, + epoch: u32, + seq: u64, + dir: SecureDatagramDirection, + now_ms: u64, + ) -> bool { + self.check_replay(epoch, seq, dir, now_ms) + } +} + +fn now_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tunnel::packet_def::PacketType; + + #[test] + fn secure_datagram_supports_asymmetric_algorithms() { + let root_key = SecureDatagramSession::new_root_key(); + let generation = 1u32; + let initial_epoch = 0u32; + + let ab = SecureDatagramSession::new( + root_key, + generation, + initial_epoch, + "aes-256-gcm".to_string(), + "chacha20-poly1305".to_string(), + ); + let ba = SecureDatagramSession::new( + root_key, + generation, + initial_epoch, + "chacha20-poly1305".to_string(), + "aes-256-gcm".to_string(), + ); + + let plaintext1 = b"hello from a"; + let mut pkt1 = ZCPacket::new_with_payload(plaintext1); + pkt1.fill_peer_manager_hdr(10, 20, PacketType::Data as u8); + ab.encrypt_payload(SecureDatagramDirection::AToB, &mut pkt1) + .unwrap(); + ba.decrypt_payload(SecureDatagramDirection::AToB, &mut pkt1) + .unwrap(); + assert_eq!(pkt1.payload(), plaintext1); + + let plaintext2 = b"hello from b"; + let mut pkt2 = ZCPacket::new_with_payload(plaintext2); + pkt2.fill_peer_manager_hdr(20, 10, PacketType::Data as u8); + ba.encrypt_payload(SecureDatagramDirection::BToA, &mut pkt2) + .unwrap(); + ab.decrypt_payload(SecureDatagramDirection::BToA, &mut pkt2) + .unwrap(); + assert_eq!(pkt2.payload(), plaintext2); + } + + #[test] + fn replay_rejects_far_future_epoch_without_poisoning_window() { + let s = SecureDatagramSession::new( + SecureDatagramSession::new_root_key(), + 1, + 0, + "aes-256-gcm".to_string(), + "aes-256-gcm".to_string(), + ); + + let now = now_ms(); + + assert!(s.check_replay_for_test(0, 1, SecureDatagramDirection::AToB, now)); + assert!(s.check_replay_for_test(0, 2, SecureDatagramDirection::AToB, now)); + + assert!(!s.check_replay_for_test(1000, 1, SecureDatagramDirection::AToB, now)); + + assert!(s.check_replay_for_test(1, 1, SecureDatagramDirection::AToB, now + 1)); + assert!(s.check_replay_for_test(1, 2, SecureDatagramDirection::AToB, now + 2)); + } + + #[test] + fn failed_decrypt_does_not_poison_replay_window() { + let root_key = SecureDatagramSession::new_root_key(); + let sender = SecureDatagramSession::new( + root_key, + 1, + 0, + "aes-256-gcm".to_string(), + "aes-256-gcm".to_string(), + ); + let receiver = SecureDatagramSession::new( + root_key, + 1, + 0, + "aes-256-gcm".to_string(), + "aes-256-gcm".to_string(), + ); + + let mut pkt0 = ZCPacket::new_with_payload(b"pkt0"); + pkt0.fill_peer_manager_hdr(10, 20, PacketType::Data as u8); + sender + .encrypt_payload(SecureDatagramDirection::AToB, &mut pkt0) + .unwrap(); + receiver + .decrypt_payload(SecureDatagramDirection::AToB, &mut pkt0) + .unwrap(); + + let mut forged = ZCPacket::new_with_payload(b"forged"); + forged.fill_peer_manager_hdr(10, 20, PacketType::Data as u8); + sender + .encrypt_payload(SecureDatagramDirection::AToB, &mut forged) + .unwrap(); + + let mut poisoned_nonce = [0u8; StandardAeadTail::NONCE_SIZE]; + poisoned_nonce[..4].copy_from_slice(&0u32.to_be_bytes()); + poisoned_nonce[4..].copy_from_slice(&500u64.to_be_bytes()); + + let payload = forged.mut_payload(); + let nonce_offset = payload.len() - StandardAeadTail::NONCE_SIZE; + payload[nonce_offset..].copy_from_slice(&poisoned_nonce); + + assert!( + receiver + .decrypt_payload(SecureDatagramDirection::AToB, &mut forged) + .is_err() + ); + + let plaintext = b"pkt2"; + let mut pkt2 = ZCPacket::new_with_payload(plaintext); + pkt2.fill_peer_manager_hdr(10, 20, PacketType::Data as u8); + sender + .encrypt_payload(SecureDatagramDirection::AToB, &mut pkt2) + .unwrap(); + receiver + .decrypt_payload(SecureDatagramDirection::AToB, &mut pkt2) + .unwrap(); + assert_eq!(pkt2.payload(), plaintext); + } + + #[test] + fn replay_window_shift_preserves_bits() { + let mut w = ReplayWindow256::default(); + for i in 0..10u64 { + assert!(w.accept(i), "seq {i} should be accepted"); + } + assert_eq!(w.max_seq, 9); + + for i in 0..10u64 { + assert!(!w.accept(i), "seq {i} should be rejected as replay"); + } + + assert!(w.accept(10)); + } + + #[test] + fn replay_window_out_of_order_within_window() { + let mut w = ReplayWindow256::default(); + for i in (0..=20u64).step_by(2) { + assert!(w.accept(i), "seq {i} should be accepted"); + } + for i in (1..=19u64).step_by(2) { + assert!(w.accept(i), "seq {i} should be accepted (out of order)"); + } + for i in 0..=20u64 { + assert!(!w.accept(i), "seq {i} should be rejected as replay"); + } + } + + #[test] + fn sync_root_key_allows_any_epoch_from_remote() { + let s = SecureDatagramSession::new( + SecureDatagramSession::new_root_key(), + 1, + 0, + "aes-256-gcm".to_string(), + "aes-256-gcm".to_string(), + ); + + let root_key = s.root_key(); + let now = now_ms(); + assert!(s.check_replay_for_test(0, 0, SecureDatagramDirection::AToB, now)); + assert!(s.check_replay_for_test(0, 1, SecureDatagramDirection::AToB, now)); + + s.sync_root_key(root_key, 2, 2, true); + + assert!(s.check_replay_for_test(0, 10, SecureDatagramDirection::AToB, now + 1)); + } + + #[test] + fn sync_root_key_keeps_previous_epochs_during_grace_window() { + let s = SecureDatagramSession::new( + SecureDatagramSession::new_root_key(), + 1, + 0, + "aes-256-gcm".to_string(), + "aes-256-gcm".to_string(), + ); + + let root_key = s.root_key(); + let now = now_ms(); + assert!(s.check_replay_for_test(0, 0, SecureDatagramDirection::AToB, now)); + assert!(s.check_replay_for_test(1, 0, SecureDatagramDirection::AToB, now + 1)); + + s.sync_root_key(root_key, 2, 2, true); + + assert!(s.check_replay_for_test(2, 0, SecureDatagramDirection::AToB, now + 2)); + assert!(s.check_replay_for_test(1, 1, SecureDatagramDirection::AToB, now + 3)); + assert!(s.check_replay_for_test(0, 1, SecureDatagramDirection::AToB, now + 4)); + } + + #[test] + fn sync_root_key_expires_previous_epochs_after_grace_window() { + let s = SecureDatagramSession::new( + SecureDatagramSession::new_root_key(), + 1, + 0, + "aes-256-gcm".to_string(), + "aes-256-gcm".to_string(), + ); + + let root_key = s.root_key(); + let now = now_ms(); + assert!(s.check_replay_for_test(0, 0, SecureDatagramDirection::AToB, now)); + assert!(s.check_replay_for_test(1, 0, SecureDatagramDirection::AToB, now + 1)); + + s.sync_root_key(root_key, 2, 2, true); + assert!(s.check_replay_for_test(2, 0, SecureDatagramDirection::AToB, now + 2)); + + assert!(!s.check_replay_for_test( + 0, + 1, + SecureDatagramDirection::AToB, + now + SecureDatagramSession::SYNC_RX_GRACE_AFTER_MS + 3 + )); + } + + #[test] + fn sync_root_key_does_not_preserve_previous_epochs_when_root_key_changes() { + let s = SecureDatagramSession::new( + SecureDatagramSession::new_root_key(), + 1, + 0, + "aes-256-gcm".to_string(), + "aes-256-gcm".to_string(), + ); + + let now = now_ms(); + assert!(s.check_replay_for_test(0, 0, SecureDatagramDirection::AToB, now)); + assert!(s.check_replay_for_test(1, 0, SecureDatagramDirection::AToB, now + 1)); + + s.sync_root_key(SecureDatagramSession::new_root_key(), 2, 2, true); + assert!(s.check_replay_for_test(2, 0, SecureDatagramDirection::AToB, now + 2)); + assert!(!s.check_replay_for_test(1, 1, SecureDatagramDirection::AToB, now + 3)); + } +} diff --git a/easytier/src/peers/tests.rs b/easytier/src/peers/tests.rs index 647859f3..ed626316 100644 --- a/easytier/src/peers/tests.rs +++ b/easytier/src/peers/tests.rs @@ -8,7 +8,7 @@ use crate::{ PeerId, error::Error, global_ctx::{ - NetworkIdentity, + NetworkIdentity, TrustedKeySource, tests::{get_mock_global_ctx, get_mock_global_ctx_with_network}, }, stats_manager::{LabelSet, LabelType, MetricName}, @@ -1315,6 +1315,107 @@ async fn credential_node_group_assignment() { .await; } +#[tokio::test] +async fn credential_node_connected_via_admin_b_trusts_admin_a_groups() { + use crate::proto::acl::{Acl, AclV1, GroupIdentity, GroupInfo}; + + let admin_a = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; + let admin_b = create_mock_peer_manager_secure("net1".to_string(), "secret".to_string()).await; + + let group_declares = vec![GroupIdentity { + group_name: "platform-admin".to_string(), + group_secret: "platform-admin-secret".to_string(), + }]; + admin_a.get_global_ctx().config.set_acl(Some(Acl { + acl_v1: Some(AclV1 { + group: Some(GroupInfo { + declares: group_declares.clone(), + members: vec!["platform-admin".to_string()], + }), + ..Default::default() + }), + })); + admin_b.get_global_ctx().config.set_acl(Some(Acl { + acl_v1: Some(AclV1 { + group: Some(GroupInfo { + declares: group_declares, + members: vec![], + }), + ..Default::default() + }), + })); + + connect_peer_manager(admin_a.clone(), admin_b.clone()).await; + wait_route_appear(admin_a.clone(), admin_b.clone()) + .await + .unwrap(); + + let (_cred_id, cred_secret) = admin_a + .get_global_ctx() + .get_credential_manager() + .generate_credential(vec![], false, vec![], std::time::Duration::from_secs(3600)); + admin_a + .get_global_ctx() + .issue_event(crate::common::global_ctx::GlobalCtxEvent::CredentialChanged); + + let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD + .decode(&cred_secret) + .unwrap() + .try_into() + .unwrap(); + let private = x25519_dalek::StaticSecret::from(privkey_bytes); + let credential_pubkey = x25519_dalek::PublicKey::from(&private).as_bytes().to_vec(); + + wait_for_condition( + || { + let admin_b = admin_b.clone(); + let credential_pubkey = credential_pubkey.clone(); + async move { + admin_b.get_global_ctx().is_pubkey_trusted_with_source( + &credential_pubkey, + "net1", + TrustedKeySource::OspfCredential, + ) + } + }, + Duration::from_secs(10), + ) + .await; + + let cred_c = create_mock_peer_manager_credential("net1".to_string(), &private).await; + connect_peer_manager(cred_c.clone(), admin_b.clone()).await; + + let admin_a_id = admin_a.my_peer_id(); + wait_for_condition( + || { + let cred_c = cred_c.clone(); + async move { + cred_c + .list_routes() + .await + .iter() + .any(|r| r.peer_id == admin_a_id) + } + }, + Duration::from_secs(10), + ) + .await; + + wait_for_condition( + || { + let cred_c = cred_c.clone(); + async move { + cred_c + .get_route() + .get_peer_groups(admin_a_id) + .contains(&"platform-admin".to_string()) + } + }, + Duration::from_secs(10), + ) + .await; +} + /// Minimal test: two secure peers connect and discover each other's route. #[tokio::test] async fn two_secure_peers_route_appear() { diff --git a/easytier/src/tunnel/websocket.rs b/easytier/src/tunnel/websocket.rs index e561c834..a9f58bd4 100644 --- a/easytier/src/tunnel/websocket.rs +++ b/easytier/src/tunnel/websocket.rs @@ -220,6 +220,9 @@ impl WsTunnelConnector { let is_wss = is_wss(&addr)?; let socket_addr = SocketAddr::from_url(addr.clone(), ip_version).await?; let stream = tcp_socket.connect(socket_addr).await?; + if let Err(error) = stream.set_nodelay(true) { + tracing::warn!(?error, "set_nodelay fail in ws connect"); + } let info = TunnelInfo { tunnel_type: addr.scheme().to_owned(), diff --git a/easytier/src/web_client/mod.rs b/easytier/src/web_client/mod.rs index c487d13f..ee478e1c 100644 --- a/easytier/src/web_client/mod.rs +++ b/easytier/src/web_client/mod.rs @@ -126,7 +126,7 @@ impl WebClient { } }; - if support_encryption { + if support_encryption && security::web_secure_tunnel_supported() { log::info!("Server supports encryption, reconnecting with secure tunnel"); drop(session); @@ -159,6 +159,23 @@ impl WebClient { continue; } + if support_encryption { + if secure_mode { + connected.store(false, Ordering::Release); + let wait = 1; + log::warn!( + "secure-mode enabled but local build lacks aes-gcm support for web secure tunnel, retrying in {} seconds...", + wait + ); + tokio::time::sleep(std::time::Duration::from_secs(wait)).await; + continue; + } + + log::warn!( + "Server supports encryption but local build lacks aes-gcm support for web secure tunnel, falling back to legacy tunnel" + ); + } + if secure_mode { connected.store(false, Ordering::Release); let wait = 1; diff --git a/easytier/src/web_client/security.rs b/easytier/src/web_client/security.rs index 30020024..2916624a 100644 --- a/easytier/src/web_client/security.rs +++ b/easytier/src/web_client/security.rs @@ -1,11 +1,12 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; -use bytes::BytesMut; use futures::{SinkExt, StreamExt}; -use snow::{Builder, TransportState, params::NoiseParams}; +use snow::{Builder, params::NoiseParams}; use crate::{ + common::config::EncryptionAlgorithm, + peers::secure_datagram::{SecureDatagramDirection, SecureDatagramSession}, proto::common::TunnelInfo, tunnel::{ SplitTunnel, StreamItem, Tunnel, TunnelError, ZCPacketSink, ZCPacketStream, @@ -17,6 +18,11 @@ use crate::{ const NOISE_MAGIC: &[u8] = b"ET_WEB_NOISE_V1:"; const NOISE_PROLOGUE: &[u8] = b"easytier-webclient-noise-v1"; const NOISE_PATTERN: &str = "Noise_NN_25519_ChaChaPoly_SHA256"; +const WEB_SECURE_CIPHER_ALGORITHM: &str = "aes-gcm"; +const WEB_SESSION_GENERATION: u32 = 1; +const WEB_INITIAL_EPOCH: u32 = 0; +const WEB_SECURE_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(3); +const WEB_SECURE_ACCEPT_TIMEOUT: Duration = WEB_SECURE_HANDSHAKE_TIMEOUT; struct RawSplitTunnel { info: Option, @@ -50,24 +56,42 @@ impl Tunnel for RawSplitTunnel { } } -struct NoiseTunnelFilter { - transport: Arc>, +#[derive(Clone, Copy)] +enum SecureTunnelRole { + Initiator, + Responder, } -impl TunnelFilter for NoiseTunnelFilter { +impl SecureTunnelRole { + fn send_dir(self) -> SecureDatagramDirection { + match self { + Self::Initiator => SecureDatagramDirection::AToB, + Self::Responder => SecureDatagramDirection::BToA, + } + } + + fn recv_dir(self) -> SecureDatagramDirection { + match self { + Self::Initiator => SecureDatagramDirection::BToA, + Self::Responder => SecureDatagramDirection::AToB, + } + } +} + +struct SecureDatagramTunnelFilter { + session: Arc, + role: SecureTunnelRole, +} + +impl TunnelFilter for SecureDatagramTunnelFilter { type FilterOutput = (); fn before_send(&self, data: ZCPacket) -> Option { - let plain = data.tunnel_payload(); - let mut encrypted = vec![0u8; plain.len() + 64]; - let len = self - .transport - .lock() - .unwrap() - .write_message(plain, &mut encrypted) - .ok()?; - let mut packet = ZCPacket::new_with_payload(&encrypted[..len]); + let mut packet = ZCPacket::new_with_payload(data.tunnel_payload()); packet.fill_peer_manager_hdr(0, 0, PacketType::Data as u8); + self.session + .encrypt_payload(self.role.send_dir(), &mut packet) + .ok()?; Some(packet) } @@ -76,23 +100,24 @@ impl TunnelFilter for NoiseTunnelFilter { Ok(v) => v, Err(e) => return Some(Err(e)), }; - let cipher = packet.payload(); - let mut plain = vec![0u8; cipher.len() + 64]; - let len = match self - .transport - .lock() + + let mut cipher = ZCPacket::new_with_payload(packet.payload()); + cipher.fill_peer_manager_hdr(0, 0, PacketType::Data as u8); + cipher + .mut_peer_manager_header() .unwrap() - .read_message(cipher, &mut plain) + .set_encrypted(true); + if let Err(e) = self + .session + .decrypt_payload(self.role.recv_dir(), &mut cipher) { - Ok(v) => v, - Err(e) => { - return Some(Err(TunnelError::InvalidPacket(format!( - "noise decrypt failed: {e}" - )))); - } - }; + return Some(Err(TunnelError::InvalidPacket(format!( + "secure datagram decrypt failed: {e}" + )))); + } + Some(Ok(ZCPacket::new_from_buf( - BytesMut::from(&plain[..len]), + cipher.payload_bytes(), ZCPacketType::DummyTunnel, ))) } @@ -117,24 +142,50 @@ fn decode_noise_payload(payload: &[u8]) -> Option<&[u8]> { payload.strip_prefix(NOISE_MAGIC) } +pub fn web_secure_tunnel_supported() -> bool { + WEB_SECURE_CIPHER_ALGORITHM + .parse::() + .is_ok() +} + +fn web_secure_cipher_algorithm() -> Result<&'static str, TunnelError> { + if !web_secure_tunnel_supported() { + return Err(TunnelError::InternalError(format!( + "web secure tunnel requires {WEB_SECURE_CIPHER_ALGORITHM} support" + ))); + } + Ok(WEB_SECURE_CIPHER_ALGORITHM) +} + +fn new_web_secure_session(root_key: [u8; 32], algorithm: &str) -> Arc { + let algo = algorithm.to_string(); + Arc::new(SecureDatagramSession::new( + root_key, + WEB_SESSION_GENERATION, + WEB_INITIAL_EPOCH, + algo.clone(), + algo, + )) +} + fn wrap_secure_tunnel( info: Option, stream: std::pin::Pin>, sink: std::pin::Pin>, - transport: TransportState, + session: Arc, + role: SecureTunnelRole, ) -> Box { let raw = RawSplitTunnel::new(info, stream, sink); Box::new(TunnelWithFilter::new( raw, - NoiseTunnelFilter { - transport: Arc::new(Mutex::new(transport)), - }, + SecureDatagramTunnelFilter { session, role }, )) } pub async fn upgrade_client_tunnel( tunnel: Box, ) -> Result, TunnelError> { + let web_cipher_algorithm = web_secure_cipher_algorithm()?; let info = tunnel.info(); let (mut stream, mut sink) = tunnel.split(); @@ -156,19 +207,32 @@ pub async fn upgrade_client_tunnel( ))) .await?; - let msg2_packet = stream.next().await.ok_or(TunnelError::Shutdown)??; + let msg2_packet = match tokio::time::timeout(WEB_SECURE_HANDSHAKE_TIMEOUT, stream.next()).await + { + Ok(Some(Ok(packet))) => packet, + Ok(Some(Err(error))) => return Err(error), + Ok(None) => return Err(TunnelError::Shutdown), + Err(error) => return Err(error.into()), + }; let msg2_cipher = decode_noise_payload(msg2_packet.payload()) .ok_or_else(|| TunnelError::InvalidPacket("invalid noise msg2 magic".to_string()))?; - let mut msg2 = vec![0u8; 1024]; - state - .read_message(msg2_cipher, &mut msg2) + let mut root_key_buf = [0u8; 32]; + let root_key_len = state + .read_message(msg2_cipher, &mut root_key_buf) .map_err(|e| TunnelError::InvalidPacket(format!("read noise msg2 failed: {e}")))?; + if root_key_len != root_key_buf.len() { + return Err(TunnelError::InvalidPacket(format!( + "invalid web secure root key len: {root_key_len}" + ))); + } - let transport = state - .into_transport_mode() - .map_err(|e| TunnelError::InternalError(format!("switch transport mode failed: {e}")))?; - - Ok(wrap_secure_tunnel(info, stream, sink, transport)) + Ok(wrap_secure_tunnel( + info, + stream, + sink, + new_web_secure_session(root_key_buf, web_cipher_algorithm), + SecureTunnelRole::Initiator, + )) } pub async fn accept_or_upgrade_server_tunnel( @@ -179,7 +243,7 @@ pub async fn accept_or_upgrade_server_tunnel( let mut stream = stream; let mut sink = sink; - let first_packet = match tokio::time::timeout(Duration::from_secs(1), stream.next()).await { + let first_packet = match tokio::time::timeout(WEB_SECURE_ACCEPT_TIMEOUT, stream.next()).await { Ok(Some(Ok(packet))) => packet, Ok(Some(Err(error))) => return Err(error), Ok(None) => return Err(TunnelError::Shutdown), @@ -197,6 +261,7 @@ pub async fn accept_or_upgrade_server_tunnel( false, )); }; + let web_cipher_algorithm = web_secure_cipher_algorithm()?; let params: NoiseParams = NOISE_PATTERN .parse() @@ -212,18 +277,81 @@ pub async fn accept_or_upgrade_server_tunnel( .read_message(msg1_cipher, &mut msg1) .map_err(|e| TunnelError::InvalidPacket(format!("read noise msg1 failed: {e}")))?; + let root_key = SecureDatagramSession::new_root_key(); let mut msg2 = vec![0u8; 1024]; let msg2_len = state - .write_message(&[], &mut msg2) + .write_message(&root_key, &mut msg2) .map_err(|e| TunnelError::InternalError(format!("write noise msg2 failed: {e}")))?; sink.send(pack_control_packet(&encode_noise_payload( &msg2[..msg2_len], ))) .await?; - let transport = state - .into_transport_mode() - .map_err(|e| TunnelError::InternalError(format!("switch transport mode failed: {e}")))?; - - Ok((wrap_secure_tunnel(info, stream, sink, transport), true)) + Ok(( + wrap_secure_tunnel( + info, + stream, + sink, + new_web_secure_session(root_key, web_cipher_algorithm), + SecureTunnelRole::Responder, + ), + true, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tunnel::ring::create_ring_tunnel_pair; + + #[test] + fn web_secure_cipher_algorithm_matches_support_flag() { + let result = web_secure_cipher_algorithm(); + if web_secure_tunnel_supported() { + assert_eq!(result.unwrap(), WEB_SECURE_CIPHER_ALGORITHM); + } else { + assert!(matches!(result, Err(TunnelError::InternalError(_)))); + } + } + + #[test] + fn web_secure_session_uses_pinned_cipher_algorithm() { + if !web_secure_tunnel_supported() { + return; + } + + let session = new_web_secure_session( + SecureDatagramSession::new_root_key(), + web_secure_cipher_algorithm().unwrap(), + ); + session + .check_encrypt_algo_same(WEB_SECURE_CIPHER_ALGORITHM, WEB_SECURE_CIPHER_ALGORITHM) + .unwrap(); + } + + #[tokio::test] + async fn upgrade_client_tunnel_times_out_when_server_never_replies() { + let (server_tunnel, client_tunnel) = create_ring_tunnel_pair(); + let _server_tunnel = server_tunnel; + + let err = upgrade_client_tunnel(client_tunnel).await.unwrap_err(); + assert!(matches!(err, TunnelError::Timeout(_))); + } + + #[tokio::test] + async fn accept_secure_tunnel_after_short_client_delay() { + let (server_tunnel, client_tunnel) = create_ring_tunnel_pair(); + + let server_task = + tokio::spawn(async move { accept_or_upgrade_server_tunnel(server_tunnel).await }); + + tokio::time::sleep(Duration::from_millis(1500)).await; + + let client_task = tokio::spawn(async move { upgrade_client_tunnel(client_tunnel).await }); + + let (server_res, client_res) = tokio::join!(server_task, client_task); + let (_, secure) = server_res.unwrap().unwrap(); + assert!(secure); + assert!(client_res.unwrap().is_ok()); + } } From 40c6de8e318d9f4dde148ec18f8ebc0512eae3bc Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Sun, 19 Apr 2026 10:39:04 +0800 Subject: [PATCH 04/26] fix(core): restrict implicit config merge to explicit config files (#2127) --- easytier/src/core.rs | 48 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/easytier/src/core.rs b/easytier/src/core.rs index 634f36bb..f57daf14 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -788,20 +788,33 @@ impl Cli { } impl NetworkOptions { - fn can_merge(&self, cfg: &TomlConfigLoader, config_file_count: usize) -> bool { + fn can_merge( + &self, + cfg: &TomlConfigLoader, + source: ConfigSource, + explicit_config_file_count: usize, + config_dir_file_count: usize, + ) -> bool { if (*self) == NetworkOptions::default() { return false; } - if config_file_count == 1 { + + if source == ConfigSource::CliConfigFile + && explicit_config_file_count == 1 + && config_dir_file_count == 0 + { return true; } + let Some(network_name) = &self.network_name else { return false; }; - if cfg.get_network_identity().network_name == *network_name { - return true; + + if source == ConfigSource::ConfigDir { + return cfg.get_network_identity().network_name == *network_name; } - false + + cfg.get_network_identity().network_name == *network_name } fn merge_into(&self, cfg: &TomlConfigLoader) -> anyhow::Result<()> { @@ -1122,6 +1135,12 @@ impl NetworkOptions { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConfigSource { + CliConfigFile, + ConfigDir, +} + impl LoggingConfigLoader for &LoggingOptions { fn get_console_logger_config(&self) -> ConsoleLoggerConfig { ConsoleLoggerConfig { @@ -1304,8 +1323,13 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { None }; + let explicit_config_file_count = cli.config_file.as_ref().map_or(0, |files| files.len()); + let mut config_dir_file_count = 0; let mut config_files = if let Some(v) = cli.config_file { - v.clone() + v.iter() + .cloned() + .map(|path| (path, ConfigSource::CliConfigFile)) + .collect() } else { vec![] }; @@ -1326,7 +1350,8 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { if ext != "toml" { continue; } - config_files.push(path); + config_dir_file_count += 1; + config_files.push((path, ConfigSource::ConfigDir)); } } let config_file_count = config_files.len(); @@ -1339,7 +1364,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { cli.network_options.network_name.is_some() } }; - for config_file in config_files { + for (config_file, source) in config_files { let (cfg, mut control) = load_config_from_file( &config_file, cli.config_dir.as_ref(), @@ -1347,7 +1372,12 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { ) .await?; - if cli.network_options.can_merge(&cfg, config_file_count) { + if cli.network_options.can_merge( + &cfg, + source, + explicit_config_file_count, + config_dir_file_count, + ) { cli.network_options .merge_into(&cfg) .with_context(|| format!("failed to merge config from cli: {:?}", config_file))?; From efc043abbb43cfab1930e433afd5932dd865d1ac Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Sun, 19 Apr 2026 16:49:45 +0800 Subject: [PATCH 05/26] bump version to v2.6.1 (#2129) --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 2 +- Cargo.lock | 6 +++--- README.md | 6 +++--- README_CN.md | 6 +++--- easytier-contrib/easytier-magisk/module.prop | 2 +- easytier-gui/package.json | 2 +- easytier-gui/src-tauri/Cargo.toml | 2 +- easytier-gui/src-tauri/tauri.conf.json | 2 +- easytier-web/Cargo.toml | 2 +- easytier/Cargo.toml | 2 +- script/install.ps1 | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 823da5c1..5297c288 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,7 +11,7 @@ on: image_tag: description: 'Tag for this image build' type: string - default: 'v2.6.0' + default: 'v2.6.1' required: true mark_latest: description: 'Mark this image as latest' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2fa2eecb..e805b45f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ on: version: description: 'Version for this release' type: string - default: 'v2.6.0' + default: 'v2.6.1' required: true make_latest: description: 'Mark this release as latest' diff --git a/Cargo.lock b/Cargo.lock index 0e9a6104..63cb89de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2186,7 +2186,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "easytier" -version = "2.6.0" +version = "2.6.1" dependencies = [ "aes-gcm", "anyhow", @@ -2360,7 +2360,7 @@ dependencies = [ [[package]] name = "easytier-gui" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "async-trait", @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "easytier-web" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "async-trait", diff --git a/README.md b/README.md index bab53f64..00df972f 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,9 @@ After successful execution, you can check the network status using `easytier-cli ```text | ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version | | ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- | -| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.0-70e69a38~ | -| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.0-70e69a38~ | -| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.0-70e69a38~ | +| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.1-70e69a38~ | +| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.1-70e69a38~ | +| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.1-70e69a38~ | ``` You can test connectivity between nodes: diff --git a/README_CN.md b/README_CN.md index 9fd6af7b..26142595 100644 --- a/README_CN.md +++ b/README_CN.md @@ -108,9 +108,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享 ```text | ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version | | ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- | -| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.0-70e69a38~ | -| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.0-70e69a38~ | -| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.0-70e69a38~ | +| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.1-70e69a38~ | +| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.1-70e69a38~ | +| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.1-70e69a38~ | ``` 您可以测试节点之间的连通性: diff --git a/easytier-contrib/easytier-magisk/module.prop b/easytier-contrib/easytier-magisk/module.prop index 1818e3bd..852e02fe 100644 --- a/easytier-contrib/easytier-magisk/module.prop +++ b/easytier-contrib/easytier-magisk/module.prop @@ -1,6 +1,6 @@ id=easytier_magisk name=EasyTier_Magisk -version=v2.6.0 +version=v2.6.1 versionCode=1 author=EasyTier description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier) diff --git a/easytier-gui/package.json b/easytier-gui/package.json index 354c0aa2..db26ee2c 100644 --- a/easytier-gui/package.json +++ b/easytier-gui/package.json @@ -1,7 +1,7 @@ { "name": "easytier-gui", "type": "module", - "version": "2.6.0", + "version": "2.6.1", "private": true, "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4", "scripts": { diff --git a/easytier-gui/src-tauri/Cargo.toml b/easytier-gui/src-tauri/Cargo.toml index 3dfb9498..38cb414f 100644 --- a/easytier-gui/src-tauri/Cargo.toml +++ b/easytier-gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "easytier-gui" -version = "2.6.0" +version = "2.6.1" description = "EasyTier GUI" authors = ["you"] edition.workspace = true diff --git a/easytier-gui/src-tauri/tauri.conf.json b/easytier-gui/src-tauri/tauri.conf.json index e55ecdab..516457f0 100644 --- a/easytier-gui/src-tauri/tauri.conf.json +++ b/easytier-gui/src-tauri/tauri.conf.json @@ -17,7 +17,7 @@ "createUpdaterArtifacts": false }, "productName": "easytier-gui", - "version": "2.6.0", + "version": "2.6.1", "identifier": "com.kkrainbow.easytier", "plugins": { "shell": { diff --git a/easytier-web/Cargo.toml b/easytier-web/Cargo.toml index b681dc5b..1d4ab306 100644 --- a/easytier-web/Cargo.toml +++ b/easytier-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "easytier-web" -version = "2.6.0" +version = "2.6.1" edition.workspace = true description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server." diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 9e2d0789..41801677 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -3,7 +3,7 @@ name = "easytier" description = "A full meshed p2p VPN, connecting all your devices in one network with one command." homepage = "https://github.com/EasyTier/EasyTier" repository = "https://github.com/EasyTier/EasyTier" -version = "2.6.0" +version = "2.6.1" edition.workspace = true rust-version.workspace = true authors = ["kkrainbow"] diff --git a/script/install.ps1 b/script/install.ps1 index e56b1bf6..c909e04a 100644 --- a/script/install.ps1 +++ b/script/install.ps1 @@ -7,7 +7,7 @@ Copies binaries to the install directory and updates the system PATH. .PARAMETER Version - Target version: "latest", "stable", or a specific tag like "v2.6.0". + Target version: "latest", "stable", or a specific tag like "v2.6.1". Default: "latest" .PARAMETER InstallDir @@ -16,7 +16,7 @@ .EXAMPLE .\install.ps1 - .\install.ps1 -Version v2.6.0 + .\install.ps1 -Version v2.6.1 .\install.ps1 -InstallDir "C:\EasyTier" .NOTES From f63054e937d1a83e981817ebf148b6ea8c58dfd1 Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Sun, 19 Apr 2026 19:06:37 +0800 Subject: [PATCH 06/26] fix: resolve Android APK version fallback to 1.0 on CI (#2131) --- .../gen/android/app/build.gradle.kts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/easytier-gui/src-tauri/gen/android/app/build.gradle.kts b/easytier-gui/src-tauri/gen/android/app/build.gradle.kts index f55d32de..c160d076 100644 --- a/easytier-gui/src-tauri/gen/android/app/build.gradle.kts +++ b/easytier-gui/src-tauri/gen/android/app/build.gradle.kts @@ -1,5 +1,6 @@ import java.util.Properties import java.io.FileInputStream +import groovy.json.JsonSlurper plugins { id("com.android.application") @@ -14,6 +15,35 @@ val tauriProperties = Properties().apply { } } +val versionPattern = Regex("""^(\d+)\.(\d+)\.(\d+)$""") + +val tauriVersionName = tauriProperties.getProperty("tauri.android.versionName")?.ifBlank { null } ?: run { + val tauriConfFile = file("../../../tauri.conf.json") + check(tauriConfFile.exists()) { "Missing tauri.conf.json at ${tauriConfFile.path}" } + + val tauriConf = tauriConfFile.reader(Charsets.UTF_8).use { JsonSlurper().parse(it) as? Map<*, *> } + ?: error("Failed to parse ${tauriConfFile.path} as a JSON object") + tauriConf["version"] as? String + ?: error("Missing string field \"version\" in ${tauriConfFile.path}") +} + +val tauriVersionMatch = versionPattern.matchEntire(tauriVersionName) + ?: error("Android version must use x.y.z format, but got \"$tauriVersionName\"") + +val tauriVersionCode = if (tauriProperties.getProperty("tauri.android.versionName")?.ifBlank { null } != null) { + val versionCodeProp = tauriProperties.getProperty("tauri.android.versionCode") + if (versionCodeProp != null) { + versionCodeProp.toIntOrNull() + ?: error("Property \"tauri.android.versionCode\" must be an integer, but got \"$versionCodeProp\"") + } else { + val (major, minor, patch) = tauriVersionMatch.destructured + major.toInt() * 1_000_000 + minor.toInt() * 1_000 + patch.toInt() + } +} else { + val (major, minor, patch) = tauriVersionMatch.destructured + major.toInt() * 1_000_000 + minor.toInt() * 1_000 + patch.toInt() +} + android { compileSdk = 34 namespace = "com.kkrainbow.easytier" @@ -22,8 +52,8 @@ android { applicationId = "com.kkrainbow.easytier" minSdk = 24 targetSdk = 34 - versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() - versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + versionCode = tauriVersionCode + versionName = tauriVersionName } signingConfigs { create("release") { @@ -82,4 +112,4 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") } -apply(from = "tauri.build.gradle.kts") \ No newline at end of file +apply(from = "tauri.build.gradle.kts") From d7a5fb8d66852bfb637b5992c0d0dcd39090ca28 Mon Sep 17 00:00:00 2001 From: Luna Yao <40349250+ZnqbuZ@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:46:26 +0200 Subject: [PATCH 07/26] remove --no-deps from lock check (#2134) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45f345f0..a01b4f1a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,7 @@ jobs: - name: Check Cargo.lock is up to date run: | - if ! cargo metadata --format-version 1 --locked --no-deps > /dev/null; then + if ! cargo metadata --format-version 1 --locked > /dev/null; then echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock." exit 1 fi From 0091a535d55692bdfb8b514d721281b864b02e27 Mon Sep 17 00:00:00 2001 From: Luna Yao <40349250+ZnqbuZ@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:40:21 +0200 Subject: [PATCH 08/26] use mimalloc for FreeBSD (#2144) --- .github/workflows/core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 53c18618..a817bff1 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -178,7 +178,7 @@ jobs: BUILD=zigbuild fi - if [[ "$TARGET" =~ ^(riscv64|loongarch64|aarch64).*$ || "$TARGET" =~ windows ]]; then + if [[ "$TARGET" =~ ^(riscv64|loongarch64|aarch64).*$ || "$TARGET" =~ (freebsd|windows) ]]; then FEATURES="mimalloc" else FEATURES="jemalloc" From f4319c4d4feb72bb2c4950f35e76de5cc631c5e0 Mon Sep 17 00:00:00 2001 From: Luna Yao <40349250+ZnqbuZ@users.noreply.github.com> Date: Tue, 21 Apr 2026 04:08:27 +0200 Subject: [PATCH 09/26] ci(test): always check everything (#2142) * ci(test): always check everything * move Cargo.lock check to the last step --- .github/workflows/test.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a01b4f1a..82e4a207 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,23 +58,26 @@ jobs: - uses: taiki-e/install-action@cargo-hack - - name: Check Cargo.lock is up to date - run: | - if ! cargo metadata --format-version 1 --locked > /dev/null; then - echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock." - exit 1 - fi - - name: Check formatting + if: ${{ !cancelled() }} run: cargo fmt --all -- --check - name: Check Clippy + if: ${{ !cancelled() }} run: cargo clippy --all-targets --features full --all -- -D warnings - name: Check features if: ${{ !cancelled() }} run: cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose + - name: Check Cargo.lock is up to date + if: ${{ !cancelled() }} + run: | + if ! cargo metadata --format-version 1 --locked > /dev/null; then + echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock." + exit 1 + fi + pre-test: name: Build test runs-on: ubuntu-latest From 5cd0a3e846ef44e5c68b94fad2a571f346901f70 Mon Sep 17 00:00:00 2001 From: Debugger Chen <278290457@qq.com> Date: Tue, 21 Apr 2026 17:19:04 +0800 Subject: [PATCH 10/26] feat: add upnp support (#1449) --- .github/workflows/test.yml | 4 + Cargo.lock | 514 ++++- easytier/Cargo.toml | 2 + easytier/locales/app.yml | 3 + easytier/src/common/config.rs | 1 + easytier/src/common/global_ctx.rs | 5 + easytier/src/common/mod.rs | 1 + easytier/src/common/stun.rs | 21 +- easytier/src/common/upnp.rs | 767 +++++++ easytier/src/connector/direct.rs | 151 +- easytier/src/connector/tcp_hole_punch.rs | 7 + .../src/connector/udp_hole_punch/common.rs | 270 ++- easytier/src/connector/udp_hole_punch/cone.rs | 33 +- easytier/src/connector/udp_hole_punch/mod.rs | 10 +- .../connector/udp_hole_punch/sym_to_cone.rs | 5 +- easytier/src/core.rs | 14 +- easytier/src/instance_manager.rs | 15 + easytier/src/launcher.rs | 6 + easytier/src/peers/peer_rpc_service.rs | 20 +- easytier/src/proto/api_manage.proto | 1 + easytier/src/proto/common.proto | 1 + easytier/src/proto/peer_rpc.proto | 5 +- easytier/src/tests/mod.rs | 3 + easytier/src/tests/upnp_test.rs | 1952 +++++++++++++++++ easytier/src/tunnel/packet_def.rs | 11 +- easytier/src/tunnel/udp.rs | 120 +- 26 files changed, 3707 insertions(+), 235 deletions(-) create mode 100644 easytier/src/common/upnp.rs create mode 100644 easytier/src/tests/upnp_test.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82e4a207..599f11ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,6 +128,10 @@ jobs: - name: Setup tools for test run: sudo apt install bridge-utils + - name: Setup upnpd for test + run: | + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y miniupnpd miniupnpd-iptables iptables - name: Setup system for test run: | diff --git a/Cargo.lock b/Cargo.lock index 63cb89de..067526d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,7 +32,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.13", ] [[package]] @@ -237,7 +237,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.13", "password-hash", ] @@ -353,7 +353,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -403,7 +403,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -420,7 +420,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -476,6 +476,18 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + [[package]] name = "auto_impl" version = "1.2.1" @@ -484,7 +496,7 @@ checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -669,7 +681,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -680,7 +692,7 @@ checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -765,7 +777,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -857,7 +869,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -906,7 +918,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1184,7 +1196,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.13", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -1194,7 +1217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1299,7 +1322,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1527,6 +1550,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.2.1" @@ -1666,7 +1698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1676,7 +1708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1711,7 +1743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.13", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1728,7 +1760,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1768,7 +1800,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1781,7 +1813,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1792,7 +1824,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1803,7 +1835,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1883,7 +1915,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1903,7 +1935,7 @@ checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1946,7 +1978,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1967,7 +1999,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1977,7 +2009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -1990,7 +2022,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -2012,7 +2044,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.87", + "syn 2.0.117", "unicode-xid", ] @@ -2089,7 +2121,18 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", ] [[package]] @@ -2112,7 +2155,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -2240,6 +2283,7 @@ dependencies = [ "humansize", "humantime-serde", "idna 1.0.3", + "igd-next", "indoc", "itertools 0.14.0", "kcp-sys", @@ -2247,6 +2291,7 @@ dependencies = [ "maplit", "mimalloc", "multimap", + "natpmp", "netlink-packet-core", "netlink-packet-route 0.21.0", "netlink-packet-utils", @@ -2676,7 +2721,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -2697,7 +2742,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -2827,7 +2872,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -2950,7 +2995,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -3089,7 +3134,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -3313,11 +3358,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.2.0", "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -3377,7 +3436,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -3414,7 +3473,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -3528,7 +3587,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -4121,9 +4180,15 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -4161,6 +4226,26 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "igd-next" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.10.1", + "tokio", + "url", + "xmltree", +] + [[package]] name = "ignore" version = "0.4.22" @@ -4271,7 +4356,7 @@ checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -4578,6 +4663,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libappindicator" version = "0.9.0" @@ -4826,7 +4917,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -4986,7 +5077,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -5080,6 +5171,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "natpmp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77366fa8ce34e2e1322dd97da65f11a62f451bd3daae8be6993c00800f61dd07" +dependencies = [ + "async-trait", + "cc", + "netdev", + "tokio", +] + [[package]] name = "ndk" version = "0.9.0" @@ -5110,6 +5213,23 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "netdev" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f901362e84cd407be6f8cd9d3a46bccf09136b095792785401ea7d283c79b91d" +dependencies = [ + "dlopen2 0.5.0", + "ipnet", + "libc", + "netlink-packet-core", + "netlink-packet-route 0.17.1", + "netlink-sys", + "once_cell", + "system-configuration", + "windows-sys 0.52.0", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -5447,7 +5567,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -5758,7 +5878,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -5879,7 +5999,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -6191,7 +6311,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -6238,7 +6358,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -6351,7 +6471,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -6443,7 +6563,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.13", "opaque-debug", "universal-hash", ] @@ -6455,7 +6575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.13", "opaque-debug", "universal-hash", ] @@ -6531,7 +6651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -6614,7 +6734,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -6640,7 +6760,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", "version_check", "yansi", ] @@ -6671,7 +6791,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.87", + "syn 2.0.117", "tempfile", ] @@ -6685,7 +6805,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -6718,7 +6838,7 @@ checksum = "f4fce6b22f15cc8d8d400a2b98ad29202b33bd56c7d9ddd815bc803a807ecb65" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -6909,6 +7029,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -6960,6 +7086,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -7017,6 +7154,12 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_distr" version = "0.2.2" @@ -7136,7 +7279,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -7415,7 +7558,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.87", + "syn 2.0.117", "unicode-ident", ] @@ -7439,7 +7582,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.87", + "syn 2.0.117", "walkdir", ] @@ -7482,7 +7625,7 @@ dependencies = [ "serde", "serde_json", "serde_yml", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -7754,7 +7897,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -7785,7 +7928,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -7845,7 +7988,7 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.87", + "syn 2.0.117", "unicode-ident", ] @@ -7908,7 +8051,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", "thiserror 2.0.11", ] @@ -7932,7 +8075,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8067,7 +8210,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8078,7 +8221,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8135,7 +8278,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8196,7 +8339,7 @@ dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8251,7 +8394,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8273,7 +8416,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8308,7 +8451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.13", "digest", ] @@ -8319,7 +8462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.13", "digest", ] @@ -8646,7 +8789,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8669,7 +8812,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.87", + "syn 2.0.117", "tokio", "url", ] @@ -8869,7 +9012,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -8917,9 +9060,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -8943,7 +9086,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -9040,7 +9183,7 @@ dependencies = [ "core-graphics", "crossbeam-channel", "dispatch2", - "dlopen2", + "dlopen2 0.8.2", "dpi", "gdkwayland-sys", "gdkx11-sys", @@ -9074,7 +9217,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -9181,7 +9324,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.87", + "syn 2.0.117", "tauri-utils", "thiserror 2.0.11", "time", @@ -9199,7 +9342,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", "tauri-codegen", "tauri-utils", ] @@ -9491,7 +9634,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -9502,7 +9645,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -9655,7 +9798,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -9901,7 +10044,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -10076,7 +10219,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -10257,7 +10400,7 @@ checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -10467,7 +10610,7 @@ checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -10497,7 +10640,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -10584,6 +10727,24 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasite" version = "0.1.0" @@ -10633,7 +10794,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -10646,6 +10807,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -10659,6 +10842,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.8.0", + "hashbrown 0.15.3", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.15" @@ -10861,7 +11056,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -11109,7 +11304,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -11120,7 +11315,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -11131,7 +11326,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -11142,7 +11337,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -11622,6 +11817,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -11631,6 +11852,74 @@ dependencies = [ "bitflags 2.8.0", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.8.0", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "wl-clipboard-rs" version = "0.9.3" @@ -11771,6 +12060,15 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yansi" version = "1.0.1" @@ -11806,7 +12104,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", "synstructure", ] @@ -11853,7 +12151,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -11897,7 +12195,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -11908,7 +12206,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -11928,7 +12226,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", "synstructure", ] @@ -11949,7 +12247,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -11971,7 +12269,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", ] [[package]] @@ -12090,7 +12388,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.117", "zvariant_utils", ] @@ -12103,6 +12401,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.87", + "syn 2.0.117", "winnow 0.7.15", ] diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 41801677..70127b62 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -252,6 +252,8 @@ shellexpand = "3.1.1" # for fake tcp flume = { version = "0.12", optional = true } +igd-next = { version = "0.17.0", features = ["aio_tokio"] } +natpmp = "0.5.0" [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies] machine-uid = "0.5.3" diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index bc6cb7d3..6139e56c 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -172,6 +172,9 @@ core_clap: disable_sym_hole_punching: en: "if true, disable udp nat hole punching for symmetric nat (NAT4), which is based on birthday attack and may be blocked by ISP." zh-CN: "如果为true,则禁用基于生日攻击的对称NAT (NAT4) UDP 打洞功能,该打洞方式可能会被运营商封锁" + disable_upnp: + en: "disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default" + zh-CN: "禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启" relay_all_peer_rpc: en: "relay all peer rpc packets, even if the peer is not in the relay network whitelist. this can help peers not in relay network whitelist to establish p2p connection." zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。" diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 173ead6f..3969dd0a 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -69,6 +69,7 @@ pub fn gen_default_flags() -> Flags { quic_listen_port: u32::MAX, need_p2p: false, instance_recv_bps_limit: u64::MAX, + disable_upnp: false, } } diff --git a/easytier/src/common/global_ctx.rs b/easytier/src/common/global_ctx.rs index b6af900b..a0b0e6ad 100644 --- a/easytier/src/common/global_ctx.rs +++ b/easytier/src/common/global_ctx.rs @@ -53,6 +53,11 @@ pub enum GlobalCtxEvent { ListenerAcceptFailed(url::Url, String), // (url, error message) ConnectionAccepted(String, String), // (local url, remote url) ConnectionError(String, String, String), // (local url, remote url, error message) + ListenerPortMappingEstablished { + local_listener: url::Url, + mapped_listener: url::Url, + backend: String, + }, Connecting(url::Url), ConnectError(String, String, String), // (dst, ip version, error message) diff --git a/easytier/src/common/mod.rs b/easytier/src/common/mod.rs index 1b8e303c..8d066dde 100644 --- a/easytier/src/common/mod.rs +++ b/easytier/src/common/mod.rs @@ -31,6 +31,7 @@ pub mod stun; pub mod stun_codec_ext; pub mod token_bucket; pub mod tracing_rolling_appender; +pub mod upnp; pub fn get_logger_timer( format: F, diff --git a/easytier/src/common/stun.rs b/easytier/src/common/stun.rs index 94a36479..a4a8a397 100644 --- a/easytier/src/common/stun.rs +++ b/easytier/src/common/stun.rs @@ -276,7 +276,6 @@ impl StunClient { let stun_host = self.stun_server; // repeat req in case of packet loss let mut tids = vec![]; - for _ in 0..self.req_repeat { let tid = rand::random::(); // let tid = 1; @@ -912,6 +911,10 @@ impl TcpNatTypeDetector { pub trait StunInfoCollectorTrait: Send + Sync { fn get_stun_info(&self) -> StunInfo; async fn get_udp_port_mapping(&self, local_port: u16) -> Result; + async fn get_udp_port_mapping_with_socket( + &self, + udp: Arc, + ) -> Result; async fn get_tcp_port_mapping(&self, local_port: u16) -> Result; } @@ -975,6 +978,14 @@ impl StunInfoCollectorTrait for StunInfoCollector { } async fn get_udp_port_mapping(&self, local_port: u16) -> Result { + let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", local_port)).await?); + self.get_udp_port_mapping_with_socket(udp).await + } + + async fn get_udp_port_mapping_with_socket( + &self, + udp: Arc, + ) -> Result { self.start_stun_routine(); let mut stun_servers = self @@ -1000,7 +1011,6 @@ impl StunInfoCollectorTrait for StunInfoCollector { return Err(Error::NotFound); } - let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", local_port)).await?); let mut client_builder = StunClientBuilder::new(udp.clone()); for server in stun_servers.iter() { @@ -1316,6 +1326,13 @@ impl StunInfoCollectorTrait for MockStunInfoCollector { Ok(format!("127.0.0.1:{}", port).parse().unwrap()) } + async fn get_udp_port_mapping_with_socket( + &self, + udp: Arc, + ) -> Result { + self.get_udp_port_mapping(udp.local_addr()?.port()).await + } + async fn get_tcp_port_mapping(&self, mut port: u16) -> Result { if port == 0 { port = 40144; diff --git a/easytier/src/common/upnp.rs b/easytier/src/common/upnp.rs new file mode 100644 index 00000000..61ec357f --- /dev/null +++ b/easytier/src/common/upnp.rs @@ -0,0 +1,767 @@ +use std::{ + fmt, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + sync::Arc, + time::Duration, +}; + +use anyhow::{Context, anyhow, bail}; +use igd_next::{ + AddAnyPortError, PortMappingProtocol, SearchOptions, + aio::{ + Gateway, + tokio::{Tokio, search_gateway}, + }, +}; +use natpmp::{ + Protocol as NatPmpProtocol, Response as NatPmpResponse, new_tokio_natpmp, new_tokio_natpmp_with, +}; +use tokio::{net::UdpSocket, sync::oneshot}; + +use super::{ + global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, + stun::StunInfoCollectorTrait as _, +}; +use crate::tunnel::build_url_from_socket_addr; + +const UPNP_SEARCH_TIMEOUT: Duration = Duration::from_secs(1); +const UPNP_SEARCH_RESPONSE_TIMEOUT: Duration = Duration::from_millis(300); +const NAT_PMP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(1); +const UPNP_LEASE_DURATION_SECS: u32 = 300; +const UPNP_RENEW_INTERVAL: Duration = Duration::from_secs(240); +const UPNP_DESCRIPTION: &str = "EasyTier udp hole punch"; +const PORT_MAPPING_BACKEND_NAT_PMP: &str = "nat-pmp"; +const PORT_MAPPING_BACKEND_IGD: &str = "igd"; + +type TokioGateway = Gateway; + +enum PortMappingBackend { + NatPmp { gateway: Ipv4Addr }, + Igd { gateway: TokioGateway }, +} + +impl PortMappingBackend { + fn name(&self) -> &'static str { + match self { + Self::NatPmp { .. } => PORT_MAPPING_BACKEND_NAT_PMP, + Self::Igd { .. } => PORT_MAPPING_BACKEND_IGD, + } + } +} + +struct ActiveUdpPortMapping { + backend: PortMappingBackend, + local_listener: url::Url, + local_addr: SocketAddr, + gateway_external_port: u16, +} + +impl ActiveUdpPortMapping { + async fn discover_nat_pmp_gateway( + local_listener: &url::Url, + ) -> anyhow::Result<(Ipv4Addr, SocketAddr)> { + let client = new_tokio_natpmp().await.context("create nat-pmp client")?; + let gateway = *client.gateway(); + let gateway_addr = SocketAddr::V4(SocketAddrV4::new(gateway, natpmp::NATPMP_PORT)); + let local_addr = resolve_internal_addr(gateway_addr, local_listener).await?; + Ok((gateway, local_addr)) + } + + async fn establish_via_nat_pmp( + local_listener: &url::Url, + gateway: Ipv4Addr, + local_addr: SocketAddr, + ) -> anyhow::Result { + let gateway_external_port = + add_udp_mapping_port_nat_pmp(gateway, local_addr, local_listener) + .await + .with_context(|| { + format!("map udp socket for {local_listener} via nat-pmp gateway {gateway}") + })?; + + Ok(Self { + backend: PortMappingBackend::NatPmp { gateway }, + local_listener: local_listener.clone(), + local_addr, + gateway_external_port, + }) + } + + async fn discover_igd_gateway( + global_ctx: &ArcGlobalCtx, + local_listener: &url::Url, + ) -> anyhow::Result<(TokioGateway, SocketAddr)> { + let _g = global_ctx.net_ns.guard(); + let gateway = search_gateway(SearchOptions { + timeout: Some(UPNP_SEARCH_TIMEOUT), + single_search_timeout: Some(UPNP_SEARCH_RESPONSE_TIMEOUT), + ..Default::default() + }) + .await + .with_context(|| format!("search igd gateway for {local_listener}"))?; + let local_addr = resolve_internal_addr(gateway.addr, local_listener).await?; + + Ok((gateway, local_addr)) + } + + async fn establish_via_igd( + local_listener: &url::Url, + gateway: TokioGateway, + local_addr: SocketAddr, + ) -> anyhow::Result { + let gateway_external_port = add_udp_mapping_port_igd(&gateway, local_addr, local_listener) + .await + .with_context(|| { + format!( + "map udp socket for {local_listener} via gateway {}", + gateway.addr + ) + })?; + + Ok(Self { + backend: PortMappingBackend::Igd { gateway }, + local_listener: local_listener.clone(), + local_addr, + gateway_external_port, + }) + } + + fn backend_name(&self) -> &'static str { + self.backend.name() + } + + async fn renew(&self) -> anyhow::Result<()> { + match &self.backend { + PortMappingBackend::NatPmp { gateway } => { + renew_udp_mapping_nat_pmp( + *gateway, + self.local_addr, + self.gateway_external_port, + &self.local_listener, + ) + .await + } + PortMappingBackend::Igd { gateway } => { + renew_udp_mapping_igd( + gateway, + self.local_addr, + self.gateway_external_port, + &self.local_listener, + ) + .await + } + } + } + + async fn remove(&self) -> anyhow::Result<()> { + match &self.backend { + PortMappingBackend::NatPmp { gateway } => { + remove_udp_mapping_nat_pmp( + *gateway, + self.local_addr, + self.gateway_external_port, + &self.local_listener, + ) + .await + } + PortMappingBackend::Igd { gateway } => { + remove_udp_mapping_igd(gateway, self.gateway_external_port, &self.local_listener) + .await + } + } + } +} + +pub struct UdpPortMappingLease { + backend: &'static str, + gateway_external_port: u16, + stop_tx: Option>, +} + +impl UdpPortMappingLease { + pub fn backend(&self) -> &'static str { + self.backend + } + + pub fn gateway_external_port(&self) -> u16 { + self.gateway_external_port + } +} + +impl fmt::Debug for UdpPortMappingLease { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UdpPortMappingLease") + .field("backend", &self.backend) + .field("gateway_external_port", &self.gateway_external_port) + .finish() + } +} + +impl Drop for UdpPortMappingLease { + fn drop(&mut self) { + if let Some(stop_tx) = self.stop_tx.take() { + let _ = stop_tx.send(()); + } + } +} + +pub async fn resolve_udp_public_addr( + global_ctx: ArcGlobalCtx, + local_listener: &url::Url, + socket: Arc, +) -> anyhow::Result<(SocketAddr, Option)> { + let port_mapping = match try_start_udp_port_mapping(&global_ctx, local_listener).await { + Ok(mapping) => mapping, + Err(err) => { + tracing::warn!( + ?err, + %local_listener, + "failed to establish udp port mapping, fallback to stun-only public addr resolution" + ); + None + } + }; + + let mapped_addr = global_ctx + .get_stun_info_collector() + .get_udp_port_mapping_with_socket(socket) + .await + .map_err(anyhow::Error::from) + .with_context(|| format!("resolve udp public addr for {local_listener}"))?; + + if let Some(port_mapping) = port_mapping.as_ref() { + let mapped_listener = build_url_from_socket_addr(&mapped_addr.to_string(), "udp"); + global_ctx.issue_event(GlobalCtxEvent::ListenerPortMappingEstablished { + local_listener: local_listener.clone(), + mapped_listener, + backend: port_mapping.backend().to_string(), + }); + tracing::info!( + %local_listener, + backend = port_mapping.backend(), + gateway_external_port = port_mapping.gateway_external_port(), + stun_mapped_addr = %mapped_addr, + "udp public addr resolved after port mapping" + ); + } else { + tracing::debug!( + %local_listener, + stun_mapped_addr = %mapped_addr, + "udp public addr resolved without port mapping" + ); + } + + Ok((mapped_addr, port_mapping)) +} + +async fn try_start_udp_port_mapping( + global_ctx: &ArcGlobalCtx, + local_listener: &url::Url, +) -> anyhow::Result> { + if global_ctx.get_flags().disable_upnp || !should_map_udp_listener(local_listener) { + return Ok(None); + } + + let mapping = discover_udp_port_mapping(global_ctx.clone(), local_listener.clone()).await?; + tracing::info!( + %local_listener, + backend = mapping.backend_name(), + local_addr = %mapping.local_addr, + gateway_external_port = mapping.gateway_external_port, + "udp port mapping established" + ); + + let backend = mapping.backend_name(); + let gateway_external_port = mapping.gateway_external_port; + let runtime_global_ctx = global_ctx.clone(); + let runtime_local_listener = local_listener.clone(); + let (stop_tx, stop_rx) = oneshot::channel(); + if should_run_port_mapping_in_dedicated_thread(&runtime_global_ctx) { + tokio::task::spawn_blocking(move || { + let _g = runtime_global_ctx.net_ns.guard(); + match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(runtime) => { + runtime.block_on(run_udp_port_mapping_task( + runtime_local_listener, + mapping, + stop_rx, + )); + } + Err(err) => { + tracing::error!( + ?err, + %runtime_local_listener, + "failed to build runtime for udp port mapping renew task" + ); + } + } + }); + } else { + tokio::spawn(run_udp_port_mapping_task( + runtime_local_listener, + mapping, + stop_rx, + )); + } + + Ok(Some(UdpPortMappingLease { + backend, + gateway_external_port, + stop_tx: Some(stop_tx), + })) +} + +async fn discover_udp_port_mapping( + global_ctx: ArcGlobalCtx, + local_listener: url::Url, +) -> anyhow::Result { + match discover_igd_gateway_in_netns(global_ctx.clone(), local_listener.clone()).await { + Ok((gateway, local_addr)) => match establish_igd_mapping_in_netns( + global_ctx.clone(), + local_listener.clone(), + gateway, + local_addr, + ) + .await + { + Ok(mapping) => Ok(mapping), + Err(igd_err) => { + tracing::debug!( + ?igd_err, + %local_listener, + "igd udp port mapping failed, retry with nat-pmp" + ); + match discover_nat_pmp_gateway_in_netns(global_ctx.clone(), local_listener.clone()) + .await + { + Ok((gateway, local_addr)) => establish_nat_pmp_mapping_in_netns( + global_ctx, + local_listener.clone(), + gateway, + local_addr, + ) + .await + .map_err(|nat_pmp_err| { + anyhow!( + "udp port mapping failed for {local_listener}: igd error: {igd_err}; nat-pmp error: {nat_pmp_err}" + ) + }), + Err(nat_pmp_err) => Err(anyhow!( + "udp port mapping failed for {local_listener}: igd error: {igd_err}; nat-pmp discovery error: {nat_pmp_err}" + )), + } + } + }, + Err(igd_err) => { + tracing::debug!( + ?igd_err, + %local_listener, + "igd gateway discovery failed, retry with nat-pmp" + ); + match discover_nat_pmp_gateway_in_netns(global_ctx.clone(), local_listener.clone()).await + { + Ok((gateway, local_addr)) => establish_nat_pmp_mapping_in_netns( + global_ctx, + local_listener.clone(), + gateway, + local_addr, + ) + .await + .map_err(|nat_pmp_err| { + anyhow!( + "udp port mapping failed for {local_listener}: igd discovery error: {igd_err}; nat-pmp error: {nat_pmp_err}" + ) + }), + Err(nat_pmp_err) => Err(anyhow!( + "udp port mapping failed for {local_listener}: igd discovery error: {igd_err}; nat-pmp discovery error: {nat_pmp_err}" + )), + } + } + } +} + +async fn discover_igd_gateway_in_netns( + global_ctx: ArcGlobalCtx, + local_listener: url::Url, +) -> anyhow::Result<(TokioGateway, SocketAddr)> { + if !should_run_port_mapping_in_dedicated_thread(&global_ctx) { + return ActiveUdpPortMapping::discover_igd_gateway(&global_ctx, &local_listener).await; + } + + tokio::task::spawn_blocking(move || { + let _g = global_ctx.net_ns.guard(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("build runtime for igd gateway discovery")? + .block_on(ActiveUdpPortMapping::discover_igd_gateway( + &global_ctx, + &local_listener, + )) + }) + .await + .context("join igd gateway discovery task")? +} + +async fn establish_igd_mapping_in_netns( + global_ctx: ArcGlobalCtx, + local_listener: url::Url, + gateway: TokioGateway, + local_addr: SocketAddr, +) -> anyhow::Result { + if !should_run_port_mapping_in_dedicated_thread(&global_ctx) { + return ActiveUdpPortMapping::establish_via_igd(&local_listener, gateway, local_addr).await; + } + + tokio::task::spawn_blocking(move || { + let _g = global_ctx.net_ns.guard(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("build runtime for igd mapping establishment")? + .block_on(ActiveUdpPortMapping::establish_via_igd( + &local_listener, + gateway, + local_addr, + )) + }) + .await + .context("join igd mapping establishment task")? +} + +async fn discover_nat_pmp_gateway_in_netns( + global_ctx: ArcGlobalCtx, + local_listener: url::Url, +) -> anyhow::Result<(Ipv4Addr, SocketAddr)> { + if !should_run_port_mapping_in_dedicated_thread(&global_ctx) { + return ActiveUdpPortMapping::discover_nat_pmp_gateway(&local_listener).await; + } + + tokio::task::spawn_blocking(move || { + let _g = global_ctx.net_ns.guard(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("build runtime for nat-pmp gateway discovery")? + .block_on(ActiveUdpPortMapping::discover_nat_pmp_gateway( + &local_listener, + )) + }) + .await + .context("join nat-pmp gateway discovery task")? +} + +async fn establish_nat_pmp_mapping_in_netns( + global_ctx: ArcGlobalCtx, + local_listener: url::Url, + gateway: Ipv4Addr, + local_addr: SocketAddr, +) -> anyhow::Result { + if !should_run_port_mapping_in_dedicated_thread(&global_ctx) { + return ActiveUdpPortMapping::establish_via_nat_pmp(&local_listener, gateway, local_addr) + .await; + } + + tokio::task::spawn_blocking(move || { + let _g = global_ctx.net_ns.guard(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("build runtime for nat-pmp mapping establishment")? + .block_on(ActiveUdpPortMapping::establish_via_nat_pmp( + &local_listener, + gateway, + local_addr, + )) + }) + .await + .context("join nat-pmp mapping establishment task")? +} + +async fn run_udp_port_mapping_task( + local_listener: url::Url, + mapping: ActiveUdpPortMapping, + mut stop_rx: oneshot::Receiver<()>, +) { + loop { + tokio::select! { + _ = tokio::time::sleep(UPNP_RENEW_INTERVAL) => { + if let Err(err) = mapping.renew().await { + tracing::warn!( + ?err, + %local_listener, + backend = mapping.backend_name(), + gateway_external_port = mapping.gateway_external_port, + "failed to renew udp port mapping" + ); + } + } + _ = &mut stop_rx => break, + } + } + + if let Err(err) = mapping.remove().await { + tracing::debug!( + ?err, + %local_listener, + backend = mapping.backend_name(), + gateway_external_port = mapping.gateway_external_port, + "failed to remove udp port mapping" + ); + } +} + +fn should_run_port_mapping_in_dedicated_thread(global_ctx: &ArcGlobalCtx) -> bool { + global_ctx.net_ns.name().is_some() +} + +async fn add_udp_mapping_port_igd( + gateway: &TokioGateway, + local_addr: SocketAddr, + local_listener: &url::Url, +) -> anyhow::Result { + match gateway + .add_any_port( + PortMappingProtocol::UDP, + local_addr, + UPNP_LEASE_DURATION_SECS, + UPNP_DESCRIPTION, + ) + .await + { + Ok(external_port) => Ok(external_port), + Err(AddAnyPortError::RequestError(err)) => { + tracing::debug!( + ?err, + %local_listener, + gateway = %gateway.addr, + %local_addr, + "igd any-port udp mapping failed, retry with same-port mapping" + ); + + gateway + .add_port( + PortMappingProtocol::UDP, + local_addr.port(), + local_addr, + UPNP_LEASE_DURATION_SECS, + UPNP_DESCRIPTION, + ) + .await + .map(|_| local_addr.port()) + .map_err(|same_port_err| { + anyhow!( + "igd udp mapping failed for {local_listener}: any-port error: {err}; same-port error: {same_port_err}" + ) + }) + } + Err(err) => Err(err.into()), + } +} + +async fn add_udp_mapping_port_nat_pmp( + gateway: Ipv4Addr, + local_addr: SocketAddr, + local_listener: &url::Url, +) -> anyhow::Result { + match request_nat_pmp_mapping(gateway, local_addr.port(), 0, UPNP_LEASE_DURATION_SECS).await { + Ok(external_port) => Ok(external_port), + Err(any_port_err) => { + tracing::debug!( + ?any_port_err, + %local_listener, + gateway = %gateway, + %local_addr, + "nat-pmp any-port udp mapping failed, retry with same-port mapping" + ); + + request_nat_pmp_mapping( + gateway, + local_addr.port(), + local_addr.port(), + UPNP_LEASE_DURATION_SECS, + ) + .await + .map_err(|same_port_err| { + anyhow!( + "nat-pmp udp mapping failed for {local_listener}: any-port error: {any_port_err}; same-port error: {same_port_err}" + ) + }) + } + } +} + +async fn request_nat_pmp_mapping( + gateway: Ipv4Addr, + private_port: u16, + public_port: u16, + lifetime_secs: u32, +) -> anyhow::Result { + let client = new_tokio_natpmp_with(gateway) + .await + .with_context(|| format!("create nat-pmp client for gateway {gateway}"))?; + client + .send_port_mapping_request( + NatPmpProtocol::UDP, + private_port, + public_port, + lifetime_secs, + ) + .await + .with_context(|| { + format!( + "send nat-pmp udp mapping request private_port={private_port} public_port={public_port} gateway={gateway}" + ) + })?; + + let response = tokio::time::timeout(NAT_PMP_RESPONSE_TIMEOUT, client.read_response_or_retry()) + .await + .with_context(|| { + format!( + "wait nat-pmp udp mapping response private_port={private_port} gateway={gateway}" + ) + })? + .map_err(anyhow::Error::from) + .with_context(|| { + format!( + "read nat-pmp udp mapping response private_port={private_port} gateway={gateway}" + ) + })?; + + match response { + NatPmpResponse::UDP(mapping) | NatPmpResponse::TCP(mapping) => Ok(mapping.public_port()), + NatPmpResponse::Gateway(_) => { + bail!("unexpected nat-pmp gateway response for udp mapping request") + } + } +} + +async fn renew_udp_mapping_nat_pmp( + gateway: Ipv4Addr, + local_addr: SocketAddr, + external_port: u16, + local_listener: &url::Url, +) -> anyhow::Result<()> { + request_nat_pmp_mapping( + gateway, + local_addr.port(), + external_port, + UPNP_LEASE_DURATION_SECS, + ) + .await + .map(|_| ()) + .with_context(|| format!("renew udp port mapping {local_listener}")) +} + +async fn remove_udp_mapping_nat_pmp( + gateway: Ipv4Addr, + local_addr: SocketAddr, + external_port: u16, + local_listener: &url::Url, +) -> anyhow::Result<()> { + request_nat_pmp_mapping(gateway, local_addr.port(), external_port, 0) + .await + .map(|_| ()) + .with_context(|| format!("remove udp port mapping {local_listener}")) +} + +fn should_map_udp_listener(local_listener: &url::Url) -> bool { + if local_listener.scheme() != "udp" { + return false; + } + + let Some(host) = listener_ipv4_host(local_listener) else { + return false; + }; + + if host.is_loopback() || host.is_broadcast() { + return false; + } + + host.is_unspecified() || host.is_private() || host.is_link_local() +} + +fn listener_ipv4_host(local_listener: &url::Url) -> Option { + local_listener.host_str()?.parse().ok() +} + +async fn resolve_internal_addr( + gateway_addr: SocketAddr, + local_listener: &url::Url, +) -> anyhow::Result { + let port = local_listener + .port() + .ok_or_else(|| anyhow!("listener port is missing"))?; + let host = + listener_ipv4_host(local_listener).ok_or_else(|| anyhow!("listener must be ipv4"))?; + + let ip = if host.is_unspecified() { + let udp = std::net::UdpSocket::bind("0.0.0.0:0") + .context("bind probe socket for gateway route")?; + udp.connect(gateway_addr) + .with_context(|| format!("connect probe socket to gateway {gateway_addr}"))?; + let SocketAddr::V4(local_addr) = udp.local_addr().context("get probe socket local addr")? + else { + bail!("gateway route selected a non-ipv4 local address"); + }; + *local_addr.ip() + } else { + host + }; + + Ok(SocketAddr::new(ip.into(), port)) +} + +async fn renew_udp_mapping_igd( + gateway: &TokioGateway, + local_addr: SocketAddr, + external_port: u16, + local_listener: &url::Url, +) -> anyhow::Result<()> { + gateway + .add_port( + PortMappingProtocol::UDP, + external_port, + local_addr, + UPNP_LEASE_DURATION_SECS, + UPNP_DESCRIPTION, + ) + .await + .with_context(|| format!("renew udp port mapping {local_listener}")) +} + +async fn remove_udp_mapping_igd( + gateway: &TokioGateway, + external_port: u16, + local_listener: &url::Url, +) -> anyhow::Result<()> { + gateway + .remove_port(PortMappingProtocol::UDP, external_port) + .await + .with_context(|| format!("remove udp port mapping {local_listener}")) +} + +#[cfg(test)] +mod tests { + #[test] + fn udp_mapping_requires_private_or_unspecified_ipv4_listener() { + assert!(super::should_map_udp_listener( + &"udp://0.0.0.0:11010".parse().unwrap() + )); + assert!(super::should_map_udp_listener( + &"udp://192.168.1.10:11010".parse().unwrap() + )); + assert!(!super::should_map_udp_listener( + &"udp://127.0.0.1:11010".parse().unwrap() + )); + assert!(!super::should_map_udp_listener( + &"udp://8.8.8.8:11010".parse().unwrap() + )); + assert!(!super::should_map_udp_listener( + &"tcp://0.0.0.0:11010".parse().unwrap() + )); + } +} diff --git a/easytier/src/connector/direct.rs b/easytier/src/connector/direct.rs index 6a5b63e1..452128f5 100644 --- a/easytier/src/connector/direct.rs +++ b/easytier/src/connector/direct.rs @@ -2,7 +2,7 @@ use std::{ collections::HashSet, - net::{IpAddr, Ipv6Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, str::FromStr, sync::{ Arc, @@ -27,7 +27,7 @@ use crate::{ proto::{ peer_rpc::{ DirectConnectorRpc, DirectConnectorRpcClientFactory, DirectConnectorRpcServer, - GetIpListRequest, GetIpListResponse, SendV6HolePunchPacketRequest, + GetIpListRequest, GetIpListResponse, SendUdpHolePunchPacketRequest, }, rpc_types::controller::BaseController, }, @@ -117,37 +117,25 @@ impl DirectConnectorManagerData { } } - async fn remote_send_v6_hole_punch_packet( + async fn remote_send_udp_hole_punch_packet( &self, dst_peer_id: PeerId, - local_socket: &UdpSocket, + connector_addr: SocketAddr, remote_url: &url::Url, ) -> Result<(), Error> { + if !matches_scheme!(remote_url, TunnelScheme::Ip(IpScheme::Udp)) { + return Err(anyhow::anyhow!( + "udp hole punch packet only applies to udp listener: {}", + remote_url + ) + .into()); + } + let global_ctx = self.peer_manager.get_global_ctx(); let listener_port = remote_url.port().ok_or(anyhow::anyhow!( "failed to parse port from remote url: {}", remote_url ))?; - let connector_ip = global_ctx - .get_stun_info_collector() - .get_stun_info() - .public_ip - .iter() - .find(|x| x.contains(":")) - .ok_or(anyhow::anyhow!( - "failed to get public ipv6 address from stun info" - ))? - .parse::() - .with_context(|| { - format!( - "failed to parse public ipv6 address from stun info: {:?}", - global_ctx.get_stun_info_collector().get_stun_info() - ) - })?; - let connector_addr = SocketAddr::new( - std::net::IpAddr::V6(connector_ip), - local_socket.local_addr()?.port(), - ); let rpc_stub = self .peer_manager @@ -160,9 +148,9 @@ impl DirectConnectorManagerData { ); rpc_stub - .send_v6_hole_punch_packet( + .send_udp_hole_punch_packet( BaseController::default(), - SendV6HolePunchPacketRequest { + SendUdpHolePunchPacketRequest { listener_port: listener_port as u32, connector_addr: Some(connector_addr.into()), }, @@ -170,7 +158,7 @@ impl DirectConnectorManagerData { .await .with_context(|| { format!( - "do rpc, send v6 hole punch packet to peer {} at {}", + "do rpc, send udp hole punch packet to peer {} at {}", dst_peer_id, remote_url ) })?; @@ -188,11 +176,34 @@ impl DirectConnectorManagerData { .await .with_context(|| format!("failed to bind local socket for {}", remote_url))?, ); + let connector_ip = self + .peer_manager + .get_global_ctx() + .get_stun_info_collector() + .get_stun_info() + .public_ip + .iter() + .find(|x| x.contains(':')) + .ok_or(anyhow::anyhow!( + "failed to get public ipv6 address from stun info" + ))? + .parse::() + .with_context(|| { + format!( + "failed to parse public ipv6 address from stun info: {:?}", + self.peer_manager + .get_global_ctx() + .get_stun_info_collector() + .get_stun_info() + ) + })?; + let connector_addr = + SocketAddr::new(IpAddr::V6(connector_ip), local_socket.local_addr()?.port()); // ask remote to send v6 hole punch packet // and no matter what the result is, continue to connect let _ = self - .remote_send_v6_hole_punch_packet(dst_peer_id, &local_socket, remote_url) + .remote_send_udp_hole_punch_packet(dst_peer_id, connector_addr, remote_url) .await; let udp_connector = UdpTunnelConnector::new(remote_url.clone()); @@ -207,14 +218,80 @@ impl DirectConnectorManagerData { .await } + async fn connect_to_public_ipv4( + &self, + dst_peer_id: PeerId, + remote_url: &url::Url, + ) -> Result<(PeerId, PeerConnId), Error> { + let local_socket = { + let _g = self.global_ctx.net_ns.guard(); + Arc::new( + UdpSocket::bind("0.0.0.0:0") + .await + .with_context(|| format!("failed to bind local socket for {}", remote_url))?, + ) + }; + let connector_addr = self + .peer_manager + .get_global_ctx() + .get_stun_info_collector() + .get_udp_port_mapping_with_socket(local_socket.clone()) + .await + .with_context(|| format!("failed to get udp port mapping for {}", remote_url))?; + + let _ = self + .remote_send_udp_hole_punch_packet(dst_peer_id, connector_addr, remote_url) + .await; + + let udp_connector = UdpTunnelConnector::new(remote_url.clone()); + let remote_addr = SocketAddr::from_url(remote_url.clone(), IpVersion::V4).await?; + let ret = udp_connector + .try_connect_with_socket(local_socket, remote_addr) + .await?; + + self.peer_manager + .add_client_tunnel_with_peer_id_hint(ret, true, Some(dst_peer_id)) + .await + } + async fn do_try_connect_to_ip(&self, dst_peer_id: PeerId, addr: String) -> Result<(), Error> { let connector = create_connector_by_url(&addr, &self.global_ctx, IpVersion::Both).await?; let remote_url = connector.remote_url(); - let (peer_id, conn_id) = if matches_scheme!(remote_url, TunnelScheme::Ip(IpScheme::Udp)) - && matches!(remote_url.host(), Some(Host::Ipv6(_))) - { - self.connect_to_public_ipv6(dst_peer_id, &remote_url) - .await? + let (peer_id, conn_id) = if matches_scheme!(remote_url, TunnelScheme::Ip(IpScheme::Udp)) { + match remote_url.host() { + Some(Host::Ipv6(_)) => { + self.connect_to_public_ipv6(dst_peer_id, &remote_url) + .await? + } + Some(Host::Ipv4(ip)) if is_public_ipv4(ip) => { + match self.connect_to_public_ipv4(dst_peer_id, &remote_url).await { + Ok(ret) => ret, + Err(err) => { + tracing::debug!( + ?err, + %remote_url, + "udp public ipv4 listener punch failed, falling back to direct connect" + ); + timeout( + std::time::Duration::from_secs(3), + self.peer_manager.try_direct_connect_with_peer_id_hint( + connector, + Some(dst_peer_id), + ), + ) + .await?? + } + } + } + _ => { + timeout( + std::time::Duration::from_secs(3), + self.peer_manager + .try_direct_connect_with_peer_id_hint(connector, Some(dst_peer_id)), + ) + .await?? + } + } } else { timeout( std::time::Duration::from_secs(3), @@ -577,6 +654,14 @@ impl DirectConnectorManagerData { } } +fn is_public_ipv4(ip: Ipv4Addr) -> bool { + !ip.is_private() + && !ip.is_loopback() + && !ip.is_link_local() + && !ip.is_broadcast() + && !ip.is_unspecified() +} + impl std::fmt::Debug for DirectConnectorManagerData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DirectConnectorManagerData") diff --git a/easytier/src/connector/tcp_hole_punch.rs b/easytier/src/connector/tcp_hole_punch.rs index 024ac163..9baaeaa0 100644 --- a/easytier/src/connector/tcp_hole_punch.rs +++ b/easytier/src/connector/tcp_hole_punch.rs @@ -621,6 +621,13 @@ mod tests { Ok(format!("127.0.0.1:{}", port).parse().unwrap()) } + async fn get_udp_port_mapping_with_socket( + &self, + udp: std::sync::Arc, + ) -> Result { + self.get_udp_port_mapping(udp.local_addr()?.port()).await + } + async fn get_tcp_port_mapping(&self, mut port: u16) -> Result { if port == 0 { port = 40144; diff --git a/easytier/src/connector/udp_hole_punch/common.rs b/easytier/src/connector/udp_hole_punch/common.rs index c99b8094..b4cf52f1 100644 --- a/easytier/src/connector/udp_hole_punch/common.rs +++ b/easytier/src/connector/udp_hole_punch/common.rs @@ -13,8 +13,7 @@ use zerocopy::FromBytes as _; use crate::{ common::{ - PeerId, error::Error, global_ctx::ArcGlobalCtx, join_joinset_background, netns::NetNS, - stun::StunInfoCollectorTrait as _, + PeerId, error::Error, global_ctx::ArcGlobalCtx, join_joinset_background, netns::NetNS, upnp, }, defer, peers::peer_manager::PeerManager, @@ -27,6 +26,7 @@ use crate::{ }; pub(crate) const HOLE_PUNCH_PACKET_BODY_LEN: u16 = 16; +const MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS: usize = 4; fn generate_shuffled_port_vec() -> Vec { let mut rng = rand::thread_rng(); @@ -352,6 +352,8 @@ pub(crate) struct UdpHolePunchListener { tasks: JoinSet<()>, running: Arc>, mapped_addr: SocketAddr, + has_port_mapping_lease: bool, + _port_mapping_lease: Option, conn_counter: Arc>, listen_time: std::time::Instant, @@ -360,11 +362,6 @@ pub(crate) struct UdpHolePunchListener { } impl UdpHolePunchListener { - async fn get_avail_port() -> Result { - let socket = UdpSocket::bind("0.0.0.0:0").await?; - Ok(socket.local_addr()?.port()) - } - #[instrument(err)] pub async fn new(peer_mgr: Arc) -> Result { Self::new_ext(peer_mgr, true, None).await @@ -376,18 +373,24 @@ impl UdpHolePunchListener { with_mapped_addr: bool, port: Option, ) -> Result { - let port = port.unwrap_or(Self::get_avail_port().await?); - let listen_url = format!("udp://0.0.0.0:{}", port); + let socket = { + let _g = peer_mgr.get_global_ctx().net_ns.guard(); + Arc::new(UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port.unwrap_or(0))).await?) + }; + let local_port = socket.local_addr()?.port(); + let listen_url: url::Url = format!("udp://0.0.0.0:{local_port}").parse().unwrap(); - let mapped_addr = if with_mapped_addr { - let gctx = peer_mgr.get_global_ctx(); - let stun_info_collect = gctx.get_stun_info_collector(); - stun_info_collect.get_udp_port_mapping(port).await? + let (mapped_addr, port_mapping_lease) = if with_mapped_addr { + upnp::resolve_udp_public_addr(peer_mgr.get_global_ctx(), &listen_url, socket.clone()) + .await? } else { - SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port)) + ( + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_port)), + None, + ) }; - let mut listener = UdpTunnelListener::new(listen_url.parse().unwrap()); + let mut listener = UdpTunnelListener::new_with_socket(listen_url, socket.clone()); { let _g = peer_mgr.get_global_ctx().net_ns.guard(); @@ -437,6 +440,8 @@ impl UdpHolePunchListener { socket, running, mapped_addr, + has_port_mapping_lease: port_mapping_lease.is_some(), + _port_mapping_lease: port_mapping_lease, conn_counter, listen_time: std::time::Instant::now(), @@ -517,45 +522,87 @@ impl PunchHoleServerCommon { pub(crate) async fn select_listener( &self, use_new_listener: bool, + prefer_port_mapping: bool, ) -> Option<(Arc, SocketAddr)> { - let all_listener_sockets = &self.listeners; - - let mut use_last = false; - if all_listener_sockets.lock().await.len() < 16 || use_new_listener { - tracing::warn!("creating new udp hole punching listener"); - all_listener_sockets.lock().await.push( - UdpHolePunchListener::new(self.peer_mgr.clone()) - .await - .ok()?, - ); - use_last = true; - } - - let mut locked = all_listener_sockets.lock().await; - - let listener = if use_last { - Some(locked.last_mut()?) - } else { - // use the listener that is active most recently - locked - .iter_mut() - .filter(|l| !l.mapped_addr.ip().is_unspecified()) - .max_by_key(|listener| listener.last_active_time.load()) + let (listener_count, has_reusable_listener, has_port_mapping_listener) = { + let locked = self.listeners.lock().await; + ( + locked.len(), + locked.iter().any(can_reuse_public_listener), + locked.iter().any(can_reuse_port_mapping_listener), + ) }; + let should_create = should_create_public_listener( + listener_count, + has_reusable_listener, + has_port_mapping_listener, + use_new_listener, + prefer_port_mapping, + ); - if listener.is_none() || listener.as_ref().unwrap().mapped_addr.ip().is_unspecified() { + if should_create { tracing::warn!( - ?use_new_listener, - "no available udp hole punching listener with mapped address" + max_listeners = MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, + "creating udp hole punching listener" ); - if !use_new_listener { - return self.select_listener(true).await; - } else { - return None; + match UdpHolePunchListener::new(self.peer_mgr.clone()).await { + Ok(listener) => self.listeners.lock().await.push(listener), + Err(err) => { + tracing::warn!(?err, "failed to create udp hole punching listener"); + } } } - let listener = listener.unwrap(); + let mut locked = self.listeners.lock().await; + let listener_count = locked.len(); + let listener_idx = if prefer_port_mapping { + select_reusable_port_mapping_listener_idx(locked.as_slice()) + .or_else(|| { + if should_create && locked.last().is_some_and(can_reuse_public_listener) { + Some(locked.len() - 1) + } else { + None + } + }) + .or_else(|| select_reusable_public_listener_idx(locked.as_slice())) + } else if should_create { + locked.len().checked_sub(1) + } else { + select_reusable_public_listener_idx(locked.as_slice()) + }; + + let Some(listener_idx) = listener_idx else { + tracing::warn!( + ?use_new_listener, + ?prefer_port_mapping, + listener_count, + max_listeners = MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, + "no available udp hole punching listener with mapped address" + ); + if should_retry_public_listener_selection( + use_new_listener, + listener_count, + prefer_port_mapping, + has_port_mapping_listener, + ) { + drop(locked); + return self.select_listener(true, prefer_port_mapping).await; + } + return None; + }; + + let listener = &mut locked[listener_idx]; + if !can_reuse_public_listener(listener) { + tracing::warn!( + ?use_new_listener, + ?prefer_port_mapping, + listener_count, + max_listeners = MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, + "selected udp hole punching listener is not reusable" + ); + return None; + } + Some((listener.get_socket().await, listener.mapped_addr)) } @@ -572,7 +619,73 @@ impl PunchHoleServerCommon { } } -#[tracing::instrument(err, ret(level=Level::DEBUG), skip(ports))] +fn can_reuse_public_listener(listener: &UdpHolePunchListener) -> bool { + listener.running.load() && !listener.mapped_addr.ip().is_unspecified() +} + +fn can_reuse_port_mapping_listener(listener: &UdpHolePunchListener) -> bool { + can_reuse_public_listener(listener) && listener.has_port_mapping_lease +} + +fn select_reusable_public_listener_idx(listeners: &[UdpHolePunchListener]) -> Option { + // Reuse the listener that was active most recently. + listeners + .iter() + .enumerate() + .filter(|(_, listener)| can_reuse_public_listener(listener)) + .max_by_key(|(_, listener)| listener.last_active_time.load()) + .map(|(idx, _)| idx) +} + +fn select_reusable_port_mapping_listener_idx(listeners: &[UdpHolePunchListener]) -> Option { + listeners + .iter() + .enumerate() + .filter(|(_, listener)| can_reuse_port_mapping_listener(listener)) + .max_by_key(|(_, listener)| listener.last_active_time.load()) + .map(|(idx, _)| idx) +} + +fn should_create_public_listener( + current_listener_count: usize, + has_reusable_listener: bool, + has_port_mapping_listener: bool, + force_new_listener: bool, + prefer_port_mapping: bool, +) -> bool { + if current_listener_count >= MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS { + return false; + } + + if current_listener_count == 0 { + return true; + } + + if force_new_listener { + return true; + } + + if prefer_port_mapping && !has_port_mapping_listener { + return true; + } + + !has_reusable_listener +} + +fn should_retry_public_listener_selection( + force_new_listener: bool, + current_listener_count: usize, + prefer_port_mapping: bool, + has_port_mapping_listener: bool, +) -> bool { + if prefer_port_mapping && has_port_mapping_listener { + return false; + } + + !force_new_listener && current_listener_count < MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS +} + +#[tracing::instrument(err, ret(level=Level::DEBUG))] pub(crate) async fn send_symmetric_hole_punch_packet( ports: &[u16], udp: Arc, @@ -647,3 +760,66 @@ pub(crate) async fn try_connect_with_socket( .await .map_err(Error::from) } + +#[cfg(test)] +mod tests { + use super::{ + MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, should_create_public_listener, + should_retry_public_listener_selection, + }; + + #[test] + fn listener_selection_prefers_reuse_before_cap() { + assert!(!should_create_public_listener(1, true, true, false, false)); + assert!(!should_create_public_listener( + MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, + true, + true, + false, + false + )); + } + + #[test] + fn listener_selection_creates_when_empty_or_no_reusable_listener() { + assert!(should_create_public_listener(0, false, false, false, false)); + assert!(should_create_public_listener(1, false, false, false, false)); + } + + #[test] + fn listener_selection_force_new_respects_cap() { + assert!(should_create_public_listener(1, true, true, true, false)); + assert!(!should_create_public_listener( + MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, + true, + true, + true, + false + )); + } + + #[test] + fn listener_selection_prefers_port_mapping_until_available() { + assert!(should_create_public_listener(1, true, false, false, true)); + assert!(!should_create_public_listener(1, true, true, false, true)); + } + + #[test] + fn listener_selection_retry_respects_cap() { + assert!(should_retry_public_listener_selection( + false, 1, false, false + )); + assert!(!should_retry_public_listener_selection( + false, + MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, + false, + false + )); + assert!(!should_retry_public_listener_selection( + true, 1, false, false + )); + assert!(!should_retry_public_listener_selection( + false, 1, true, true + )); + } +} diff --git a/easytier/src/connector/udp_hole_punch/cone.rs b/easytier/src/connector/udp_hole_punch/cone.rs index 71be9519..5d75f981 100644 --- a/easytier/src/connector/udp_hole_punch/cone.rs +++ b/easytier/src/connector/udp_hole_punch/cone.rs @@ -7,7 +7,7 @@ use anyhow::Context; use tokio::net::UdpSocket; use crate::{ - common::{PeerId, scoped_task::ScopedTask, stun::StunInfoCollectorTrait}, + common::{PeerId, scoped_task::ScopedTask, upnp}, connector::udp_hole_punch::common::{ HOLE_PUNCH_PACKET_BODY_LEN, UdpSocketArray, try_connect_with_socket, }, @@ -117,23 +117,19 @@ impl PunchConeHoleClient { let _g = self.peer_mgr.get_global_ctx().net_ns.guard(); Arc::new(UdpSocket::bind("0.0.0.0:0").await?) }; - let local_addr = local_socket .local_addr() - .with_context(|| "failed to get local port from udp array")?; - let local_port = local_addr.port(); - - drop(local_socket); - let local_mapped_addr = global_ctx - .get_stun_info_collector() - .get_udp_port_mapping(local_port) - .await - .with_context(|| "failed to get udp port mapping")?; - - let local_socket = { - let _g = self.peer_mgr.get_global_ctx().net_ns.guard(); - Arc::new(UdpSocket::bind(local_addr).await?) - }; + .with_context(|| "failed to get local addr from udp punch socket")?; + let local_listener: url::Url = format!("udp://0.0.0.0:{}", local_addr.port()) + .parse() + .unwrap(); + let (local_mapped_addr, _local_port_mapping_lease) = upnp::resolve_udp_public_addr( + global_ctx.clone(), + &local_listener, + local_socket.clone(), + ) + .await + .with_context(|| "failed to resolve udp public addr for cone hole punch")?; // client -> server: tell server the mapped port, server will return the mapped address of listening port. let rpc_stub = self @@ -149,7 +145,10 @@ impl PunchConeHoleClient { let resp = rpc_stub .select_punch_listener( BaseController::default(), - SelectPunchListenerRequest { force_new: false }, + SelectPunchListenerRequest { + force_new: false, + prefer_port_mapping: true, + }, ) .await; diff --git a/easytier/src/connector/udp_hole_punch/mod.rs b/easytier/src/connector/udp_hole_punch/mod.rs index 779d428b..e1c2e14b 100644 --- a/easytier/src/connector/udp_hole_punch/mod.rs +++ b/easytier/src/connector/udp_hole_punch/mod.rs @@ -88,7 +88,7 @@ impl UdpHolePunchRpc for UdpHolePunchServer { ) -> rpc_types::error::Result { let (_, addr) = self .common - .select_listener(input.force_new) + .select_listener(input.force_new, input.prefer_port_mapping) .await .ok_or(anyhow::anyhow!("no listener available"))?; @@ -584,6 +584,11 @@ impl UdpHolePunchConnector { Ok(()) } + + #[cfg(test)] + pub async fn run_immediately_for_test(&self) { + self.client.run_immediately().await; + } } #[cfg(test)] @@ -614,6 +619,9 @@ pub mod tests { udp_nat_type: NatType, ) -> Arc { let p_a = create_mock_peer_manager().await; + let mut flags = p_a.get_global_ctx().get_flags(); + flags.disable_upnp = true; + p_a.get_global_ctx().set_flags(flags); replace_stun_info_collector(p_a.clone(), udp_nat_type); p_a } diff --git a/easytier/src/connector/udp_hole_punch/sym_to_cone.rs b/easytier/src/connector/udp_hole_punch/sym_to_cone.rs index fc8973b4..f2475fa0 100644 --- a/easytier/src/connector/udp_hole_punch/sym_to_cone.rs +++ b/easytier/src/connector/udp_hole_punch/sym_to_cone.rs @@ -434,7 +434,10 @@ impl PunchSymToConeHoleClient { let resp = rpc_stub .select_punch_listener( BaseController::default(), - SelectPunchListenerRequest { force_new: false }, + SelectPunchListenerRequest { + force_new: false, + prefer_port_mapping: true, + }, ) .await; diff --git a/easytier/src/core.rs b/easytier/src/core.rs index f57daf14..ada771ba 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -450,6 +450,15 @@ struct NetworkOptions { )] 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_RELAY_ALL_PEER_RPC", @@ -1101,7 +1110,10 @@ impl NetworkOptions { 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(false); + 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); // 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(); diff --git a/easytier/src/instance_manager.rs b/easytier/src/instance_manager.rs index 4ad710e8..460c3772 100644 --- a/easytier/src/instance_manager.rs +++ b/easytier/src/instance_manager.rs @@ -355,6 +355,21 @@ fn handle_event( event!(info, category: "CONNECTION", local, remote, err, "[{}] connection error", instance_id); } + GlobalCtxEvent::ListenerPortMappingEstablished { + local_listener, + mapped_listener, + backend, + } => { + event!( + info, + %local_listener, + %mapped_listener, + backend, + "[{}] listener port mapping established", + instance_id + ); + } + GlobalCtxEvent::TunDeviceReady(dev) => { event!(info, dev, "[{}] tun device ready", instance_id); } diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 3ae57af4..0d64bb84 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -801,6 +801,10 @@ impl NetworkConfig { 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_sym_hole_punching) = self.disable_sym_hole_punching { flags.disable_sym_hole_punching = disable_sym_hole_punching; } @@ -963,6 +967,7 @@ impl NetworkConfig { 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_sym_hole_punching = Some(flags.disable_sym_hole_punching); result.enable_magic_dns = Some(flags.accept_dns); result.mtu = Some(flags.mtu as i32); @@ -1230,6 +1235,7 @@ mod tests { flags.enable_encryption = rng.gen_bool(0.8); flags.disable_tcp_hole_punching = rng.gen_bool(0.2); flags.disable_udp_hole_punching = rng.gen_bool(0.2); + flags.disable_upnp = rng.gen_bool(0.2); flags.accept_dns = rng.gen_bool(0.6); flags.mtu = rng.gen_range(1200..1500); flags.private_mode = rng.gen_bool(0.3); diff --git a/easytier/src/peers/peer_rpc_service.rs b/easytier/src/peers/peer_rpc_service.rs index 729f5354..901e62aa 100644 --- a/easytier/src/peers/peer_rpc_service.rs +++ b/easytier/src/peers/peer_rpc_service.rs @@ -5,7 +5,7 @@ use crate::{ proto::{ common::Void, peer_rpc::{ - DirectConnectorRpc, GetIpListRequest, GetIpListResponse, SendV6HolePunchPacketRequest, + DirectConnectorRpc, GetIpListRequest, GetIpListResponse, SendUdpHolePunchPacketRequest, }, rpc_types::{self, controller::BaseController}, }, @@ -50,29 +50,29 @@ impl DirectConnectorRpc for DirectConnectorManagerRpcServer { Ok(ret) } - async fn send_v6_hole_punch_packet( + async fn send_udp_hole_punch_packet( &self, _: BaseController, - req: SendV6HolePunchPacketRequest, + req: SendUdpHolePunchPacketRequest, ) -> rpc_types::error::Result { let listener_port = req.listener_port as u16; - let SocketAddr::V6(connector_addr) = req + let connector_addr: SocketAddr = req .connector_addr .ok_or(anyhow::anyhow!("connector_addr is required"))? - .into() - else { - return Err(anyhow::anyhow!("connector_addr is not a v6 address").into()); - }; + .into(); tracing::info!( - "Sending v6 hole punch packet to {} from listener port {}", + "Sending udp hole punch packet to {} from listener port {}", connector_addr, listener_port ); // send 3 packets to the connector for _ in 0..3 { - udp::send_v6_hole_punch_packet(listener_port, connector_addr).await?; + match connector_addr { + SocketAddr::V4(addr) => udp::send_v4_hole_punch_packet(listener_port, addr).await?, + SocketAddr::V6(addr) => udp::send_v6_hole_punch_packet(listener_port, addr).await?, + } tokio::time::sleep(std::time::Duration::from_millis(30)).await; } Ok(Default::default()) diff --git a/easytier/src/proto/api_manage.proto b/easytier/src/proto/api_manage.proto index 0641672f..ab8a648c 100644 --- a/easytier/src/proto/api_manage.proto +++ b/easytier/src/proto/api_manage.proto @@ -89,6 +89,7 @@ message NetworkConfig { optional bool lazy_p2p = 58; optional bool need_p2p = 59; optional uint64 instance_recv_bps_limit = 60; + optional bool disable_upnp = 61; } message PortForwardConfig { diff --git a/easytier/src/proto/common.proto b/easytier/src/proto/common.proto index 91e33d35..58d105a7 100644 --- a/easytier/src/proto/common.proto +++ b/easytier/src/proto/common.proto @@ -74,6 +74,7 @@ message FlagsInConfig { bool lazy_p2p = 37; bool need_p2p = 38; uint64 instance_recv_bps_limit = 39; + bool disable_upnp = 40; } message RpcDescriptor { diff --git a/easytier/src/proto/peer_rpc.proto b/easytier/src/proto/peer_rpc.proto index 5ededd12..f3dfde6c 100644 --- a/easytier/src/proto/peer_rpc.proto +++ b/easytier/src/proto/peer_rpc.proto @@ -142,18 +142,19 @@ message GetIpListResponse { repeated common.Url listeners = 5; } -message SendV6HolePunchPacketRequest { +message SendUdpHolePunchPacketRequest { common.SocketAddr connector_addr = 1; uint32 listener_port = 2; } service DirectConnectorRpc { rpc GetIpList(GetIpListRequest) returns (GetIpListResponse); - rpc SendV6HolePunchPacket(SendV6HolePunchPacketRequest) returns (common.Void); + rpc SendUdpHolePunchPacket(SendUdpHolePunchPacketRequest) returns (common.Void); } message SelectPunchListenerRequest { bool force_new = 1; + bool prefer_port_mapping = 2; } message SelectPunchListenerResponse { diff --git a/easytier/src/tests/mod.rs b/easytier/src/tests/mod.rs index 4c983252..893befaf 100644 --- a/easytier/src/tests/mod.rs +++ b/easytier/src/tests/mod.rs @@ -6,6 +6,9 @@ mod ipv6_test; #[cfg(target_os = "linux")] mod credential_tests; +#[cfg(target_os = "linux")] +mod upnp_test; + use crate::common::PeerId; use crate::peers::peer_manager::PeerManager; diff --git a/easytier/src/tests/upnp_test.rs b/easytier/src/tests/upnp_test.rs new file mode 100644 index 00000000..8e12c72c --- /dev/null +++ b/easytier/src/tests/upnp_test.rs @@ -0,0 +1,1952 @@ +use std::{ + ffi::OsStr, + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, + sync::Arc, + time::Duration, +}; + +use anyhow::{Context, anyhow, bail}; +use igd_next::{ + GetGenericPortMappingEntryError, PortMappingEntry, PortMappingProtocol, SearchOptions, + aio::tokio::search_gateway, +}; +use tempfile::TempDir; +use tokio::net::UdpSocket; + +use super::{create_netns, del_netns, drop_insts, get_host_veth_name, ping_test}; +use crate::{ + common::{ + config::{ConfigLoader, TomlConfigLoader}, + error::Error, + global_ctx::{GlobalCtx, GlobalCtxEvent}, + netns::NetNS, + stun::{MockStunInfoCollector, StunInfoCollectorTrait}, + }, + connector::udp_hole_punch::{UdpHolePunchConnector, common::UdpHolePunchListener}, + instance::instance::Instance, + peers::{ + create_packet_recv_chan, + peer_manager::{PeerManager, RouteAlgoType}, + tests::{connect_peer_manager, wait_route_appear, wait_route_appear_with_cost}, + }, + proto::common::{NatType, StunInfo}, + tunnel::{common::tests::wait_for_condition, ring::RingTunnelConnector}, +}; + +const TEST_NS_A: &str = "upnp_a"; +const TEST_NS_C: &str = "upnp_c"; +const TEST_BRIDGE: &str = "br_upnp"; +const TEST_WAN_IF: &str = "upnp_wan0"; +const TEST_GATEWAY_IP: Ipv4Addr = Ipv4Addr::new(172, 31, 255, 1); +const TEST_CLIENT_A_IP: Ipv4Addr = Ipv4Addr::new(172, 31, 255, 2); +const TEST_CLIENT_C_IP: Ipv4Addr = Ipv4Addr::new(172, 31, 255, 3); +const TEST_EXTERNAL_IP: Ipv4Addr = Ipv4Addr::new(11, 22, 33, 44); +const TEST_CONTROL_PORT: u16 = 5000; +const TEST_IGD_DESCRIPTION: &str = "EasyTier udp hole punch"; + +const DUAL_NS_A: &str = "upnp2_a"; +const DUAL_NS_C: &str = "upnp2_c"; +const DUAL_LAN_A_BRIDGE: &str = "br_upnp2_a"; +const DUAL_LAN_C_BRIDGE: &str = "br_upnp2_c"; +const DUAL_WAN_BRIDGE: &str = "br_upnp2_wan"; +const DUAL_GATEWAY_A_IP: Ipv4Addr = Ipv4Addr::new(172, 30, 1, 1); +const DUAL_GATEWAY_C_IP: Ipv4Addr = Ipv4Addr::new(172, 30, 2, 1); +const DUAL_CLIENT_A_IP: Ipv4Addr = Ipv4Addr::new(172, 30, 1, 2); +const DUAL_CLIENT_C_IP: Ipv4Addr = Ipv4Addr::new(172, 30, 2, 2); +const DUAL_EXTERNAL_A_IP: Ipv4Addr = Ipv4Addr::new(11, 22, 33, 1); +const DUAL_EXTERNAL_C_IP: Ipv4Addr = Ipv4Addr::new(11, 22, 33, 2); +const DUAL_GATEWAY_A_PORT: u16 = 5001; +const DUAL_GATEWAY_C_PORT: u16 = 5002; +const DUAL_WAN_IF_A: &str = "upnp2_wan_a"; +const DUAL_WAN_IF_A_PEER: &str = "upnp2_wan_a_p"; +const DUAL_WAN_IF_C: &str = "upnp2_wan_c"; +const DUAL_WAN_IF_C_PEER: &str = "upnp2_wan_c_p"; +const DUAL_GW_NS_A: &str = "upnp2_gw_a"; +const DUAL_GW_NS_C: &str = "upnp2_gw_c"; + +struct UpnpIntegrationEnv { + _tempdir: TempDir, + child: Option, +} + +impl UpnpIntegrationEnv { + async fn new() -> anyhow::Result { + cleanup_miniupnpd_processes(); + cleanup_test_net(); + create_test_net()?; + + let tempdir = tempfile::tempdir().context("create miniupnpd tempdir")?; + let conf_path = tempdir.path().join("miniupnpd.conf"); + let leases_path = tempdir.path().join("miniupnpd.leases"); + std::fs::write(&leases_path, "").context("create miniupnpd lease file")?; + std::fs::write( + &conf_path, + format!( + "\ +ext_ifname={TEST_WAN_IF} +listening_ip={TEST_BRIDGE} +port={TEST_CONTROL_PORT} +enable_natpmp=no +enable_upnp=yes +secure_mode=no +system_uptime=yes +lease_file={} +ext_ip={} +friendly_name=EasyTier Test IGD +model_name=EasyTier Test +serial=12345678 +uuid=9f0c5a3a-c4f0-4f1e-b4df-8a8c7b1e2d00 +allow 1024-65535 172.31.255.0/24 1024-65535 +deny 0-65535 0.0.0.0/0 0-65535 +", + leases_path.display(), + TEST_EXTERNAL_IP + ), + ) + .context("write miniupnpd config")?; + + let miniupnpd_bin = find_miniupnpd_bin()?; + let child = Command::new(miniupnpd_bin) + .args(["-d", "-f"]) + .arg(&conf_path) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .context("spawn miniupnpd")?; + + let env = Self { + _tempdir: tempdir, + child: Some(child), + }; + env.wait_ready().await?; + Ok(env) + } + + async fn wait_ready(&self) -> anyhow::Result<()> { + wait_for_condition( + || async { + tokio::net::TcpStream::connect((TEST_GATEWAY_IP, TEST_CONTROL_PORT)) + .await + .is_ok() + }, + Duration::from_secs(10), + ) + .await; + Ok(()) + } +} + +impl Drop for UpnpIntegrationEnv { + fn drop(&mut self) { + if let Some(mut child) = self.child.take() { + let _ = child.kill(); + let _ = child.wait(); + } + cleanup_miniupnpd_processes(); + cleanup_test_net(); + } +} + +struct DualGatewayUpnpIntegrationEnv { + _tempdir: TempDir, + children: Vec, +} + +struct DualGatewayConfig<'a> { + name: &'a str, + ext_if: &'a str, + listening_ip: &'a str, + port: u16, + ext_ip: Ipv4Addr, + uuid: &'a str, + chain_name: &'a str, + postrouting_chain_name: &'a str, + allow_subnet: &'a str, +} + +impl DualGatewayUpnpIntegrationEnv { + async fn new() -> anyhow::Result { + cleanup_miniupnpd_processes(); + cleanup_dual_gateway_test_net(); + create_dual_gateway_test_net()?; + + let tempdir = tempfile::tempdir().context("create dual miniupnpd tempdir")?; + let gateway_a = write_dual_gateway_config( + tempdir.path(), + DualGatewayConfig { + name: "gateway_a", + ext_if: DUAL_WAN_IF_A, + listening_ip: DUAL_LAN_A_BRIDGE, + port: DUAL_GATEWAY_A_PORT, + ext_ip: DUAL_EXTERNAL_A_IP, + uuid: "9f0c5a3a-c4f0-4f1e-b4df-8a8c7b1e2d01", + chain_name: "MINIUPNPD_A", + postrouting_chain_name: "MINIUPNPD_A-POSTROUTING", + allow_subnet: "172.30.1.0/24", + }, + )?; + let gateway_c = write_dual_gateway_config( + tempdir.path(), + DualGatewayConfig { + name: "gateway_c", + ext_if: DUAL_WAN_IF_C, + listening_ip: DUAL_LAN_C_BRIDGE, + port: DUAL_GATEWAY_C_PORT, + ext_ip: DUAL_EXTERNAL_C_IP, + uuid: "9f0c5a3a-c4f0-4f1e-b4df-8a8c7b1e2d02", + chain_name: "MINIUPNPD_C", + postrouting_chain_name: "MINIUPNPD_C-POSTROUTING", + allow_subnet: "172.30.2.0/24", + }, + )?; + let pid_a = tempdir.path().join("gateway_a.pid"); + let pid_c = tempdir.path().join("gateway_c.pid"); + + let miniupnpd_bin = find_miniupnpd_bin()?; + let miniupnpd_bin_str = miniupnpd_bin + .to_str() + .ok_or_else(|| anyhow!("non-utf8 miniupnpd path: {}", miniupnpd_bin.display()))?; + let pid_a_str = pid_a + .to_str() + .ok_or_else(|| anyhow!("non-utf8 pid path: {}", pid_a.display()))?; + let pid_c_str = pid_c + .to_str() + .ok_or_else(|| anyhow!("non-utf8 pid path: {}", pid_c.display()))?; + let child_a = Command::new("ip") + .args([ + "netns", + "exec", + DUAL_GW_NS_A, + miniupnpd_bin_str, + "-d", + "-P", + pid_a_str, + "-f", + ]) + .arg(&gateway_a) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .context("spawn gateway a miniupnpd")?; + let child_c = Command::new("ip") + .args([ + "netns", + "exec", + DUAL_GW_NS_C, + miniupnpd_bin_str, + "-d", + "-P", + pid_c_str, + "-f", + ]) + .arg(&gateway_c) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .context("spawn gateway c miniupnpd")?; + + let env = Self { + _tempdir: tempdir, + children: vec![child_a, child_c], + }; + env.wait_ready().await?; + Ok(env) + } + + async fn wait_ready(&self) -> anyhow::Result<()> { + wait_for_condition( + || async { + tcp_connect_in_ns(DUAL_NS_A, DUAL_GATEWAY_A_IP, DUAL_GATEWAY_A_PORT) + .await + .is_ok() + && tcp_connect_in_ns(DUAL_NS_C, DUAL_GATEWAY_C_IP, DUAL_GATEWAY_C_PORT) + .await + .is_ok() + }, + Duration::from_secs(10), + ) + .await; + Ok(()) + } +} + +impl Drop for DualGatewayUpnpIntegrationEnv { + fn drop(&mut self) { + for child in &mut self.children { + let _ = child.kill(); + let _ = child.wait(); + } + cleanup_miniupnpd_processes(); + cleanup_dual_gateway_test_net(); + } +} + +#[derive(Clone)] +struct GatewayBackedStunCollector { + netns: &'static str, + client_ip: Ipv4Addr, + external_ip: Ipv4Addr, +} + +#[async_trait::async_trait] +impl StunInfoCollectorTrait for GatewayBackedStunCollector { + fn get_stun_info(&self) -> StunInfo { + StunInfo { + udp_nat_type: NatType::PortRestricted as i32, + tcp_nat_type: NatType::Unknown as i32, + last_update_time: 0, + min_port: 0, + max_port: 0, + public_ip: vec![self.external_ip.to_string()], + } + } + + async fn get_udp_port_mapping(&self, local_port: u16) -> Result { + query_udp_mapping(self.netns, self.external_ip, self.client_ip, local_port).await + } + + async fn get_udp_port_mapping_with_socket( + &self, + udp: Arc, + ) -> Result { + query_udp_mapping( + self.netns, + self.external_ip, + self.client_ip, + udp.local_addr()?.port(), + ) + .await + } + + async fn get_tcp_port_mapping(&self, local_port: u16) -> Result { + Ok(SocketAddr::new(IpAddr::V4(self.external_ip), local_port)) + } +} + +fn create_test_net() -> anyhow::Result<()> { + create_netns(TEST_NS_A, &format!("{TEST_CLIENT_A_IP}/24"), "fd10::2/64"); + create_netns(TEST_NS_C, &format!("{TEST_CLIENT_C_IP}/24"), "fd10::3/64"); + run_cmd( + "ip", + &["link", "add", "name", TEST_BRIDGE, "type", "bridge"], + )?; + for netns in [TEST_NS_A, TEST_NS_C] { + run_cmd( + "ip", + &[ + "link", + "set", + get_host_veth_name(netns), + "master", + TEST_BRIDGE, + ], + )?; + } + + run_cmd( + "ip", + &[ + "addr", + "add", + &format!("{TEST_GATEWAY_IP}/24"), + "dev", + TEST_BRIDGE, + ], + )?; + run_cmd("ip", &["link", "add", TEST_WAN_IF, "type", "dummy"])?; + run_cmd( + "ip", + &[ + "addr", + "add", + &format!("{TEST_EXTERNAL_IP}/24"), + "dev", + TEST_WAN_IF, + ], + )?; + run_cmd("ip", &["link", "set", TEST_WAN_IF, "up"])?; + run_cmd("ip", &["link", "set", TEST_BRIDGE, "up"])?; + setup_iptables_rules()?; + for (netns, guest_veth) in [(TEST_NS_A, "veth_upnp_a_g"), (TEST_NS_C, "veth_upnp_c_g")] { + run_cmd( + "ip", + &[ + "netns", + "exec", + netns, + "ip", + "route", + "add", + "default", + "via", + &TEST_GATEWAY_IP.to_string(), + "dev", + guest_veth, + ], + )?; + } + run_cmd("sysctl", &["-w", "net.ipv4.ip_forward=1"])?; + Ok(()) +} + +fn cleanup_test_net() { + cleanup_iptables_rules(); + del_netns(TEST_NS_A); + del_netns(TEST_NS_C); + let _ = Command::new("ip") + .args(["link", "del", TEST_BRIDGE]) + .output(); + let _ = Command::new("ip") + .args(["link", "del", TEST_WAN_IF]) + .output(); +} + +fn write_dual_gateway_config(dir: &Path, config: DualGatewayConfig<'_>) -> anyhow::Result { + let conf_path = dir.join(format!("{}.conf", config.name)); + let lease_path = dir.join(format!("{}.leases", config.name)); + std::fs::write(&lease_path, "") + .with_context(|| format!("create lease file for {}", config.name))?; + std::fs::write( + &conf_path, + format!( + "\ +ext_ifname={ext_if} +listening_ip={listening_ip} +port={port} +enable_natpmp=no +enable_upnp=yes +secure_mode=no +system_uptime=yes +lease_file={} +ext_ip={ext_ip} +friendly_name=EasyTier Test IGD {name} +model_name=EasyTier Test +serial=12345678 +uuid={uuid} +upnp_forward_chain={chain_name} +upnp_nat_chain={chain_name} +upnp_nat_postrouting_chain={postrouting_chain_name} +allow 1024-65535 {allow_subnet} 1024-65535 +deny 0-65535 0.0.0.0/0 0-65535 +", + lease_path.display(), + ext_if = config.ext_if, + listening_ip = config.listening_ip, + port = config.port, + ext_ip = config.ext_ip, + name = config.name, + uuid = config.uuid, + chain_name = config.chain_name, + postrouting_chain_name = config.postrouting_chain_name, + allow_subnet = config.allow_subnet, + ), + ) + .with_context(|| format!("write config for {}", config.name))?; + Ok(conf_path) +} + +async fn tcp_connect_in_ns(netns: &'static str, ip: Ipv4Addr, port: u16) -> anyhow::Result<()> { + tokio::task::spawn_blocking(move || { + let _g = NetNS::new(Some(netns.to_owned())).guard(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("build runtime for tcp connect in netns")? + .block_on(async move { + tokio::net::TcpStream::connect((ip, port)) + .await + .map(|_| ()) + .context("tcp connect in netns") + }) + }) + .await + .context("join tcp connect in netns task")? +} + +fn create_empty_netns(name: &str) -> anyhow::Result<()> { + run_cmd("ip", &["netns", "add", name])?; + run_cmd( + "ip", + &["netns", "exec", name, "ip", "link", "set", "lo", "up"], + )?; + Ok(()) +} + +fn run_cmd_in_netns>(netns: &str, cmd: S, args: &[&str]) -> anyhow::Result<()> { + let cmd = cmd.as_ref(); + let cmd_str = cmd + .to_str() + .ok_or_else(|| anyhow!("non-utf8 command path: {}", Path::new(cmd).display()))?; + let mut full_args = vec!["netns", "exec", netns, cmd_str]; + full_args.extend_from_slice(args); + run_cmd("ip", &full_args) +} + +fn create_dual_gateway_test_net() -> anyhow::Result<()> { + create_netns(DUAL_NS_A, &format!("{DUAL_CLIENT_A_IP}/24"), "fd20::2/64"); + create_netns(DUAL_NS_C, &format!("{DUAL_CLIENT_C_IP}/24"), "fd21::2/64"); + create_empty_netns(DUAL_GW_NS_A)?; + create_empty_netns(DUAL_GW_NS_C)?; + + run_cmd( + "ip", + &[ + "link", + "set", + get_host_veth_name(DUAL_NS_A), + "netns", + DUAL_GW_NS_A, + ], + )?; + run_cmd( + "ip", + &[ + "link", + "set", + get_host_veth_name(DUAL_NS_C), + "netns", + DUAL_GW_NS_C, + ], + )?; + + run_cmd_in_netns( + DUAL_GW_NS_A, + "ip", + &["link", "add", "name", DUAL_LAN_A_BRIDGE, "type", "bridge"], + )?; + run_cmd_in_netns( + DUAL_GW_NS_C, + "ip", + &["link", "add", "name", DUAL_LAN_C_BRIDGE, "type", "bridge"], + )?; + run_cmd( + "ip", + &["link", "add", "name", DUAL_WAN_BRIDGE, "type", "bridge"], + )?; + + run_cmd_in_netns( + DUAL_GW_NS_A, + "ip", + &[ + "link", + "set", + get_host_veth_name(DUAL_NS_A), + "master", + DUAL_LAN_A_BRIDGE, + ], + )?; + run_cmd_in_netns( + DUAL_GW_NS_C, + "ip", + &[ + "link", + "set", + get_host_veth_name(DUAL_NS_C), + "master", + DUAL_LAN_C_BRIDGE, + ], + )?; + run_cmd_in_netns( + DUAL_GW_NS_A, + "ip", + &[ + "addr", + "add", + &format!("{DUAL_GATEWAY_A_IP}/24"), + "dev", + DUAL_LAN_A_BRIDGE, + ], + )?; + run_cmd_in_netns( + DUAL_GW_NS_C, + "ip", + &[ + "addr", + "add", + &format!("{DUAL_GATEWAY_C_IP}/24"), + "dev", + DUAL_LAN_C_BRIDGE, + ], + )?; + + run_cmd( + "ip", + &[ + "link", + "add", + DUAL_WAN_IF_A, + "type", + "veth", + "peer", + "name", + DUAL_WAN_IF_A_PEER, + ], + )?; + run_cmd( + "ip", + &[ + "link", + "add", + DUAL_WAN_IF_C, + "type", + "veth", + "peer", + "name", + DUAL_WAN_IF_C_PEER, + ], + )?; + run_cmd("ip", &["link", "set", DUAL_WAN_IF_A, "netns", DUAL_GW_NS_A])?; + run_cmd("ip", &["link", "set", DUAL_WAN_IF_C, "netns", DUAL_GW_NS_C])?; + run_cmd( + "ip", + &["link", "set", DUAL_WAN_IF_A_PEER, "master", DUAL_WAN_BRIDGE], + )?; + run_cmd( + "ip", + &["link", "set", DUAL_WAN_IF_C_PEER, "master", DUAL_WAN_BRIDGE], + )?; + run_cmd_in_netns( + DUAL_GW_NS_A, + "ip", + &[ + "addr", + "add", + &format!("{DUAL_EXTERNAL_A_IP}/24"), + "dev", + DUAL_WAN_IF_A, + ], + )?; + run_cmd_in_netns( + DUAL_GW_NS_C, + "ip", + &[ + "addr", + "add", + &format!("{DUAL_EXTERNAL_C_IP}/24"), + "dev", + DUAL_WAN_IF_C, + ], + )?; + run_cmd("ip", &["link", "set", DUAL_WAN_BRIDGE, "up"])?; + run_cmd("ip", &["link", "set", DUAL_WAN_IF_A_PEER, "up"])?; + run_cmd("ip", &["link", "set", DUAL_WAN_IF_C_PEER, "up"])?; + run_cmd_in_netns( + DUAL_GW_NS_A, + "ip", + &["link", "set", DUAL_LAN_A_BRIDGE, "up"], + )?; + run_cmd_in_netns( + DUAL_GW_NS_A, + "ip", + &["link", "set", get_host_veth_name(DUAL_NS_A), "up"], + )?; + run_cmd_in_netns(DUAL_GW_NS_A, "ip", &["link", "set", DUAL_WAN_IF_A, "up"])?; + run_cmd_in_netns( + DUAL_GW_NS_C, + "ip", + &["link", "set", DUAL_LAN_C_BRIDGE, "up"], + )?; + run_cmd_in_netns( + DUAL_GW_NS_C, + "ip", + &["link", "set", get_host_veth_name(DUAL_NS_C), "up"], + )?; + run_cmd_in_netns(DUAL_GW_NS_C, "ip", &["link", "set", DUAL_WAN_IF_C, "up"])?; + + setup_gateway_iptables_rules_in_netns( + DUAL_GW_NS_A, + DUAL_WAN_IF_A, + DUAL_LAN_A_BRIDGE, + "MINIUPNPD_A", + "MINIUPNPD_A-POSTROUTING", + )?; + setup_gateway_iptables_rules_in_netns( + DUAL_GW_NS_C, + DUAL_WAN_IF_C, + DUAL_LAN_C_BRIDGE, + "MINIUPNPD_C", + "MINIUPNPD_C-POSTROUTING", + )?; + + run_cmd( + "ip", + &[ + "netns", + "exec", + DUAL_NS_A, + "ip", + "route", + "add", + "default", + "via", + &DUAL_GATEWAY_A_IP.to_string(), + "dev", + "veth_upnp2_a_g", + ], + )?; + run_cmd( + "ip", + &[ + "netns", + "exec", + DUAL_NS_C, + "ip", + "route", + "add", + "default", + "via", + &DUAL_GATEWAY_C_IP.to_string(), + "dev", + "veth_upnp2_c_g", + ], + )?; + run_cmd_in_netns(DUAL_GW_NS_A, "sysctl", &["-w", "net.ipv4.ip_forward=1"])?; + run_cmd_in_netns(DUAL_GW_NS_C, "sysctl", &["-w", "net.ipv4.ip_forward=1"])?; + Ok(()) +} + +fn cleanup_dual_gateway_test_net() { + cleanup_gateway_iptables_rules_in_netns( + DUAL_GW_NS_A, + DUAL_WAN_IF_A, + DUAL_LAN_A_BRIDGE, + "MINIUPNPD_A", + "MINIUPNPD_A-POSTROUTING", + ); + cleanup_gateway_iptables_rules_in_netns( + DUAL_GW_NS_C, + DUAL_WAN_IF_C, + DUAL_LAN_C_BRIDGE, + "MINIUPNPD_C", + "MINIUPNPD_C-POSTROUTING", + ); + + del_netns(DUAL_NS_A); + del_netns(DUAL_NS_C); + del_netns(DUAL_GW_NS_A); + del_netns(DUAL_GW_NS_C); + for iface in [DUAL_WAN_BRIDGE, DUAL_WAN_IF_A_PEER, DUAL_WAN_IF_C_PEER] { + let _ = Command::new("ip").args(["link", "del", iface]).output(); + } +} + +fn cleanup_miniupnpd_processes() { + let _ = Command::new("pkill").args(["-x", "miniupnpd"]).output(); +} + +fn setup_gateway_iptables_rules( + ext_if: &str, + lan_bridge: &str, + chain_name: &str, + postrouting_chain_name: &str, +) -> anyhow::Result<()> { + cleanup_gateway_iptables_rules(ext_if, lan_bridge, chain_name, postrouting_chain_name); + let iptables = find_iptables_legacy_bin()?; + + run_cmd(&iptables, &["-t", "nat", "-N", chain_name])?; + run_cmd( + &iptables, + &[ + "-t", + "nat", + "-A", + "PREROUTING", + "-i", + ext_if, + "-j", + chain_name, + ], + )?; + run_cmd(&iptables, &["-t", "nat", "-N", postrouting_chain_name])?; + run_cmd( + &iptables, + &[ + "-t", + "nat", + "-A", + "POSTROUTING", + "-o", + ext_if, + "-j", + postrouting_chain_name, + ], + )?; + run_cmd(&iptables, &["-t", "mangle", "-N", chain_name])?; + run_cmd( + &iptables, + &[ + "-t", + "mangle", + "-A", + "PREROUTING", + "-i", + ext_if, + "-j", + chain_name, + ], + )?; + run_cmd(&iptables, &["-N", chain_name])?; + run_cmd( + &iptables, + &[ + "-A", "FORWARD", "-i", ext_if, "!", "-o", ext_if, "-j", chain_name, + ], + )?; + run_cmd( + &iptables, + &[ + "-A", "FORWARD", "-i", lan_bridge, "-o", ext_if, "-j", "ACCEPT", + ], + )?; + Ok(()) +} + +fn cleanup_gateway_iptables_rules( + ext_if: &str, + lan_bridge: &str, + chain_name: &str, + postrouting_chain_name: &str, +) { + let Ok(iptables) = find_iptables_legacy_bin() else { + return; + }; + + let _ = Command::new(&iptables) + .args([ + "-t", + "nat", + "-D", + "PREROUTING", + "-i", + ext_if, + "-j", + chain_name, + ]) + .output(); + let _ = Command::new(&iptables) + .args([ + "-t", + "nat", + "-D", + "POSTROUTING", + "-o", + ext_if, + "-j", + postrouting_chain_name, + ]) + .output(); + let _ = Command::new(&iptables) + .args([ + "-t", + "mangle", + "-D", + "PREROUTING", + "-i", + ext_if, + "-j", + chain_name, + ]) + .output(); + let _ = Command::new(&iptables) + .args([ + "-D", "FORWARD", "-i", ext_if, "!", "-o", ext_if, "-j", chain_name, + ]) + .output(); + let _ = Command::new(&iptables) + .args([ + "-D", "FORWARD", "-i", lan_bridge, "-o", ext_if, "-j", "ACCEPT", + ]) + .output(); + let _ = Command::new(&iptables).args(["-F", chain_name]).output(); + let _ = Command::new(&iptables).args(["-X", chain_name]).output(); + let _ = Command::new(&iptables) + .args(["-t", "mangle", "-F", chain_name]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "mangle", "-X", chain_name]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "nat", "-F", chain_name]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "nat", "-X", chain_name]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "nat", "-F", postrouting_chain_name]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "nat", "-X", postrouting_chain_name]) + .output(); +} + +fn setup_gateway_iptables_rules_in_netns( + netns: &str, + ext_if: &str, + lan_bridge: &str, + chain_name: &str, + postrouting_chain_name: &str, +) -> anyhow::Result<()> { + cleanup_gateway_iptables_rules_in_netns( + netns, + ext_if, + lan_bridge, + chain_name, + postrouting_chain_name, + ); + let iptables = find_iptables_legacy_bin()?; + + run_cmd_in_netns(netns, &iptables, &["-t", "nat", "-N", chain_name])?; + run_cmd_in_netns( + netns, + &iptables, + &[ + "-t", + "nat", + "-A", + "PREROUTING", + "-i", + ext_if, + "-j", + chain_name, + ], + )?; + run_cmd_in_netns( + netns, + &iptables, + &["-t", "nat", "-N", postrouting_chain_name], + )?; + run_cmd_in_netns( + netns, + &iptables, + &[ + "-t", + "nat", + "-A", + "POSTROUTING", + "-o", + ext_if, + "-j", + postrouting_chain_name, + ], + )?; + run_cmd_in_netns(netns, &iptables, &["-t", "mangle", "-N", chain_name])?; + run_cmd_in_netns( + netns, + &iptables, + &[ + "-t", + "mangle", + "-A", + "PREROUTING", + "-i", + ext_if, + "-j", + chain_name, + ], + )?; + run_cmd_in_netns(netns, &iptables, &["-N", chain_name])?; + run_cmd_in_netns( + netns, + &iptables, + &[ + "-A", "FORWARD", "-i", ext_if, "!", "-o", ext_if, "-j", chain_name, + ], + )?; + run_cmd_in_netns( + netns, + &iptables, + &[ + "-A", "FORWARD", "-i", lan_bridge, "-o", ext_if, "-j", "ACCEPT", + ], + )?; + Ok(()) +} + +fn cleanup_gateway_iptables_rules_in_netns( + netns: &str, + ext_if: &str, + lan_bridge: &str, + chain_name: &str, + postrouting_chain_name: &str, +) { + let Ok(iptables) = find_iptables_legacy_bin() else { + return; + }; + + let _ = run_cmd_in_netns( + netns, + &iptables, + &[ + "-t", + "nat", + "-D", + "PREROUTING", + "-i", + ext_if, + "-j", + chain_name, + ], + ); + let _ = run_cmd_in_netns( + netns, + &iptables, + &[ + "-t", + "nat", + "-D", + "POSTROUTING", + "-o", + ext_if, + "-j", + postrouting_chain_name, + ], + ); + let _ = run_cmd_in_netns( + netns, + &iptables, + &[ + "-t", + "mangle", + "-D", + "PREROUTING", + "-i", + ext_if, + "-j", + chain_name, + ], + ); + let _ = run_cmd_in_netns( + netns, + &iptables, + &[ + "-D", "FORWARD", "-i", ext_if, "!", "-o", ext_if, "-j", chain_name, + ], + ); + let _ = run_cmd_in_netns( + netns, + &iptables, + &[ + "-D", "FORWARD", "-i", lan_bridge, "-o", ext_if, "-j", "ACCEPT", + ], + ); + let _ = run_cmd_in_netns(netns, &iptables, &["-F", chain_name]); + let _ = run_cmd_in_netns(netns, &iptables, &["-X", chain_name]); + let _ = run_cmd_in_netns(netns, &iptables, &["-t", "mangle", "-F", chain_name]); + let _ = run_cmd_in_netns(netns, &iptables, &["-t", "mangle", "-X", chain_name]); + let _ = run_cmd_in_netns(netns, &iptables, &["-t", "nat", "-F", chain_name]); + let _ = run_cmd_in_netns(netns, &iptables, &["-t", "nat", "-X", chain_name]); + let _ = run_cmd_in_netns( + netns, + &iptables, + &["-t", "nat", "-F", postrouting_chain_name], + ); + let _ = run_cmd_in_netns( + netns, + &iptables, + &["-t", "nat", "-X", postrouting_chain_name], + ); +} + +fn setup_iptables_rules() -> anyhow::Result<()> { + cleanup_iptables_rules(); + let iptables = find_iptables_legacy_bin()?; + + run_cmd(&iptables, &["-t", "nat", "-N", "MINIUPNPD"])?; + run_cmd( + &iptables, + &[ + "-t", + "nat", + "-A", + "PREROUTING", + "-i", + TEST_WAN_IF, + "-j", + "MINIUPNPD", + ], + )?; + run_cmd(&iptables, &["-t", "nat", "-N", "MINIUPNPD-POSTROUTING"])?; + run_cmd( + &iptables, + &[ + "-t", + "nat", + "-A", + "POSTROUTING", + "-o", + TEST_WAN_IF, + "-j", + "MINIUPNPD-POSTROUTING", + ], + )?; + + run_cmd(&iptables, &["-t", "mangle", "-N", "MINIUPNPD"])?; + run_cmd( + &iptables, + &[ + "-t", + "mangle", + "-A", + "PREROUTING", + "-i", + TEST_WAN_IF, + "-j", + "MINIUPNPD", + ], + )?; + + run_cmd(&iptables, &["-N", "MINIUPNPD"])?; + run_cmd( + &iptables, + &[ + "-A", + "FORWARD", + "-i", + TEST_WAN_IF, + "!", + "-o", + TEST_WAN_IF, + "-j", + "MINIUPNPD", + ], + )?; + run_cmd( + &iptables, + &[ + "-A", + "FORWARD", + "-i", + TEST_BRIDGE, + "-o", + TEST_WAN_IF, + "-j", + "ACCEPT", + ], + )?; + + Ok(()) +} + +fn cleanup_iptables_rules() { + let Ok(iptables) = find_iptables_legacy_bin() else { + return; + }; + + let _ = Command::new(&iptables) + .args([ + "-t", + "nat", + "-D", + "PREROUTING", + "-i", + TEST_WAN_IF, + "-j", + "MINIUPNPD", + ]) + .output(); + let _ = Command::new(&iptables) + .args([ + "-t", + "nat", + "-D", + "POSTROUTING", + "-o", + TEST_WAN_IF, + "-j", + "MINIUPNPD-POSTROUTING", + ]) + .output(); + let _ = Command::new(&iptables) + .args([ + "-t", + "mangle", + "-D", + "PREROUTING", + "-i", + TEST_WAN_IF, + "-j", + "MINIUPNPD", + ]) + .output(); + let _ = Command::new(&iptables) + .args([ + "-D", + "FORWARD", + "-i", + TEST_WAN_IF, + "!", + "-o", + TEST_WAN_IF, + "-j", + "MINIUPNPD", + ]) + .output(); + let _ = Command::new(&iptables) + .args([ + "-D", + "FORWARD", + "-i", + TEST_BRIDGE, + "-o", + TEST_WAN_IF, + "-j", + "ACCEPT", + ]) + .output(); + let _ = Command::new(&iptables).args(["-F", "MINIUPNPD"]).output(); + let _ = Command::new(&iptables).args(["-X", "MINIUPNPD"]).output(); + let _ = Command::new(&iptables) + .args(["-t", "mangle", "-F", "MINIUPNPD"]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "mangle", "-X", "MINIUPNPD"]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "nat", "-F", "MINIUPNPD"]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "nat", "-X", "MINIUPNPD"]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "nat", "-F", "MINIUPNPD-POSTROUTING"]) + .output(); + let _ = Command::new(&iptables) + .args(["-t", "nat", "-X", "MINIUPNPD-POSTROUTING"]) + .output(); +} + +fn run_cmd>(cmd: S, args: &[&str]) -> anyhow::Result<()> { + let cmd = cmd.as_ref(); + let output = Command::new(cmd) + .args(args) + .output() + .with_context(|| format!("run command {}", Path::new(cmd).display()))?; + if output.status.success() { + return Ok(()); + } + + Err(anyhow!( + "{} {:?} failed: stdout={}, stderr={}", + Path::new(cmd).display(), + args, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )) +} + +fn find_miniupnpd_bin() -> anyhow::Result { + for candidate in ["miniupnpd", "/usr/sbin/miniupnpd", "/sbin/miniupnpd"] { + let path = Path::new(candidate); + if candidate.contains('/') { + if path.exists() { + return Ok(path.to_path_buf()); + } + continue; + } + + if Command::new("sh") + .args(["-c", &format!("command -v {candidate}")]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|status| status.success()) + .unwrap_or(false) + { + return Ok(path.to_path_buf()); + } + } + + bail!("miniupnpd binary not found; install miniupnpd and miniupnpd-iptables") +} + +fn find_iptables_legacy_bin() -> anyhow::Result { + for candidate in [ + "/usr/sbin/iptables-legacy", + "/sbin/iptables-legacy", + "iptables-legacy", + ] { + let path = Path::new(candidate); + if candidate.contains('/') { + if path.exists() { + return Ok(path.to_path_buf()); + } + continue; + } + + if Command::new("sh") + .args(["-c", &format!("command -v {candidate}")]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|status| status.success()) + .unwrap_or(false) + { + return Ok(path.to_path_buf()); + } + } + + bail!("iptables-legacy binary not found; install iptables legacy backend") +} + +async fn query_mappings( + query_netns: &'static str, + expected_external_ip: Ipv4Addr, +) -> Result, Error> { + tokio::task::spawn_blocking(move || { + let _g = NetNS::new(Some(query_netns.to_owned())).guard(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("build runtime for test igd query")? + .block_on(async move { + let gateway = search_gateway(SearchOptions { + timeout: Some(Duration::from_secs(1)), + single_search_timeout: Some(Duration::from_millis(300)), + ..Default::default() + }) + .await + .context("discover igd gateway")?; + + let external_ip = + match gateway.get_external_ip().await.context("get external ip")? { + IpAddr::V4(ip) => ip, + ip => return Err(anyhow!("unexpected external ip family: {ip}")), + }; + + let mut entries = Vec::new(); + for index in 0..64 { + match gateway.get_generic_port_mapping_entry(index).await { + Ok(entry) => { + if entry.protocol == PortMappingProtocol::UDP + && entry.port_mapping_description == TEST_IGD_DESCRIPTION + { + entries.push(entry); + } + } + Err(GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid) => break, + Err(err) => return Err(anyhow!(err.to_string())), + } + } + + if external_ip != expected_external_ip { + return Err(anyhow!( + "unexpected external ip from gateway: expected {expected_external_ip}, got {external_ip}" + )); + } + + Ok(entries) + }) + }) + .await + .context("join test igd query task") + .map_err(Error::from)? + .map_err(Error::from) +} + +async fn query_udp_mapping( + query_netns: &'static str, + expected_external_ip: Ipv4Addr, + client_ip: Ipv4Addr, + local_port: u16, +) -> Result { + let entries = query_mappings(query_netns, expected_external_ip).await?; + let client_ip = client_ip.to_string(); + let entry = entries + .into_iter() + .find(|entry| entry.internal_client == client_ip && entry.internal_port == local_port) + .ok_or_else(|| Error::from(anyhow!("udp mapping not found for local port {local_port}")))?; + Ok(SocketAddr::new( + IpAddr::V4(expected_external_ip), + entry.external_port, + )) +} + +async fn mapping_exists(local_port: u16) -> bool { + query_udp_mapping(TEST_NS_A, TEST_EXTERNAL_IP, TEST_CLIENT_A_IP, local_port) + .await + .is_ok() +} + +async fn create_test_peer_manager( + inst_name: &str, + netns: Option<&str>, + disable_upnp: bool, + stun_collector: Box, +) -> Arc { + let config = TomlConfigLoader::default(); + config.set_inst_name(inst_name.to_owned()); + config.set_netns(netns.map(ToOwned::to_owned)); + + let global_ctx = Arc::new(GlobalCtx::new(config)); + if disable_upnp { + let mut flags = global_ctx.get_flags(); + flags.disable_upnp = true; + global_ctx.set_flags(flags); + } + global_ctx.replace_stun_info_collector(stun_collector); + + let (packet_tx, _packet_rx) = create_packet_recv_chan(); + let peer_mgr = Arc::new(PeerManager::new(RouteAlgoType::Ospf, global_ctx, packet_tx)); + peer_mgr.run().await.unwrap(); + peer_mgr +} + +fn create_test_instance_config( + inst_name: &str, + netns: Option<&str>, + ipv4: &str, + ipv6: &str, +) -> TomlConfigLoader { + let config = TomlConfigLoader::default(); + config.set_inst_name(inst_name.to_owned()); + config.set_netns(netns.map(ToOwned::to_owned)); + config.set_ipv4(Some(ipv4.parse().unwrap())); + config.set_ipv6(Some(ipv6.parse().unwrap())); + config.set_listeners(vec!["udp://0.0.0.0:11010".parse().unwrap()]); + config +} + +fn create_test_instance( + inst_name: &str, + netns: Option<&str>, + ipv4: &str, + ipv6: &str, + stun_collector: Box, + configure_flags: impl FnOnce(&mut crate::common::config::Flags), +) -> Instance { + let config = create_test_instance_config(inst_name, netns, ipv4, ipv6); + let mut flags = config.get_flags(); + flags.disable_tcp_hole_punching = true; + configure_flags(&mut flags); + config.set_flags(flags); + + let instance = Instance::new(config); + instance + .get_global_ctx() + .replace_stun_info_collector(stun_collector); + instance +} + +async fn wait_for_port_mapping_event( + receiver: &mut tokio::sync::broadcast::Receiver, +) -> GlobalCtxEvent { + tokio::time::timeout(Duration::from_secs(10), async { + loop { + match receiver.recv().await.unwrap() { + event @ GlobalCtxEvent::ListenerPortMappingEstablished { .. } => return event, + _ => continue, + } + } + }) + .await + .expect("timed out waiting for port mapping event") +} + +async fn timeout_stage(stage: &str, duration: Duration, fut: F) -> T +where + F: std::future::Future, +{ + tokio::time::timeout(duration, fut) + .await + .unwrap_or_else(|_| panic!("timed out at stage: {stage}")) +} + +async fn peer_has_udp_conn_to_remote_addr( + peer_mgr: Arc, + peer_id: u32, + expected_remote_addr: SocketAddr, +) -> bool { + let Some(conns) = peer_mgr.get_peer_map().list_peer_conns(peer_id).await else { + return false; + }; + + conns.iter().any(|conn| { + let Some(tunnel) = conn.tunnel.as_ref() else { + return false; + }; + if tunnel.tunnel_type != "udp" { + return false; + } + + let Some(remote_addr) = tunnel.effective_remote_addr() else { + return false; + }; + let remote_addr: url::Url = remote_addr.clone().into(); + let Some(host) = remote_addr.host_str() else { + return false; + }; + let Ok(remote_ip) = host.parse::() else { + return false; + }; + + remote_ip == expected_remote_addr.ip() + && remote_addr.port() == Some(expected_remote_addr.port()) + }) +} + +#[tokio::test] +#[serial_test::serial(upnp)] +async fn udp_hole_punch_listener_establishes_upnp_mapping() { + let _env = UpnpIntegrationEnv::new().await.unwrap(); + let peer_mgr = create_test_peer_manager( + "upnp-test-listener", + Some(TEST_NS_A), + false, + Box::new(GatewayBackedStunCollector { + netns: TEST_NS_A, + client_ip: TEST_CLIENT_A_IP, + external_ip: TEST_EXTERNAL_IP, + }), + ) + .await; + let mut event_rx = peer_mgr.get_global_ctx().subscribe(); + + let listener = UdpHolePunchListener::new(peer_mgr.clone()).await.unwrap(); + let local_port = listener.get_socket().await.local_addr().unwrap().port(); + + let event = wait_for_port_mapping_event(&mut event_rx).await; + let mapped_addr = query_udp_mapping(TEST_NS_A, TEST_EXTERNAL_IP, TEST_CLIENT_A_IP, local_port) + .await + .unwrap(); + + match event { + GlobalCtxEvent::ListenerPortMappingEstablished { + local_listener, + mapped_listener, + backend, + } => { + let expected_external_ip = TEST_EXTERNAL_IP.to_string(); + assert_eq!(backend, "igd"); + assert_eq!(local_listener.scheme(), "udp"); + assert_eq!(local_listener.port(), Some(local_port)); + assert_eq!(mapped_listener.scheme(), "udp"); + assert_eq!( + mapped_listener.host_str(), + Some(expected_external_ip.as_str()) + ); + assert_eq!(mapped_listener.port(), Some(mapped_addr.port())); + } + other => panic!("unexpected event: {other:?}"), + } + + assert!(mapping_exists(local_port).await); + + drop(listener); + + wait_for_condition( + || async { !mapping_exists(local_port).await }, + Duration::from_secs(10), + ) + .await; +} + +#[tokio::test] +#[serial_test::serial(upnp)] +async fn udp_hole_punch_listener_skips_upnp_when_disabled() { + let _env = UpnpIntegrationEnv::new().await.unwrap(); + let peer_mgr = create_test_peer_manager( + "upnp-test-disabled", + Some(TEST_NS_A), + true, + Box::new(MockStunInfoCollector { + udp_nat_type: NatType::PortRestricted, + }), + ) + .await; + let mut event_rx = peer_mgr.get_global_ctx().subscribe(); + + let listener = UdpHolePunchListener::new(peer_mgr.clone()).await.unwrap(); + let local_port = listener.get_socket().await.local_addr().unwrap().port(); + + let event = tokio::time::timeout(Duration::from_secs(2), event_rx.recv()).await; + assert!(event.is_err(), "unexpected port mapping event: {event:?}"); + assert!(!mapping_exists(local_port).await); + + drop(listener); +} + +#[tokio::test] +#[serial_test::serial(upnp)] +async fn udp_hole_punch_succeeds_via_upnp_mappings_with_different_external_ports() { + let _env = DualGatewayUpnpIntegrationEnv::new().await.unwrap(); + + let p_a = create_test_peer_manager( + "upnp-test-a", + Some(DUAL_NS_A), + false, + Box::new(GatewayBackedStunCollector { + netns: DUAL_NS_A, + client_ip: DUAL_CLIENT_A_IP, + external_ip: DUAL_EXTERNAL_A_IP, + }), + ) + .await; + let mut event_rx_a = p_a.get_global_ctx().subscribe(); + let p_b = create_test_peer_manager( + "upnp-test-b", + None, + false, + Box::new(MockStunInfoCollector { + udp_nat_type: NatType::Unknown, + }), + ) + .await; + let p_c = create_test_peer_manager( + "upnp-test-c", + Some(DUAL_NS_C), + false, + Box::new(GatewayBackedStunCollector { + netns: DUAL_NS_C, + client_ip: DUAL_CLIENT_C_IP, + external_ip: DUAL_EXTERNAL_C_IP, + }), + ) + .await; + let mut event_rx_c = p_c.get_global_ctx().subscribe(); + + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + timeout_stage( + "wait_route_appear(a,c)", + Duration::from_secs(10), + wait_route_appear(p_a.clone(), p_c.clone()), + ) + .await + .unwrap(); + timeout_stage( + "wait_route_appear_with_cost(a,c,2)", + Duration::from_secs(10), + wait_route_appear_with_cost(p_a.clone(), p_c.my_peer_id(), Some(2)), + ) + .await + .unwrap(); + let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone()); + let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone()); + hole_punching_a.run_as_client().await.unwrap(); + hole_punching_c.run_as_server().await.unwrap(); + + timeout_stage( + "udp_hole_punch_run_immediately(a)", + Duration::from_secs(10), + hole_punching_a.run_immediately_for_test(), + ) + .await; + + let event_a = timeout_stage( + "wait_port_mapping_event(a)", + Duration::from_secs(15), + wait_for_port_mapping_event(&mut event_rx_a), + ) + .await; + let event_c = timeout_stage( + "wait_port_mapping_event(c)", + Duration::from_secs(15), + wait_for_port_mapping_event(&mut event_rx_c), + ) + .await; + + let (local_port_a, mapped_port_a) = match event_a { + GlobalCtxEvent::ListenerPortMappingEstablished { + local_listener, + mapped_listener, + backend, + } => { + assert_eq!(backend, "igd"); + ( + local_listener.port().unwrap(), + mapped_listener.port().unwrap(), + ) + } + other => panic!("unexpected event for a: {other:?}"), + }; + let (local_port_c, mapped_port_c) = match event_c { + GlobalCtxEvent::ListenerPortMappingEstablished { + local_listener, + mapped_listener, + backend, + } => { + assert_eq!(backend, "igd"); + ( + local_listener.port().unwrap(), + mapped_listener.port().unwrap(), + ) + } + other => panic!("unexpected event for c: {other:?}"), + }; + + assert_ne!(mapped_port_a, local_port_a); + assert_ne!(mapped_port_c, local_port_c); + + let mapped_addr_a = timeout_stage( + "query_udp_mapping(a)", + Duration::from_secs(10), + query_udp_mapping( + DUAL_NS_A, + DUAL_EXTERNAL_A_IP, + DUAL_CLIENT_A_IP, + local_port_a, + ), + ) + .await + .unwrap(); + let mapped_addr_c = timeout_stage( + "query_udp_mapping(c)", + Duration::from_secs(10), + query_udp_mapping( + DUAL_NS_C, + DUAL_EXTERNAL_C_IP, + DUAL_CLIENT_C_IP, + local_port_c, + ), + ) + .await + .unwrap(); + + assert_eq!(mapped_addr_a.port(), mapped_port_a); + assert_eq!(mapped_addr_c.port(), mapped_port_c); + + timeout_stage( + "wait_route_cost_1_after_udp_hole_punch", + Duration::from_secs(15), + wait_for_condition( + || { + let p_a = p_a.clone(); + let p_c = p_c.clone(); + async move { + let a_ok = p_a + .list_routes() + .await + .iter() + .any(|route| route.peer_id == p_c.my_peer_id() && route.cost == 1); + let c_ok = p_c + .list_routes() + .await + .iter() + .any(|route| route.peer_id == p_a.my_peer_id() && route.cost == 1); + a_ok && c_ok + } + }, + Duration::from_secs(15), + ), + ) + .await; + + assert_ne!(mapped_addr_a.port(), local_port_a); + assert_ne!(mapped_addr_c.port(), local_port_c); +} + +#[tokio::test] +#[serial_test::serial(upnp)] +async fn instances_build_direct_connection_via_upnp_udp_hole_punch() { + let _env = DualGatewayUpnpIntegrationEnv::new().await.unwrap(); + + let mut inst_a = create_test_instance( + "upnp-inst-a", + Some(DUAL_NS_A), + "10.144.200.1/24", + "fd20::1/64", + Box::new(GatewayBackedStunCollector { + netns: DUAL_NS_A, + client_ip: DUAL_CLIENT_A_IP, + external_ip: DUAL_EXTERNAL_A_IP, + }), + |flags| flags.need_p2p = true, + ); + let mut event_rx_a = inst_a.get_global_ctx().subscribe(); + + let mut inst_b = create_test_instance( + "upnp-inst-b", + None, + "10.144.200.2/24", + "fd20::2/64", + Box::new(MockStunInfoCollector { + udp_nat_type: NatType::Unknown, + }), + |_| {}, + ); + + let mut inst_c = create_test_instance( + "upnp-inst-c", + Some(DUAL_NS_C), + "10.144.200.3/24", + "fd20::3/64", + Box::new(GatewayBackedStunCollector { + netns: DUAL_NS_C, + client_ip: DUAL_CLIENT_C_IP, + external_ip: DUAL_EXTERNAL_C_IP, + }), + |flags| flags.need_p2p = true, + ); + let mut event_rx_c = inst_c.get_global_ctx().subscribe(); + + inst_a.run().await.unwrap(); + inst_b.run().await.unwrap(); + inst_c.run().await.unwrap(); + + inst_a + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", inst_b.id()).parse().unwrap(), + )); + inst_c + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", inst_b.id()).parse().unwrap(), + )); + + timeout_stage( + "wait_route_appear(inst_a, inst_c)", + Duration::from_secs(10), + wait_route_appear(inst_a.get_peer_manager(), inst_c.get_peer_manager()), + ) + .await + .unwrap(); + timeout_stage( + "wait_route_cost_2(inst_a -> inst_c)", + Duration::from_secs(10), + wait_route_appear_with_cost(inst_a.get_peer_manager(), inst_c.peer_id(), Some(2)), + ) + .await + .unwrap(); + + timeout_stage( + "ping_over_relay_before_p2p", + Duration::from_secs(10), + wait_for_condition( + || async { ping_test(DUAL_NS_A, "10.144.200.3", None).await }, + Duration::from_secs(10), + ), + ) + .await; + + let event_a = timeout_stage( + "wait_instance_port_mapping_event(a)", + Duration::from_secs(15), + wait_for_port_mapping_event(&mut event_rx_a), + ) + .await; + let event_c = timeout_stage( + "wait_instance_port_mapping_event(c)", + Duration::from_secs(15), + wait_for_port_mapping_event(&mut event_rx_c), + ) + .await; + + let (local_port_a, mapped_port_a) = match event_a { + GlobalCtxEvent::ListenerPortMappingEstablished { + local_listener, + mapped_listener, + backend, + } => { + assert_eq!(backend, "igd"); + ( + local_listener.port().unwrap(), + mapped_listener.port().unwrap(), + ) + } + other => panic!("unexpected instance event for a: {other:?}"), + }; + let (local_port_c, mapped_port_c) = match event_c { + GlobalCtxEvent::ListenerPortMappingEstablished { + local_listener, + mapped_listener, + backend, + } => { + assert_eq!(backend, "igd"); + ( + local_listener.port().unwrap(), + mapped_listener.port().unwrap(), + ) + } + other => panic!("unexpected instance event for c: {other:?}"), + }; + + assert_ne!(mapped_port_a, local_port_a); + assert_ne!(mapped_port_c, local_port_c); + + let mapped_addr_a = timeout_stage( + "query_instance_udp_mapping(a)", + Duration::from_secs(10), + query_udp_mapping( + DUAL_NS_A, + DUAL_EXTERNAL_A_IP, + DUAL_CLIENT_A_IP, + local_port_a, + ), + ) + .await + .unwrap(); + let mapped_addr_c = timeout_stage( + "query_instance_udp_mapping(c)", + Duration::from_secs(10), + query_udp_mapping( + DUAL_NS_C, + DUAL_EXTERNAL_C_IP, + DUAL_CLIENT_C_IP, + local_port_c, + ), + ) + .await + .unwrap(); + + assert_eq!(mapped_addr_a.port(), mapped_port_a); + assert_eq!(mapped_addr_c.port(), mapped_port_c); + + timeout_stage( + "wait_instance_direct_peer_via_upnp_and_route_cost_1", + Duration::from_secs(20), + wait_for_condition( + || { + let peer_mgr_a = inst_a.get_peer_manager(); + let peer_mgr_c = inst_c.get_peer_manager(); + let peer_id_a = inst_a.peer_id(); + let peer_id_c = inst_c.peer_id(); + async move { + peer_mgr_a.get_peer_map().has_peer(peer_id_c) + && peer_mgr_c.get_peer_map().has_peer(peer_id_a) + && peer_mgr_a + .list_routes() + .await + .iter() + .any(|route| route.peer_id == peer_id_c && route.cost == 1) + && peer_mgr_c + .list_routes() + .await + .iter() + .any(|route| route.peer_id == peer_id_a && route.cost == 1) + && peer_has_udp_conn_to_remote_addr( + peer_mgr_a.clone(), + peer_id_c, + mapped_addr_c, + ) + .await + && peer_has_udp_conn_to_remote_addr( + peer_mgr_c.clone(), + peer_id_a, + mapped_addr_a, + ) + .await + } + }, + Duration::from_secs(20), + ), + ) + .await; + + timeout_stage( + "ping_over_direct_p2p_after_upnp", + Duration::from_secs(10), + wait_for_condition( + || async { ping_test(DUAL_NS_A, "10.144.200.3", None).await }, + Duration::from_secs(10), + ), + ) + .await; + + drop_insts(vec![inst_a, inst_b, inst_c]).await; +} diff --git a/easytier/src/tunnel/packet_def.rs b/easytier/src/tunnel/packet_def.rs index 0103c4ff..7d78bbf5 100644 --- a/easytier/src/tunnel/packet_def.rs +++ b/easytier/src/tunnel/packet_def.rs @@ -28,10 +28,19 @@ pub enum UdpPacketType { Data = 3, Fin = 4, HolePunch = 5, - V6HolePunch = 6, // when receiving v6 hole punch packet, the packet contains a socket addr of other peer, we + V4HolePunch = 6, // when receiving v4 hole punch packet, the packet contains a socket addr of other peer, we + // will send a hole punch packet to that peer. we only accept this packet from loopback interface. + V6HolePunch = 7, // when receiving v6 hole punch packet, the packet contains a socket addr of other peer, we // will send a hole punch packet to that peer. we only accept this packet from lookback interface. } +#[repr(C, packed)] +#[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] +pub struct V4HolePunchPacket { + pub dst_ipv4: [u8; 4], + pub dst_port: U16, +} + #[repr(C, packed)] #[derive(AsBytes, FromBytes, FromZeroes, Clone, Debug, Default)] pub struct V6HolePunchPacket { diff --git a/easytier/src/tunnel/udp.rs b/easytier/src/tunnel/udp.rs index dde69c7e..7bd2ecf9 100644 --- a/easytier/src/tunnel/udp.rs +++ b/easytier/src/tunnel/udp.rs @@ -1,6 +1,6 @@ use std::{ fmt::Debug, - net::{Ipv6Addr, SocketAddrV6}, + net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, sync::{Arc, Weak}, }; @@ -12,7 +12,6 @@ use futures::{SinkExt, StreamExt, stream::FuturesUnordered}; use rand::{Rng, SeedableRng}; use zerocopy::{AsBytes, FromBytes}; -use std::net::SocketAddr; use tokio::{ net::UdpSocket, sync::mpsc::{Receiver, Sender, UnboundedReceiver, UnboundedSender}, @@ -24,7 +23,7 @@ use super::{ FromUrl, IpVersion, Tunnel, TunnelConnCounter, TunnelError, TunnelInfo, TunnelListener, TunnelUrl, common::wait_for_connect_futures, - packet_def::{UDP_TUNNEL_HEADER_SIZE, UDPTunnelHeader, V6HolePunchPacket}, + packet_def::{UDP_TUNNEL_HEADER_SIZE, UDPTunnelHeader, V4HolePunchPacket, V6HolePunchPacket}, ring::{RingSink, RingStream}, }; use crate::tunnel::common::bind; @@ -114,6 +113,28 @@ pub fn new_v6_hole_punch_packet(dst: &SocketAddrV6) -> ZCPacket { ) } +pub fn new_v4_hole_punch_packet(dst: &SocketAddrV4) -> ZCPacket { + let mut body = V4HolePunchPacket::default(); + body.dst_ipv4.copy_from_slice(&dst.ip().octets()); + body.dst_port.set(dst.port()); + new_udp_packet( + |header| { + header.msg_type = UdpPacketType::V4HolePunch as u8; + header.conn_id.set(dst.port() as u32); + header + .len + .set(std::mem::size_of::() as u16); + }, + Some(body.as_bytes()), + ) +} + +fn extract_dst_addr_from_v4_hole_punch_packet(buf: &[u8]) -> Option { + let body = V4HolePunchPacket::ref_from_prefix(buf)?; + let ip = Ipv4Addr::from(body.dst_ipv4); + Some(SocketAddrV4::new(ip, body.dst_port.get())) +} + fn extrace_dst_addr_from_hole_punch_packet(buf: &[u8]) -> Option { let body = V6HolePunchPacket::ref_from_prefix(buf)?; let ip = Ipv6Addr::from(body.dst_ipv6); @@ -142,6 +163,21 @@ pub async fn send_v6_hole_punch_packet( Ok(()) } +pub async fn send_v4_hole_punch_packet( + listener_port: u16, + dst_addr: SocketAddrV4, +) -> Result<(), TunnelError> { + let local_socket = UdpSocket::bind("127.0.0.1:0").await?; + let udp_packet = new_v4_hole_punch_packet(&dst_addr); + let remote_addr = format!("127.0.0.1:{}", listener_port) + .parse::() + .unwrap(); + local_socket + .send_to(&udp_packet.into_bytes(), remote_addr) + .await?; + Ok(()) +} + async fn respond_stun_packet( socket: Arc, addr: SocketAddr, @@ -455,6 +491,27 @@ impl UdpTunnelListenerData { tracing::error!(?e, "udp respond stun packet error"); } }); + } else if header.msg_type == UdpPacketType::V4HolePunch as u8 { + if !addr.ip().is_loopback() { + tracing::warn!(?addr, "v4 hole punch packet should be from loopback"); + return; + } + if !addr.ip().is_ipv4() { + tracing::warn!(?addr, "v4 hole punch packet should be sent from ipv4"); + return; + } + let Some(dst_addr) = + extract_dst_addr_from_v4_hole_punch_packet(zc_packet.udp_payload()) + else { + tracing::warn!("invalid v4 hole punch packet"); + return; + }; + let socket = self.socket.as_ref().unwrap().clone(); + let udp_packet = new_hole_punch_packet(1, 32); + if let Err(e) = socket.try_send_to(&udp_packet.into_bytes(), SocketAddr::V4(dst_addr)) { + tracing::error!(?e, "udp send hole punch packet error"); + } + tracing::debug!(?dst_addr, "udp forward packet send hole punch packet"); } else if header.msg_type == UdpPacketType::V6HolePunch as u8 { if !addr.ip().is_loopback() { tracing::warn!(?addr, "v6 hole punch packet should be from loopback"); @@ -527,6 +584,12 @@ impl UdpTunnelListener { } } + pub fn new_with_socket(addr: url::Url, socket: Arc) -> Self { + let mut listener = Self::new(addr); + listener.socket = Some(socket); + listener + } + pub fn get_socket(&self) -> Option> { self.socket.clone() } @@ -535,15 +598,17 @@ impl UdpTunnelListener { #[async_trait] impl TunnelListener for UdpTunnelListener { async fn listen(&mut self) -> Result<(), TunnelError> { - let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?; - let tunnel_url: TunnelUrl = self.addr.clone().into(); - self.socket = Some(Arc::new( - bind() - .addr(addr) - .only_v6(true) - .maybe_dev(tunnel_url.bind_dev()) - .call()?, - )); + if self.socket.is_none() { + let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?; + let tunnel_url: TunnelUrl = self.addr.clone().into(); + self.socket = Some(Arc::new( + bind() + .addr(addr) + .only_v6(true) + .maybe_dev(tunnel_url.bind_dev()) + .call()?, + )); + } self.data.socket = self.socket.clone(); self.addr @@ -1147,4 +1212,35 @@ mod tests { .expect("Timeout waiting for v6 hole punch packet") .unwrap(); } + + #[tokio::test] + async fn test_v4_hole_punch_packet() { + let mut lis = UdpTunnelListener::new("udp://0.0.0.0:0".parse().unwrap()); + lis.listen().await.unwrap(); + + let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap()); + let socket_clone = socket.clone(); + let t = tokio::spawn(async move { + let mut buf = BytesMut::new(); + buf.resize(128, 0); + socket_clone.recv_from(&mut buf).await.unwrap(); + }); + + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + send_v4_hole_punch_packet( + lis.local_url().port().unwrap(), + match socket.local_addr().unwrap() { + std::net::SocketAddr::V4(addr_v4) => addr_v4, + _ => panic!("Expected an IPv4 address"), + }, + ) + .await + .unwrap(); + + tokio::time::timeout(tokio::time::Duration::from_secs(2), t) + .await + .expect("Timeout waiting for v4 hole punch packet") + .unwrap(); + } } From ffddc517e1a3baab0a2f007e2d01513ac5c5d3f5 Mon Sep 17 00:00:00 2001 From: Luna Yao <40349250+ZnqbuZ@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:45:22 +0200 Subject: [PATCH 11/26] fix: listener parsing (#2143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a CLI listener parsing regression where url crate special-casing for ws/wss could misinterpret inputs like ws:11011, and adds coverage to prevent future regressions. Changes: Refactors listener parsing to avoid url::Url parsing for proto:port forms and to support additional shorthand inputs (port-only / IP-only / SocketAddr). Centralizes “expand to all IpScheme variants” logic in a helper (gen_listeners) while preserving the “port=0 is dynamic” behavior. Adds unit tests covering valid/invalid listener inputs and expansion behavior. --- easytier/src/core.rs | 199 ++++++++++++++++++++++++++++++++----------- 1 file changed, 150 insertions(+), 49 deletions(-) diff --git a/easytier/src/core.rs b/easytier/src/core.rs index ada771ba..4f89a843 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -1,12 +1,5 @@ #![allow(dead_code)] -use std::{ - net::{IpAddr, SocketAddr}, - path::PathBuf, - process::ExitCode, - sync::{Arc, atomic::AtomicBool}, -}; - use crate::{ ShellType, common::{ @@ -30,6 +23,12 @@ use anyhow::Context; use cidr::IpCidr; use clap::{CommandFactory, Parser}; use rust_i18n::t; +use std::{ + net::{IpAddr, SocketAddr}, + path::PathBuf, + process::ExitCode, + sync::{Arc, atomic::AtomicBool}, +}; use strum::VariantArray; use tokio::io::AsyncReadExt; @@ -744,55 +743,69 @@ struct RpcPortalOptions { } impl Cli { + fn gen_listeners(addr: SocketAddr) -> impl Iterator { + let dynamic = addr.port() == 0; + IpScheme::VARIANTS.iter().map(move |proto| { + let mut addr = addr; + if !dynamic { + addr.set_port(addr.port() + proto.port_offset()); + } + format!("{}://{}", proto, addr) + }) + } + fn parse_listeners(no_listener: bool, listeners: Vec) -> anyhow::Result> { if no_listener || listeners.is_empty() { return Ok(vec![]); } - if listeners.len() == 1 - && let Ok(port) = listeners[0].parse::() - { - let listeners = IpScheme::VARIANTS - .iter() - .map(|proto| { - format!( - "{}://0.0.0.0:{}", - proto, - if port == 0 { - 0 - } else { - port + proto.port_offset() - } - ) - }) - .collect(); - return Ok(listeners); + let mut parsed = vec![]; + + for l in listeners.into_iter() { + if let Ok(port) = l.parse::() { + parsed.extend(Self::gen_listeners(SocketAddr::new( + "0.0.0.0".parse()?, + port, + ))); + continue; + } + + if let Ok(ip) = l.trim_matches(|c| c == '[' || c == ']').parse::() { + parsed.extend(Self::gen_listeners(SocketAddr::new(ip, 11010))); + continue; + } + + if let Ok(addr) = l.parse::() { + parsed.extend(Self::gen_listeners(addr)); + continue; + } + + let (scheme, rest) = l.split_once(':').unwrap_or((&l, "")); + let Ok(scheme) = scheme.parse::() else { + anyhow::bail!("invalid listener: {}", l); + }; + + if rest.is_empty() { + parsed.push(format!( + "{}://0.0.0.0:{}", + scheme, + 11010 + scheme.port_offset() + )); + continue; + } + + if let Ok(port) = rest.parse::() { + parsed.push(format!("{}://0.0.0.0:{}", scheme, port)); + continue; + } + + if !l.parse::()?.has_authority() { + anyhow::bail!("invalid listener: {}", l); + } + parsed.push(l); } - listeners - .into_iter() - .map(|l| { - let l = l - .parse::() - .or_else(|_| url::Url::parse(&format!("{}:", l)))?; - - if l.has_authority() { - return Ok(l.to_string()); - } - - let scheme: IpScheme = l.scheme().parse()?; - let port = { - let port = l.path(); - if port.is_empty() { - 11010 + scheme.port_offset() - } else { - port.parse::() - .with_context(|| format!("invalid port: {}", port))? - } - }; - Ok(format!("{}://0.0.0.0:{}", scheme, port)) - }) - .collect() + Ok(parsed) } } @@ -1594,3 +1607,91 @@ async fn validate_config(cli: &Cli) -> anyhow::Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_listeners() { + type IpSchemeMap = fn(&IpScheme) -> String; + + let cases: [(&str, IpSchemeMap); _] = [ + ("0", |s| format!("{}://0.0.0.0:0", s)), + ("11010", |s| { + format!("{}://0.0.0.0:{}", s, 11010 + s.port_offset()) + }), + ("1.1.1.1", |s| { + format!("{}://1.1.1.1:{}", s, 11010 + s.port_offset()) + }), + ("1.1.1.1:50000", |s| { + format!("{}://1.1.1.1:{}", s, 50000 + s.port_offset()) + }), + ("[::1]", |s| { + format!("{}://[::1]:{}", s, 11010 + s.port_offset()) + }), + ("[::1]:50000", |s| { + format!("{}://[::1]:{}", s, 50000 + s.port_offset()) + }), + ]; + + for (input, output) in cases { + assert_eq!( + Cli::parse_listeners(false, vec![input.to_string()]).unwrap(), + IpScheme::VARIANTS.iter().map(output).collect::>() + ); + } + + let input = cases.iter().map(|(i, _)| i.to_string()).collect::>(); + let output = cases + .iter() + .flat_map(|(_, o)| IpScheme::VARIANTS.iter().map(o)) + .collect::>(); + assert_eq!(Cli::parse_listeners(false, input).unwrap(), output); + + let cases: [(IpSchemeMap, IpSchemeMap); _] = [ + ( + |s| format!("{}", s), + |s| format!("{}://0.0.0.0:{}", s, 11010 + s.port_offset()), + ), + ( + |s| format!("{}:50000", s), + |s| format!("{}://0.0.0.0:50000", s), + ), + ( + |s| format!("{}://1.1.1.1:50000", s), + |s| format!("{}://1.1.1.1:50000", s), + ), + ]; + + for (input, output) in cases { + assert_eq!( + Cli::parse_listeners( + false, + IpScheme::VARIANTS.iter().map(input).collect::>(), + ) + .unwrap(), + IpScheme::VARIANTS.iter().map(output).collect::>() + ); + } + + let input = cases + .iter() + .flat_map(|(i, _)| IpScheme::VARIANTS.iter().map(i)) + .collect::>(); + let output = cases + .iter() + .flat_map(|(_, o)| IpScheme::VARIANTS.iter().map(o)) + .collect::>(); + assert_eq!(Cli::parse_listeners(false, input).unwrap(), output); + + let cases = ["tcp://[::1", "xxx", "tcp:/abc", "tcp:abc"]; + for input in cases { + assert!( + Cli::parse_listeners(false, vec![input.to_string()]).is_err(), + "input: {}", + input + ); + } + } +} From 263f4c3bc90eab0e1fee9677f0c6d0610d920a56 Mon Sep 17 00:00:00 2001 From: "james.zhang" <68689915+zenzenless@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:30:38 +0800 Subject: [PATCH 12/26] fix(peer_route): exclude current peer ID from proxy CIDR lists (#2149) --- easytier/src/peers/peer_ospf_route.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index f260e601..9a71fa90 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -3564,21 +3564,25 @@ impl Route for PeerRoute { } async fn list_proxy_cidrs(&self) -> BTreeSet { + let my_peer_id = self.my_peer_id; self.service_impl .route_table .cidr_peer_id_map .load() .iter() + .filter(|(_, pv)| pv.peer_id != my_peer_id) .map(|(cidr, _)| *cidr) .collect() } async fn list_proxy_cidrs_v6(&self) -> BTreeSet { + let my_peer_id = self.my_peer_id; self.service_impl .route_table .cidr_v6_peer_id_map .load() .iter() + .filter(|(_, pv)| pv.peer_id != my_peer_id) .map(|(cidr, _)| *cidr) .collect() } From 958b246f0565c13ca5f7792eb6ebf45010215d3b Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Thu, 23 Apr 2026 13:44:18 +0800 Subject: [PATCH 13/26] improve webclient (#2151) --- easytier-gui/src-tauri/src/lib.rs | 229 +++++- easytier-gui/src/composables/backend.ts | 41 +- easytier-gui/src/composables/event.ts | 15 +- easytier-web/src/client_manager/session.rs | 757 +++++++++++++++++- .../db/entity/user_running_network_configs.rs | 12 +- easytier-web/src/db/mod.rs | 81 +- ...260421_000003_add_network_config_source.rs | 125 +++ easytier-web/src/migrator/mod.rs | 2 + easytier/src/common/config.rs | 125 +++ easytier/src/core.rs | 12 +- easytier/src/instance_manager.rs | 11 +- easytier/src/launcher.rs | 8 +- easytier/src/peers/foreign_network_manager.rs | 35 + easytier/src/peers/peer_manager.rs | 114 +-- easytier/src/proto/api_manage.proto | 13 +- easytier/src/proto/web.proto | 1 + easytier/src/rpc_service/instance_manage.rs | 116 ++- easytier/src/rpc_service/remote_client.rs | 75 +- easytier/src/web_client/session.rs | 1 + 19 files changed, 1585 insertions(+), 188 deletions(-) create mode 100644 easytier-web/src/migrator/m20260421_000003_add_network_config_source.rs diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index d4dd6a4d..73a74c04 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -15,7 +15,9 @@ use easytier::rpc_service::remote_client::{ use easytier::web_client::{self, WebClient}; use easytier::{ common::{ - config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader}, + config::{ + ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader, + }, log, }, instance_manager::NetworkInstanceManager, @@ -118,7 +120,7 @@ async fn run_network_instance( let client_manager = get_client_manager!()?; let toml_config = cfg.gen_config().map_err(|e| e.to_string())?; client_manager - .pre_run_network_instance_hook(&app, &toml_config) + .pre_run_network_instance_hook(&app, &toml_config, manager::PersistedConfigSource::User) .await?; client_manager .handle_run_network_instance(app.clone(), cfg, save) @@ -207,13 +209,17 @@ async fn update_network_config_state( .map_err(|e: uuid::Error| e.to_string())?; let client_manager = get_client_manager!()?; if !disabled { - let cfg = client_manager - .handle_get_network_config(app.clone(), instance_id) + let (cfg, source) = client_manager + .handle_get_network_config_with_source(app.clone(), instance_id) .await .map_err(|e| e.to_string())?; let toml_config = cfg.gen_config().map_err(|e| e.to_string())?; client_manager - .pre_run_network_instance_hook(&app, &toml_config) + .pre_run_network_instance_hook( + &app, + &toml_config, + manager::PersistedConfigSource::from_runtime_source(source), + ) .await?; } client_manager @@ -272,7 +278,7 @@ async fn get_config(app: AppHandle, instance_id: String) -> Result, + configs: Vec, enabled_networks: Vec, ) -> Result<(), String> { get_client_manager!()? @@ -612,7 +618,11 @@ mod manager { ) -> Result<(), String> { let client_manager = get_client_manager!()?; client_manager - .pre_run_network_instance_hook(&self.app, cfg) + .pre_run_network_instance_hook( + &self.app, + cfg, + PersistedConfigSource::from_runtime_source(cfg.get_network_config_source()), + ) .await } @@ -631,14 +641,87 @@ mod manager { } } + #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] + #[serde(rename_all = "snake_case")] + #[derive(Default)] + pub(super) enum PersistedConfigSource { + User, + Webhook, + #[serde(other)] + #[default] + Legacy, + } + + impl PersistedConfigSource { + pub(super) fn from_runtime_source(source: ConfigSource) -> Self { + match source { + ConfigSource::User => Self::User, + ConfigSource::Webhook => Self::Webhook, + } + } + + fn merge_persisted(self, incoming: Self) -> Self { + match (self, incoming) { + // Older runtimes report missing source as `user`. Keep the stronger persisted + // ownership until webhook sync or an explicit user save repairs it. + (Self::Webhook, Self::User) | (Self::Legacy, Self::User) => self, + (_, next) => next, + } + } + + fn to_runtime_source(self) -> ConfigSource { + match self { + Self::User | Self::Legacy => ConfigSource::User, + Self::Webhook => ConfigSource::Webhook, + } + } + + #[cfg(any(test, target_os = "android"))] + fn is_webhook_like(self) -> bool { + matches!(self, Self::Webhook) + } + } + #[derive(Clone)] - pub(super) struct GUIConfig(String, pub(crate) NetworkConfig); + pub(super) struct GUIConfig { + inst_id: String, + pub(crate) config: NetworkConfig, + source: PersistedConfigSource, + } + + #[derive(Clone, serde::Serialize, serde::Deserialize)] + pub(super) struct StoredGuiConfig { + config: NetworkConfig, + #[serde(default)] + source: PersistedConfigSource, + } + + impl GUIConfig { + fn new(inst_id: String, config: NetworkConfig, source: PersistedConfigSource) -> Self { + Self { + inst_id, + config, + source, + } + } + + fn into_stored(self) -> StoredGuiConfig { + StoredGuiConfig { + config: self.config, + source: self.source, + } + } + } + impl PersistentConfig for GUIConfig { fn get_network_inst_id(&self) -> &str { - &self.0 + &self.inst_id } fn get_network_config(&self) -> Result { - Ok(self.1.clone()) + Ok(self.config.clone()) + } + fn get_network_config_source(&self) -> ConfigSource { + self.source.to_runtime_source() } } @@ -655,13 +738,12 @@ mod manager { } fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> { - let configs: Result, _> = self + let configs = self .network_configs .iter() - .map(|entry| serde_json::to_string(&entry.value().1)) - .collect(); - let payload = format!("[{}]", configs?.join(",")); - app.emit_str("save_configs", payload)?; + .map(|entry| entry.value().clone().into_stored()) + .collect::>(); + app.emit("save_configs", configs)?; Ok(()) } @@ -680,8 +762,14 @@ mod manager { app: &AppHandle, inst_id: Uuid, cfg: NetworkConfig, + source: PersistedConfigSource, ) -> anyhow::Result<()> { - let config = GUIConfig(inst_id.to_string(), cfg); + let source = self + .network_configs + .get(&inst_id) + .map(|existing| existing.source.merge_persisted(source)) + .unwrap_or(source); + let config = GUIConfig::new(inst_id.to_string(), cfg, source); self.network_configs.insert(inst_id, config); self.save_configs(app) } @@ -693,8 +781,14 @@ mod manager { app: AppHandle, network_inst_id: Uuid, network_config: NetworkConfig, + source: ConfigSource, ) -> Result<(), anyhow::Error> { - self.save_config(&app, network_inst_id, network_config)?; + self.save_config( + &app, + network_inst_id, + network_config, + PersistedConfigSource::from_runtime_source(source), + )?; self.enabled_networks.insert(network_inst_id); self.save_enabled_networks(&app)?; Ok(()) @@ -811,17 +905,36 @@ mod manager { .network_configs .iter() .filter(|v| self.storage.enabled_networks.contains(v.key())) - .filter(|v| !v.1.no_tun()) - .filter_map(|c| c.1.instance_id().parse::().ok()) + .filter(|v| !v.config.no_tun()) + .filter_map(|c| c.config.instance_id().parse::().ok()) + } + + #[cfg(target_os = "android")] + pub fn get_enabled_instances_with_webhook_like_tun_ids( + &self, + ) -> impl Iterator + '_ { + self.storage + .network_configs + .iter() + .filter(|v| self.storage.enabled_networks.contains(v.key())) + .filter(|v| !v.config.no_tun()) + .filter(|v| v.source.is_webhook_like()) + .filter_map(|c| c.config.instance_id().parse::().ok()) } #[cfg(target_os = "android")] pub(super) async fn disable_instances_with_tun( &self, app: &AppHandle, + webhook_only: bool, ) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError> { - let inst_ids: Vec = self.get_enabled_instances_with_tun_ids().collect(); + let inst_ids: Vec = if webhook_only { + self.get_enabled_instances_with_webhook_like_tun_ids() + .collect() + } else { + self.get_enabled_instances_with_tun_ids().collect() + }; for inst_id in inst_ids { self.handle_update_network_state(app.clone(), inst_id, true) .await?; @@ -842,6 +955,7 @@ mod manager { &self, app: &AppHandle, cfg: &easytier::common::config::TomlConfigLoader, + source: PersistedConfigSource, ) -> Result<(), String> { let instance_id = cfg.get_id(); app.emit("pre_run_network_instance", instance_id.to_string()) @@ -849,9 +963,24 @@ mod manager { #[cfg(target_os = "android")] if !cfg.get_flags().no_tun { - self.disable_instances_with_tun(app) - .await - .map_err(|e| e.to_string())?; + match source { + PersistedConfigSource::User | PersistedConfigSource::Legacy => { + self.disable_instances_with_tun(app, false) + .await + .map_err(|e| e.to_string())?; + } + PersistedConfigSource::Webhook => { + self.disable_instances_with_tun(app, true) + .await + .map_err(|e| e.to_string())?; + if self.get_enabled_instances_with_tun_ids().next().is_some() { + return Err( + "Android only supports one active TUN network; user-managed VPN remains active" + .to_string(), + ); + } + } + } } self.storage @@ -859,6 +988,7 @@ mod manager { app, instance_id, NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?, + source, ) .map_err(|e| e.to_string())?; @@ -962,15 +1092,15 @@ mod manager { pub(super) async fn load_configs( &self, app: AppHandle, - configs: Vec, + configs: Vec, enabled_networks: Vec, ) -> anyhow::Result<()> { self.storage.network_configs.clear(); - for cfg in configs { - let instance_id = cfg.instance_id(); + for stored in configs { + let instance_id = stored.config.instance_id(); self.storage.network_configs.insert( instance_id.parse()?, - GUIConfig(instance_id.to_string(), cfg), + GUIConfig::new(instance_id.to_string(), stored.config, stored.source), ); } @@ -986,12 +1116,12 @@ mod manager { .storage .network_configs .get(&uuid) - .map(|i| i.value().1.clone()); - let Some(config) = config else { + .map(|i| (i.value().config.clone(), i.value().source)); + let Some((config, source)) = config else { continue; }; let toml_config = config.gen_config()?; - self.pre_run_network_instance_hook(&app, &toml_config) + self.pre_run_network_instance_hook(&app, &toml_config, source) .await .map_err(|e| anyhow::anyhow!(e))?; client @@ -1001,6 +1131,7 @@ mod manager { inst_id: None, config: Some(config), overwrite: false, + source: source.to_runtime_source().to_rpc(), }, ) .await?; @@ -1032,6 +1163,44 @@ mod manager { &self.storage } } + + #[cfg(test)] + mod tests { + use super::{PersistedConfigSource, StoredGuiConfig}; + use easytier::proto::api::manage::NetworkConfig; + + #[test] + fn stored_gui_config_defaults_missing_source_to_legacy() { + let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({ + "config": NetworkConfig::default(), + })) + .unwrap(); + assert_eq!(stored.source, PersistedConfigSource::Legacy); + } + + #[test] + fn persisted_source_merge_keeps_legacy_and_webhook_over_ambiguous_user() { + assert_eq!( + PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::User), + PersistedConfigSource::Legacy + ); + assert_eq!( + PersistedConfigSource::Webhook.merge_persisted(PersistedConfigSource::User), + PersistedConfigSource::Webhook + ); + assert_eq!( + PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Webhook), + PersistedConfigSource::Webhook + ); + } + + #[test] + fn only_webhook_configs_are_webhook_like() { + assert!(!PersistedConfigSource::Legacy.is_webhook_like()); + assert!(!PersistedConfigSource::User.is_webhook_like()); + assert!(PersistedConfigSource::Webhook.is_webhook_like()); + } + } } #[cfg(not(target_os = "android"))] diff --git a/easytier-gui/src/composables/backend.ts b/easytier-gui/src/composables/backend.ts index 31024b9c..9b9a2f62 100644 --- a/easytier-gui/src/composables/backend.ts +++ b/easytier-gui/src/composables/backend.ts @@ -6,6 +6,7 @@ import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist type NetworkConfig = NetworkTypes.NetworkConfig type ValidateConfigResponse = Api.ValidateConfigResponse type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse +type ConfigSource = 'user' | 'webhook' | 'legacy' interface ServiceOptions { config_dir: string rpc_portal: string @@ -16,6 +17,39 @@ interface ServiceOptions { export type ServiceStatus = "Running" | "Stopped" | "NotInstalled" +interface StoredGuiConfig { + config: NetworkConfig + source: ConfigSource +} + +function parseStoredConfigs(raw: string | null): StoredGuiConfig[] { + const parsed: unknown = JSON.parse(raw || '[]') + if (!Array.isArray(parsed)) { + return [] + } + + return parsed.flatMap((entry): StoredGuiConfig[] => { + if (entry && typeof entry === 'object' && 'config' in entry) { + const { config, source } = entry as { + config?: NetworkConfig + source?: ConfigSource + } + if (!config) { + return [] + } + return [{ + config: NetworkTypes.normalizeNetworkConfig(config), + source: source === 'user' || source === 'webhook' ? source : 'legacy', + }] + } + + return [{ + config: NetworkTypes.normalizeNetworkConfig(entry as NetworkConfig), + source: 'legacy', + }] + }) +} + export async function parseNetworkConfig(cfg: NetworkConfig) { return invoke('parse_network_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) }) } @@ -71,9 +105,12 @@ export async function getConfig(instanceId: string) { } export async function sendConfigs(enabledNetworks: string[]) { - const networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]'); + const networkList = parseStoredConfigs(localStorage.getItem('networkList')) return await invoke('load_configs', { - configs: networkList.map((config) => NetworkTypes.toBackendNetworkConfig(NetworkTypes.normalizeNetworkConfig(config))), + configs: networkList.map(({ config, source }) => ({ + config: NetworkTypes.toBackendNetworkConfig(config), + source, + })), enabledNetworks }) } diff --git a/easytier-gui/src/composables/event.ts b/easytier-gui/src/composables/event.ts index 6f24ec36..588be86e 100644 --- a/easytier-gui/src/composables/event.ts +++ b/easytier-gui/src/composables/event.ts @@ -3,6 +3,11 @@ import { type } from "@tauri-apps/plugin-os"; import { NetworkTypes } from "easytier-frontend-lib" import { Utils } from "easytier-frontend-lib"; +interface StoredGuiConfig { + config: NetworkTypes.NetworkConfig + source?: 'user' | 'webhook' | 'legacy' +} + const EVENTS = Object.freeze({ SAVE_CONFIGS: 'save_configs', PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance', @@ -13,9 +18,15 @@ const EVENTS = Object.freeze({ EVENT_LAGGED: 'event_lagged', }); -function onSaveConfigs(event: Event) { +function onSaveConfigs(event: Event) { console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`); - localStorage.setItem('networkList', JSON.stringify(event.payload.map((config) => NetworkTypes.normalizeNetworkConfig(config)))); + localStorage.setItem( + 'networkList', + JSON.stringify(event.payload.map(({ config, source }) => ({ + config: NetworkTypes.normalizeNetworkConfig(config), + source: source ?? 'legacy', + }))), + ); } function normalizeInstanceIdPayload(payload: unknown): string { diff --git a/easytier-web/src/client_manager/session.rs b/easytier-web/src/client_manager/session.rs index 3867ca4d..99b1daea 100644 --- a/easytier-web/src/client_manager/session.rs +++ b/easytier-web/src/client_manager/session.rs @@ -7,17 +7,17 @@ use std::{ use anyhow::Context; use easytier::{ - common::scoped_task::ScopedTask, + common::{config::ConfigSource, scoped_task::ScopedTask}, proto::{ api::manage::{ - NetworkConfig, RunNetworkInstanceRequest, WebClientService, - WebClientServiceClientFactory, + ConfigSource as RpcConfigSource, NetworkConfig, NetworkMeta, RunNetworkInstanceRequest, + WebClientService, WebClientServiceClientFactory, }, rpc_impl::bidirect::BidirectRpcManager, rpc_types::{self, controller::BaseController}, web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer}, }, - rpc_service::remote_client::{ListNetworkProps, Storage as _}, + rpc_service::remote_client::{ListNetworkProps, PersistentConfig as _, Storage as _}, tunnel::Tunnel, }; use tokio::sync::{RwLock, broadcast}; @@ -26,6 +26,50 @@ use super::storage::{Storage, StorageToken, WeakRefStorage}; use crate::FeatureFlags; use crate::webhook::SharedWebhookConfig; +const LEGACY_NETWORK_CONFIG_SOURCE: &str = "legacy"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PersistedConfigSource { + User, + Webhook, + Legacy, +} + +impl PersistedConfigSource { + fn from_db(source: &str) -> Self { + match source { + "webhook" => Self::Webhook, + "user" => Self::User, + LEGACY_NETWORK_CONFIG_SOURCE => Self::Legacy, + _ => Self::User, + } + } + + fn should_update_from_runtime(self, runtime_source: ConfigSource) -> bool { + match (self, runtime_source) { + // Older clients report missing source as `user`, which is not authoritative enough + // to downgrade an existing webhook-owned or legacy row. + (Self::Webhook | Self::Legacy, ConfigSource::User) => false, + _ => self.as_runtime_source() != runtime_source, + } + } + + fn as_runtime_source(self) -> ConfigSource { + match self { + Self::User | Self::Legacy => ConfigSource::User, + Self::Webhook => ConfigSource::Webhook, + } + } + + fn auto_run_rpc_source(self) -> Option { + match self { + Self::User => Some(RpcConfigSource::User), + Self::Webhook => Some(RpcConfigSource::Webhook), + Self::Legacy => None, + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Location { pub country: String, @@ -148,7 +192,7 @@ impl SessionRpcService { Ok(serde_json::from_value::(network_config)?) } - async fn reconcile_managed_network_configs( + async fn reconcile_webhook_source_configs( storage: &Storage, user_id: i32, machine_id: uuid::Uuid, @@ -159,9 +203,19 @@ impl SessionRpcService { .list_network_configs((user_id, machine_id), ListNetworkProps::All) .await .map_err(|e| anyhow::anyhow!("failed to list existing network configs: {:?}", e))?; - let existing_ids = existing_configs + let existing_sources = existing_configs .iter() - .filter_map(|cfg| uuid::Uuid::parse_str(&cfg.network_instance_id).ok()) + .filter_map(|cfg| { + uuid::Uuid::parse_str(&cfg.network_instance_id) + .ok() + .map(|inst_id| (inst_id, PersistedConfigSource::from_db(&cfg.source))) + }) + .collect::>(); + let existing_webhook_ids = existing_sources + .iter() + .filter_map(|(inst_id, source)| { + (*source == PersistedConfigSource::Webhook).then_some(*inst_id) + }) .collect::>(); let mut desired_ids = HashSet::with_capacity(desired_configs.len()); @@ -169,10 +223,30 @@ impl SessionRpcService { for desired in desired_configs { let inst_id = uuid::Uuid::parse_str(&desired.instance_id).with_context(|| { format!( - "invalid desired managed instance id: {}", + "invalid desired webhook config instance id: {}", desired.instance_id ) })?; + match existing_sources.get(&inst_id) { + Some(PersistedConfigSource::User) => { + tracing::warn!( + ?user_id, + ?machine_id, + instance_id = %inst_id, + "skip webhook config because a user-owned config already exists" + ); + continue; + } + Some(PersistedConfigSource::Legacy) => { + tracing::info!( + ?user_id, + ?machine_id, + instance_id = %inst_id, + "adopt legacy config as webhook-owned during reconciliation" + ); + } + _ => {} + } let config = Self::normalize_network_config(desired.network_config, inst_id)?; desired_ids.insert(inst_id); normalized.insert(inst_id, config); @@ -181,18 +255,23 @@ impl SessionRpcService { for (inst_id, config) in normalized { storage .db() - .insert_or_update_user_network_config((user_id, machine_id), inst_id, config) + .insert_or_update_user_network_config( + (user_id, machine_id), + inst_id, + config, + ConfigSource::Webhook, + ) .await .map_err(|e| { anyhow::anyhow!( - "failed to persist managed network config {}: {:?}", + "failed to persist webhook network config {}: {:?}", inst_id, e ) })?; } - let stale_ids = existing_ids + let stale_ids = existing_webhook_ids .difference(&desired_ids) .copied() .collect::>(); @@ -225,7 +304,7 @@ impl SessionRpcService { let ( user_id, - webhook_managed_network_configs, + webhook_source_configs, webhook_config_revision, webhook_validated, binding_version, @@ -306,11 +385,11 @@ impl SessionRpcService { if webhook_validated && data.applied_config_revision.as_deref() != Some(webhook_config_revision.as_str()) { - Self::reconcile_managed_network_configs( + Self::reconcile_webhook_source_configs( &storage, user_id, machine_id, - webhook_managed_network_configs, + webhook_source_configs, ) .await .map_err(rpc_types::error::Error::from)?; @@ -448,13 +527,133 @@ impl Session { ); } + fn collect_webhook_source_instance_ids( + metas: Vec, + ) -> HashSet { + metas + .into_iter() + .filter_map(|meta| { + (RpcConfigSource::try_from(meta.source).ok() == Some(RpcConfigSource::Webhook)) + .then(|| { + meta.inst_id + .map(|inst_id| Into::::into(inst_id).to_string()) + }) + .flatten() + }) + .collect() + } + + async fn sync_running_config_sources( + db: &crate::db::Db, + user_id: i32, + machine_id: uuid::Uuid, + local_configs: &[crate::db::entity::user_running_network_configs::Model], + metas: &[NetworkMeta], + ) -> anyhow::Result<()> { + let local_configs_by_id = local_configs + .iter() + .map(|cfg| (cfg.network_instance_id.clone(), cfg)) + .collect::>(); + + for meta in metas { + let Some(inst_id) = meta.inst_id.as_ref().map(|inst_id| { + let inst_id: uuid::Uuid = (*inst_id).into(); + inst_id + }) else { + continue; + }; + let inst_id_str = inst_id.to_string(); + let Some(local_cfg) = local_configs_by_id.get(&inst_id_str) else { + continue; + }; + + let Some(running_source) = ConfigSource::from_rpc(meta.source) else { + continue; + }; + let local_source = PersistedConfigSource::from_db(&local_cfg.source); + if !local_source.should_update_from_runtime(running_source) { + continue; + } + + db.insert_or_update_user_network_config( + (user_id, machine_id), + inst_id, + local_cfg.get_network_config().map_err(|e| { + anyhow::anyhow!("failed to decode local network config {}: {:?}", inst_id, e) + })?, + running_source, + ) + .await + .map_err(|e| { + anyhow::anyhow!( + "failed to sync running network config source {}: {:?}", + inst_id, + e + ) + })?; + } + + Ok(()) + } + + async fn repair_legacy_running_config_sources( + db: &crate::db::Db, + user_id: i32, + machine_id: uuid::Uuid, + local_configs: &[crate::db::entity::user_running_network_configs::Model], + ) -> anyhow::Result { + let legacy_configs = local_configs + .iter() + .filter(|cfg| { + PersistedConfigSource::from_db(&cfg.source) == PersistedConfigSource::Legacy + }) + .collect::>(); + + if legacy_configs.is_empty() { + return Ok(false); + } + + for local_cfg in legacy_configs { + let inst_id = + uuid::Uuid::parse_str(&local_cfg.network_instance_id).with_context(|| { + format!( + "failed to parse legacy network config instance id {}", + local_cfg.network_instance_id + ) + })?; + + db.insert_or_update_user_network_config( + (user_id, machine_id), + inst_id, + local_cfg.get_network_config().map_err(|e| { + anyhow::anyhow!( + "failed to decode legacy network config {}: {:?}", + inst_id, + e + ) + })?, + ConfigSource::User, + ) + .await + .map_err(|e| { + anyhow::anyhow!( + "failed to repair legacy network config source {}: {:?}", + inst_id, + e + ) + })?; + } + + Ok(true) + } + async fn run_network_on_start( mut heartbeat_waiter: broadcast::Receiver, storage: WeakRefStorage, rpc_client: SessionRpcClient, ) { - let mut cleaned_web_managed_instances = false; - let mut last_desired_inst_ids: Option> = None; + let mut cleaned_webhook_source_instances = false; + let mut last_desired_webhook_inst_ids: Option> = None; loop { heartbeat_waiter = heartbeat_waiter.resubscribe(); let req = heartbeat_waiter.recv().await; @@ -510,37 +709,160 @@ impl Session { } }; + let mut local_configs = local_configs; + let running_metas = if req.support_config_source { + let ret = if running_inst_ids.is_empty() { + Ok(Vec::new()) + } else { + rpc_client + .list_network_instance_meta( + BaseController::default(), + easytier::proto::api::manage::ListNetworkInstanceMetaRequest { + inst_ids: running_inst_ids + .iter() + .filter_map(|inst_id| uuid::Uuid::parse_str(inst_id).ok()) + .map(Into::into) + .collect(), + }, + ) + .await + .map(|resp| resp.metas) + }; + + match ret { + Ok(metas) => { + if let Err(e) = Self::sync_running_config_sources( + &storage.db, + user_id, + machine_id.into(), + &local_configs, + &metas, + ) + .await + { + tracing::warn!( + ?user_id, + ?machine_id, + %e, + "Failed to sync running network config sources" + ); + } else if !metas.is_empty() { + local_configs = match storage + .db + .list_network_configs( + (user_id, machine_id.into()), + ListNetworkProps::EnabledOnly, + ) + .await + { + Ok(configs) => configs, + Err(e) => { + tracing::error!( + "Failed to reload network configs after source sync, error: {:?}", + e + ); + return; + } + }; + } + Some(metas) + } + Err(e) => { + tracing::warn!( + ?user_id, + %e, + "Failed to list running network instance metadata" + ); + None + } + } + } else { + None + }; + + match Self::repair_legacy_running_config_sources( + &storage.db, + user_id, + machine_id.into(), + &local_configs, + ) + .await + { + Ok(true) => { + local_configs = match storage + .db + .list_network_configs( + (user_id, machine_id.into()), + ListNetworkProps::EnabledOnly, + ) + .await + { + Ok(configs) => configs, + Err(e) => { + tracing::error!( + "Failed to reload network configs after legacy source repair, error: {:?}", + e + ); + return; + } + }; + } + Ok(false) => {} + Err(e) => { + tracing::warn!( + ?user_id, + ?machine_id, + %e, + "Failed to repair legacy running network config sources" + ); + } + } + let mut has_failed = false; - let should_be_alive_inst_ids = local_configs + let should_be_alive_webhook_inst_ids = local_configs .iter() + .filter(|cfg| cfg.get_runtime_network_config_source() == ConfigSource::Webhook) .map(|cfg| cfg.network_instance_id.clone()) .collect::>(); - let desired_changed = last_desired_inst_ids + let desired_changed = last_desired_webhook_inst_ids .as_ref() - .is_none_or(|last| last != &should_be_alive_inst_ids); + .is_none_or(|last| last != &should_be_alive_webhook_inst_ids); - if !cleaned_web_managed_instances || desired_changed { - let all_local_configs = match storage + if !cleaned_webhook_source_instances || desired_changed { + let db_webhook_inst_ids = match storage .db .list_network_configs((user_id, machine_id.into()), ListNetworkProps::All) .await { - Ok(configs) => configs, + Ok(configs) => configs + .iter() + .filter(|cfg| { + cfg.get_runtime_network_config_source() == ConfigSource::Webhook + }) + .map(|cfg| cfg.network_instance_id.clone()) + .collect::>(), Err(e) => { tracing::error!("Failed to list all network configs, error: {:?}", e); return; } }; - let all_inst_ids = all_local_configs - .iter() - .map(|cfg| cfg.network_instance_id.clone()) + let running_webhook_inst_ids = if let Some(metas) = running_metas.as_ref() { + Self::collect_webhook_source_instance_ids(metas.clone()) + } else { + running_inst_ids + .intersection(&db_webhook_inst_ids) + .cloned() + .collect() + }; + + let should_delete_inst_ids = running_webhook_inst_ids + .difference(&should_be_alive_webhook_inst_ids) + .cloned() .collect::>(); - let should_delete_ids = running_inst_ids + let should_delete_ids = should_delete_inst_ids .iter() - .chain(all_inst_ids.iter()) - .filter(|inst_id| !should_be_alive_inst_ids.contains(*inst_id)) .filter_map(|inst_id| uuid::Uuid::parse_str(inst_id).ok()) .map(Into::into) .collect::>(); @@ -556,7 +878,7 @@ impl Session { .await; tracing::info!( ?user_id, - "Clean non-web-managed network instances on start: {:?}, user_token: {:?}", + "Clean stale webhook-source network instances on start: {:?}, user_token: {:?}", ret, req.user_token ); @@ -564,8 +886,8 @@ impl Session { } if !has_failed { - cleaned_web_managed_instances = true; - last_desired_inst_ids = Some(should_be_alive_inst_ids.clone()); + cleaned_webhook_source_instances = true; + last_desired_webhook_inst_ids = Some(should_be_alive_webhook_inst_ids.clone()); } } @@ -573,6 +895,16 @@ impl Session { if running_inst_ids.contains(&c.network_instance_id) { continue; } + let Some(source) = PersistedConfigSource::from_db(&c.source).auto_run_rpc_source() + else { + tracing::warn!( + ?user_id, + ?machine_id, + instance_id = %c.network_instance_id, + "skip auto-run for legacy config until source ownership is repaired" + ); + continue; + }; let ret = rpc_client .run_network_instance( BaseController::default(), @@ -582,6 +914,7 @@ impl Session { serde_json::from_str::(&c.network_config).unwrap(), ), overwrite: false, + source: source as i32, }, ) .await; @@ -596,7 +929,7 @@ impl Session { } if !has_failed { - last_desired_inst_ids = Some(should_be_alive_inst_ids); + last_desired_webhook_inst_ids = Some(should_be_alive_webhook_inst_ids); } } } @@ -634,13 +967,17 @@ impl Session { #[cfg(test)] mod tests { - use easytier::rpc_service::remote_client::{ListNetworkProps, Storage as _}; + use easytier::{ + common::config::ConfigSource, + rpc_service::remote_client::{ListNetworkProps, PersistentConfig as _, Storage as _}, + }; + use sea_orm::{ActiveModelTrait, Set}; use serde_json::json; use super::{super::storage::Storage, *}; #[tokio::test] - async fn reconcile_managed_network_configs_upserts_and_deletes_exact_set() { + async fn reconcile_webhook_source_configs_upserts_and_deletes_exact_set() { let storage = Storage::new(crate::db::Db::memory_db().await); let user_id = storage .db() @@ -662,6 +999,7 @@ mod tests { network_name: Some("old-name".to_string()), ..Default::default() }, + ConfigSource::Webhook, ) .await .unwrap(); @@ -674,11 +1012,12 @@ mod tests { network_name: Some("stale".to_string()), ..Default::default() }, + ConfigSource::Webhook, ) .await .unwrap(); - SessionRpcService::reconcile_managed_network_configs( + SessionRpcService::reconcile_webhook_source_configs( &storage, user_id, machine_id, @@ -729,5 +1068,353 @@ mod tests { updated_keep_config.network_name.as_deref(), Some("updated-name") ); + assert_eq!( + updated_keep.get_network_config_source(), + ConfigSource::Webhook + ); + } + + #[tokio::test] + async fn reconcile_webhook_source_configs_keep_user_owned_configs() { + let storage = Storage::new(crate::db::Db::memory_db().await); + let user_id = storage + .db() + .auto_create_user("webhook-user-keep-user") + .await + .unwrap() + .id; + let machine_id = uuid::Uuid::new_v4(); + let user_owned_id = uuid::Uuid::new_v4(); + let webhook_owned_id = uuid::Uuid::new_v4(); + + storage + .db() + .insert_or_update_user_network_config( + (user_id, machine_id), + user_owned_id, + NetworkConfig { + network_name: Some("user-owned".to_string()), + ..Default::default() + }, + ConfigSource::User, + ) + .await + .unwrap(); + storage + .db() + .insert_or_update_user_network_config( + (user_id, machine_id), + webhook_owned_id, + NetworkConfig { + network_name: Some("webhook-owned".to_string()), + ..Default::default() + }, + ConfigSource::Webhook, + ) + .await + .unwrap(); + + SessionRpcService::reconcile_webhook_source_configs( + &storage, + user_id, + machine_id, + vec![crate::webhook::ManagedNetworkConfig { + instance_id: user_owned_id.to_string(), + network_config: json!({ + "instance_id": user_owned_id.to_string(), + "network_name": "webhook-tries-to-take-over" + }), + }], + ) + .await + .unwrap(); + + let user_owned = storage + .db() + .get_network_config((user_id, machine_id), &user_owned_id.to_string()) + .await + .unwrap() + .unwrap(); + assert_eq!(user_owned.get_network_config_source(), ConfigSource::User); + let user_owned_cfg: NetworkConfig = + serde_json::from_str(&user_owned.network_config).unwrap(); + assert_eq!(user_owned_cfg.network_name.as_deref(), Some("user-owned")); + + let webhook_owned = storage + .db() + .get_network_config((user_id, machine_id), &webhook_owned_id.to_string()) + .await + .unwrap(); + assert!(webhook_owned.is_none()); + } + + #[tokio::test] + async fn reconcile_webhook_source_configs_adopts_legacy_rows_for_webhook() { + let storage = Storage::new(crate::db::Db::memory_db().await); + let user_id = storage + .db() + .auto_create_user("webhook-user-legacy") + .await + .unwrap() + .id; + let machine_id = uuid::Uuid::new_v4(); + let legacy_match_id = uuid::Uuid::new_v4(); + let legacy_user_id = uuid::Uuid::new_v4(); + + crate::db::entity::user_running_network_configs::ActiveModel { + user_id: Set(user_id), + device_id: Set(machine_id.to_string()), + network_instance_id: Set(legacy_match_id.to_string()), + network_config: Set(serde_json::to_string(&NetworkConfig { + network_name: Some("legacy-webhook".to_string()), + ..Default::default() + }) + .unwrap()), + source: Set(LEGACY_NETWORK_CONFIG_SOURCE.to_string()), + disabled: Set(false), + create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + ..Default::default() + } + .insert(storage.db().orm_db()) + .await + .unwrap(); + + crate::db::entity::user_running_network_configs::ActiveModel { + user_id: Set(user_id), + device_id: Set(machine_id.to_string()), + network_instance_id: Set(legacy_user_id.to_string()), + network_config: Set(serde_json::to_string(&NetworkConfig { + network_name: Some("legacy-user".to_string()), + ..Default::default() + }) + .unwrap()), + source: Set(LEGACY_NETWORK_CONFIG_SOURCE.to_string()), + disabled: Set(false), + create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + ..Default::default() + } + .insert(storage.db().orm_db()) + .await + .unwrap(); + + SessionRpcService::reconcile_webhook_source_configs( + &storage, + user_id, + machine_id, + vec![crate::webhook::ManagedNetworkConfig { + instance_id: legacy_match_id.to_string(), + network_config: json!({ + "instance_id": legacy_match_id.to_string(), + "network_name": "managed-by-webhook" + }), + }], + ) + .await + .unwrap(); + + let adopted = storage + .db() + .get_network_config((user_id, machine_id), &legacy_match_id.to_string()) + .await + .unwrap() + .unwrap(); + assert_eq!(adopted.source, ConfigSource::Webhook.as_str()); + let adopted_cfg: NetworkConfig = serde_json::from_str(&adopted.network_config).unwrap(); + assert_eq!( + adopted_cfg.network_name.as_deref(), + Some("managed-by-webhook") + ); + + let untouched_legacy = storage + .db() + .get_network_config((user_id, machine_id), &legacy_user_id.to_string()) + .await + .unwrap() + .unwrap(); + assert_eq!(untouched_legacy.source, LEGACY_NETWORK_CONFIG_SOURCE); + } + + #[tokio::test] + async fn sync_running_config_sources_updates_enabled_config_source_from_runtime() { + let storage = Storage::new(crate::db::Db::memory_db().await); + let user_id = storage + .db() + .auto_create_user("webhook-user-sync-source") + .await + .unwrap() + .id; + let machine_id = uuid::Uuid::new_v4(); + let inst_id = uuid::Uuid::new_v4(); + + storage + .db() + .insert_or_update_user_network_config( + (user_id, machine_id), + inst_id, + NetworkConfig { + network_name: Some("webhook-owned".to_string()), + ..Default::default() + }, + ConfigSource::Webhook, + ) + .await + .unwrap(); + + let local_configs = storage + .db() + .list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly) + .await + .unwrap(); + Session::sync_running_config_sources( + storage.db(), + user_id, + machine_id, + &local_configs, + &[easytier::proto::api::manage::NetworkMeta { + inst_id: Some(inst_id.into()), + source: RpcConfigSource::User as i32, + ..Default::default() + }], + ) + .await + .unwrap(); + + let updated = storage + .db() + .get_network_config((user_id, machine_id), &inst_id.to_string()) + .await + .unwrap() + .unwrap(); + assert_eq!(updated.get_network_config_source(), ConfigSource::Webhook); + } + + #[tokio::test] + async fn sync_running_config_sources_keeps_legacy_rows_when_runtime_source_is_user() { + let storage = Storage::new(crate::db::Db::memory_db().await); + let user_id = storage + .db() + .auto_create_user("webhook-user-sync-legacy") + .await + .unwrap() + .id; + let machine_id = uuid::Uuid::new_v4(); + let inst_id = uuid::Uuid::new_v4(); + + crate::db::entity::user_running_network_configs::ActiveModel { + user_id: Set(user_id), + device_id: Set(machine_id.to_string()), + network_instance_id: Set(inst_id.to_string()), + network_config: Set(serde_json::to_string(&NetworkConfig { + network_name: Some("legacy".to_string()), + ..Default::default() + }) + .unwrap()), + source: Set(LEGACY_NETWORK_CONFIG_SOURCE.to_string()), + disabled: Set(false), + create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + ..Default::default() + } + .insert(storage.db().orm_db()) + .await + .unwrap(); + + let local_configs = storage + .db() + .list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly) + .await + .unwrap(); + Session::sync_running_config_sources( + storage.db(), + user_id, + machine_id, + &local_configs, + &[easytier::proto::api::manage::NetworkMeta { + inst_id: Some(inst_id.into()), + source: RpcConfigSource::User as i32, + ..Default::default() + }], + ) + .await + .unwrap(); + + let updated = storage + .db() + .get_network_config((user_id, machine_id), &inst_id.to_string()) + .await + .unwrap() + .unwrap(); + assert_eq!(updated.source, LEGACY_NETWORK_CONFIG_SOURCE); + } + + #[tokio::test] + async fn repair_legacy_running_config_sources_promotes_remaining_legacy_rows_to_user() { + let storage = Storage::new(crate::db::Db::memory_db().await); + let user_id = storage + .db() + .auto_create_user("webhook-user-repair-legacy") + .await + .unwrap() + .id; + let machine_id = uuid::Uuid::new_v4(); + let inst_id = uuid::Uuid::new_v4(); + + crate::db::entity::user_running_network_configs::ActiveModel { + user_id: Set(user_id), + device_id: Set(machine_id.to_string()), + network_instance_id: Set(inst_id.to_string()), + network_config: Set(serde_json::to_string(&NetworkConfig { + network_name: Some("legacy".to_string()), + ..Default::default() + }) + .unwrap()), + source: Set(LEGACY_NETWORK_CONFIG_SOURCE.to_string()), + disabled: Set(false), + create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + ..Default::default() + } + .insert(storage.db().orm_db()) + .await + .unwrap(); + + let local_configs = storage + .db() + .list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly) + .await + .unwrap(); + assert!( + Session::repair_legacy_running_config_sources( + storage.db(), + user_id, + machine_id, + &local_configs, + ) + .await + .unwrap() + ); + + let updated = storage + .db() + .get_network_config((user_id, machine_id), &inst_id.to_string()) + .await + .unwrap() + .unwrap(); + assert_eq!(updated.source, ConfigSource::User.as_str()); + } + + #[test] + fn legacy_configs_are_not_auto_run_until_repaired() { + assert_eq!(PersistedConfigSource::Legacy.auto_run_rpc_source(), None); + assert_eq!( + PersistedConfigSource::Webhook.auto_run_rpc_source(), + Some(RpcConfigSource::Webhook) + ); + assert_eq!( + PersistedConfigSource::User.auto_run_rpc_source(), + Some(RpcConfigSource::User) + ); } } diff --git a/easytier-web/src/db/entity/user_running_network_configs.rs b/easytier-web/src/db/entity/user_running_network_configs.rs index 0525ba36..88a974a7 100644 --- a/easytier-web/src/db/entity/user_running_network_configs.rs +++ b/easytier-web/src/db/entity/user_running_network_configs.rs @@ -1,6 +1,9 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 -use easytier::{launcher::NetworkConfig, rpc_service::remote_client::PersistentConfig}; +use easytier::{ + common::config::ConfigSource, launcher::NetworkConfig, + rpc_service::remote_client::PersistentConfig, +}; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; @@ -12,10 +15,12 @@ pub struct Model { pub user_id: i32, #[sea_orm(column_type = "Text")] pub device_id: String, - #[sea_orm(column_type = "Text", unique)] + #[sea_orm(column_type = "Text")] pub network_instance_id: String, #[sea_orm(column_type = "Text")] pub network_config: String, + #[sea_orm(column_type = "Text")] + pub source: String, pub disabled: bool, pub create_time: DateTimeWithTimeZone, pub update_time: DateTimeWithTimeZone, @@ -48,4 +53,7 @@ impl PersistentConfig for Model { fn get_network_config(&self) -> Result { serde_json::from_str(&self.network_config).map_err(|e| DbErr::Json(e.to_string())) } + fn get_network_config_source(&self) -> ConfigSource { + self.source.parse().unwrap_or(ConfigSource::User) + } } diff --git a/easytier-web/src/db/mod.rs b/easytier-web/src/db/mod.rs index 8627b505..66b0e567 100644 --- a/easytier-web/src/db/mod.rs +++ b/easytier-web/src/db/mod.rs @@ -3,6 +3,7 @@ pub mod entity; use easytier::{ + common::config::ConfigSource, launcher::NetworkConfig, rpc_service::remote_client::{ListNetworkProps, Storage}, }; @@ -149,6 +150,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for (user_id, device_id): (UserIdInDb, Uuid), network_inst_id: Uuid, network_config: NetworkConfig, + source: ConfigSource, ) -> Result<(), DbErr> { let txn = self.orm_db().begin().await?; @@ -161,6 +163,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for ]) .update_columns([ urnc::Column::NetworkConfig, + urnc::Column::Source, urnc::Column::Disabled, urnc::Column::UpdateTime, ]) @@ -172,6 +175,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for network_config: sea_orm::Set( serde_json::to_string(&network_config).map_err(|e| DbErr::Json(e.to_string()))?, ), + source: sea_orm::Set(source.as_str().to_string()), disabled: sea_orm::Set(false), create_time: sea_orm::Set(chrono::Local::now().fixed_offset()), update_time: sea_orm::Set(chrono::Local::now().fixed_offset()), @@ -277,8 +281,12 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for #[cfg(test)] mod tests { - use easytier::{proto::api::manage::NetworkConfig, rpc_service::remote_client::Storage}; - use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _}; + use easytier::{ + common::config::ConfigSource, + proto::api::manage::NetworkConfig, + rpc_service::remote_client::{PersistentConfig, Storage}, + }; + use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter as _, Set}; use crate::db::{Db, ListNetworkProps, entity::user_running_network_configs}; @@ -294,9 +302,14 @@ mod tests { let inst_id = uuid::Uuid::new_v4(); let device_id = uuid::Uuid::new_v4(); - db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config) - .await - .unwrap(); + db.insert_or_update_user_network_config( + (user_id, device_id), + inst_id, + network_config, + ConfigSource::User, + ) + .await + .unwrap(); let result = user_running_network_configs::Entity::find() .filter(user_running_network_configs::Column::UserId.eq(user_id)) @@ -306,6 +319,7 @@ mod tests { .unwrap(); println!("{:?}", result); assert_eq!(result.network_config, network_config_json); + assert_eq!(result.get_network_config_source(), ConfigSource::User); // overwrite the config let network_config = NetworkConfig { @@ -313,9 +327,14 @@ mod tests { ..Default::default() }; let network_config_json = serde_json::to_string(&network_config).unwrap(); - db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config) - .await - .unwrap(); + db.insert_or_update_user_network_config( + (user_id, device_id), + inst_id, + network_config, + ConfigSource::Webhook, + ) + .await + .unwrap(); let result2 = user_running_network_configs::Entity::find() .filter(user_running_network_configs::Column::UserId.eq(user_id)) @@ -325,6 +344,11 @@ mod tests { .unwrap(); println!("device: {}, {:?}", device_id, result2); assert_eq!(result2.network_config, network_config_json); + assert_eq!(result2.get_network_config_source(), ConfigSource::Webhook); + assert_eq!( + result2.get_runtime_network_config_source(), + ConfigSource::Webhook + ); assert_eq!(result.create_time, result2.create_time); assert_ne!(result.update_time, result2.update_time); @@ -348,6 +372,45 @@ mod tests { assert!(result3.is_none()); } + #[tokio::test] + async fn test_legacy_network_config_defaults_to_user_runtime_source() { + let db = Db::memory_db().await; + let user_id = 1; + let inst_id = uuid::Uuid::new_v4(); + let device_id = uuid::Uuid::new_v4(); + + user_running_network_configs::ActiveModel { + user_id: Set(user_id), + device_id: Set(device_id.to_string()), + network_instance_id: Set(inst_id.to_string()), + network_config: Set(serde_json::to_string(&NetworkConfig { + network_name: Some("legacy".to_string()), + ..Default::default() + }) + .unwrap()), + source: Set("legacy".to_string()), + disabled: Set(false), + create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()), + ..Default::default() + } + .insert(db.orm_db()) + .await + .unwrap(); + + let result = user_running_network_configs::Entity::find() + .filter(user_running_network_configs::Column::UserId.eq(user_id)) + .one(db.orm_db()) + .await + .unwrap() + .unwrap(); + assert_eq!(result.get_network_config_source(), ConfigSource::User); + assert_eq!( + result.get_runtime_network_config_source(), + ConfigSource::User + ); + } + #[tokio::test] async fn test_user_network_config_same_instance_id_is_scoped_by_device() { let db = Db::memory_db().await; @@ -363,6 +426,7 @@ mod tests { network_name: Some("cfg-1".to_string()), ..Default::default() }, + ConfigSource::User, ) .await .unwrap(); @@ -373,6 +437,7 @@ mod tests { network_name: Some("cfg-2".to_string()), ..Default::default() }, + ConfigSource::User, ) .await .unwrap(); diff --git a/easytier-web/src/migrator/m20260421_000003_add_network_config_source.rs b/easytier-web/src/migrator/m20260421_000003_add_network_config_source.rs new file mode 100644 index 00000000..e2ee1719 --- /dev/null +++ b/easytier-web/src/migrator/m20260421_000003_add_network_config_source.rs @@ -0,0 +1,125 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20260421_000003_add_network_config_source" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared( + r#" + CREATE TABLE user_running_network_configs_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + user_id INTEGER NOT NULL, + device_id TEXT NOT NULL, + network_instance_id TEXT NOT NULL, + network_config TEXT NOT NULL, + source TEXT NOT NULL DEFAULT 'user', + disabled BOOLEAN NOT NULL DEFAULT FALSE, + create_time TEXT NOT NULL, + update_time TEXT NOT NULL, + CONSTRAINT fk_user_running_network_configs_user_id_to_users_id + FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE + ); + + INSERT INTO user_running_network_configs_new ( + id, + user_id, + device_id, + network_instance_id, + network_config, + source, + disabled, + create_time, + update_time + ) + SELECT + id, + user_id, + device_id, + network_instance_id, + network_config, + 'legacy', + disabled, + create_time, + update_time + FROM user_running_network_configs; + + DROP TABLE user_running_network_configs; + ALTER TABLE user_running_network_configs_new RENAME TO user_running_network_configs; + + CREATE INDEX idx_user_running_network_configs_user_id + ON user_running_network_configs(user_id); + CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst + ON user_running_network_configs(user_id, device_id, network_instance_id); + "#, + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared( + r#" + CREATE TABLE user_running_network_configs_old ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + user_id INTEGER NOT NULL, + device_id TEXT NOT NULL, + network_instance_id TEXT NOT NULL, + network_config TEXT NOT NULL, + disabled BOOLEAN NOT NULL DEFAULT FALSE, + create_time TEXT NOT NULL, + update_time TEXT NOT NULL, + CONSTRAINT fk_user_running_network_configs_user_id_to_users_id + FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE + ON UPDATE CASCADE + ); + + INSERT INTO user_running_network_configs_old ( + id, + user_id, + device_id, + network_instance_id, + network_config, + disabled, + create_time, + update_time + ) + SELECT + id, + user_id, + device_id, + network_instance_id, + network_config, + disabled, + create_time, + update_time + FROM user_running_network_configs; + + DROP TABLE user_running_network_configs; + ALTER TABLE user_running_network_configs_old RENAME TO user_running_network_configs; + + CREATE INDEX idx_user_running_network_configs_user_id + ON user_running_network_configs(user_id); + CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst + ON user_running_network_configs(user_id, device_id, network_instance_id); + "#, + ) + .await?; + + Ok(()) + } +} diff --git a/easytier-web/src/migrator/mod.rs b/easytier-web/src/migrator/mod.rs index e5490267..03be5eea 100644 --- a/easytier-web/src/migrator/mod.rs +++ b/easytier-web/src/migrator/mod.rs @@ -2,6 +2,7 @@ use sea_orm_migration::prelude::*; mod m20241029_000001_init; mod m20260403_000002_scope_network_config_unique; +mod m20260421_000003_add_network_config_source; pub struct Migrator; @@ -11,6 +12,7 @@ impl MigratorTrait for Migrator { vec![ Box::new(m20241029_000001_init::Migration), Box::new(m20260403_000002_scope_network_config_unique::Migration), + Box::new(m20260421_000003_add_network_config_source::Migration), ] } } diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 3969dd0a..be959587 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -18,6 +18,7 @@ use crate::{ instance::dns_server::DEFAULT_ET_DNS_ZONE, proto::{ acl::Acl, + api::manage::ConfigSource as RpcConfigSource, common::{CompressionAlgoPb, PortForwardConfigPb, SecureModeConfig, SocketType}, }, tunnel::generate_digest_from_str, @@ -206,6 +207,11 @@ pub trait ConfigLoader: Send + Sync { } fn set_credential_file(&self, _path: Option) {} + fn get_network_config_source(&self) -> ConfigSource { + ConfigSource::User + } + fn set_network_config_source(&self, _source: Option) {} + fn dump(&self) -> String; } @@ -225,6 +231,55 @@ pub struct NetworkIdentity { pub network_secret_digest: Option, } +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum ConfigSource { + #[default] + User, + Webhook, +} + +impl ConfigSource { + pub fn as_str(self) -> &'static str { + match self { + Self::User => "user", + Self::Webhook => "webhook", + } + } + + pub fn from_rpc(source: i32) -> Option { + match RpcConfigSource::try_from(source).ok() { + Some(RpcConfigSource::Webhook) => Some(Self::Webhook), + Some(RpcConfigSource::User) => Some(Self::User), + _ => None, + } + } + + pub fn to_rpc(self) -> i32 { + match self { + Self::User => RpcConfigSource::User as i32, + Self::Webhook => RpcConfigSource::Webhook as i32, + } + } +} + +impl std::str::FromStr for ConfigSource { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "user" => Ok(Self::User), + "webhook" => Ok(Self::Webhook), + other => Err(format!("unknown network config source: {other}")), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +struct ConfigSourceConfig { + source: ConfigSource, +} + #[derive(Eq, PartialEq, Hash)] struct NetworkIdentityWithOnlyDigest { network_name: String, @@ -466,6 +521,7 @@ struct Config { stun_servers_v6: Option>, credential_file: Option, + source: Option, } #[derive(Debug, Clone)] @@ -480,10 +536,21 @@ impl Default for TomlConfigLoader { } impl TomlConfigLoader { + fn normalize_config_source(config: &mut Config) { + if matches!( + config.source.as_ref().map(|source| source.source), + Some(ConfigSource::User) + ) { + config.source = None; + } + } + pub fn new_from_str(config_str: &str) -> Result { let mut config = toml::de::from_str::(config_str) .with_context(|| format!("failed to parse config file: {}", config_str))?; + Self::normalize_config_source(&mut config); + config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default())); let config = TomlConfigLoader { @@ -867,6 +934,23 @@ impl ConfigLoader for TomlConfigLoader { self.config.lock().unwrap().credential_file = path; } + fn get_network_config_source(&self) -> ConfigSource { + self.config + .lock() + .unwrap() + .source + .as_ref() + .map(|source| source.source) + .unwrap_or(ConfigSource::User) + } + + fn set_network_config_source(&self, source: Option) { + self.config.lock().unwrap().source = source.and_then(|source| match source { + ConfigSource::User => None, + other => Some(ConfigSourceConfig { source: other }), + }); + } + fn dump(&self) -> String { let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap(); let default_flags_hashmap = @@ -888,6 +972,7 @@ impl ConfigLoader for TomlConfigLoader { } let mut config = self.config.lock().unwrap().clone(); + Self::normalize_config_source(&mut config); config.flags = Some(flag_map); if config.stun_servers == Some(StunInfoCollector::get_default_servers()) { config.stun_servers = None; @@ -1126,6 +1211,46 @@ stun_servers = [ assert_eq!(stun_servers[2], "txt:stun.easytier.cn"); } + #[test] + fn test_network_config_source_toml_roundtrip() { + let config = TomlConfigLoader::default(); + assert_eq!(config.get_network_config_source(), ConfigSource::User); + + config.set_network_config_source(Some(ConfigSource::Webhook)); + let dumped = config.dump(); + + assert!(dumped.contains("[source]")); + assert!(dumped.contains("source = \"webhook\"")); + + let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap(); + assert_eq!(loaded.get_network_config_source(), ConfigSource::Webhook); + } + + #[test] + fn test_network_config_source_user_is_implicit() { + let config = TomlConfigLoader::default(); + config.set_network_config_source(Some(ConfigSource::User)); + let dumped = config.dump(); + + assert!(!dumped.contains("[source]")); + + let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap(); + assert_eq!(loaded.get_network_config_source(), ConfigSource::User); + + let explicit_user = TomlConfigLoader::new_from_str( + r#" +[source] +source = "user" +"#, + ) + .unwrap(); + assert_eq!( + explicit_user.get_network_config_source(), + ConfigSource::User + ); + assert!(!explicit_user.dump().contains("[source]")); + } + #[tokio::test] async fn full_example_test() { let config_str = r#" diff --git a/easytier/src/core.rs b/easytier/src/core.rs index 4f89a843..96e3df75 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -813,7 +813,7 @@ impl NetworkOptions { fn can_merge( &self, cfg: &TomlConfigLoader, - source: ConfigSource, + source: ConfigFileSource, explicit_config_file_count: usize, config_dir_file_count: usize, ) -> bool { @@ -821,7 +821,7 @@ impl NetworkOptions { return false; } - if source == ConfigSource::CliConfigFile + if source == ConfigFileSource::CliConfigFile && explicit_config_file_count == 1 && config_dir_file_count == 0 { @@ -832,7 +832,7 @@ impl NetworkOptions { return false; }; - if source == ConfigSource::ConfigDir { + if source == ConfigFileSource::ConfigDir { return cfg.get_network_identity().network_name == *network_name; } @@ -1161,7 +1161,7 @@ impl NetworkOptions { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ConfigSource { +enum ConfigFileSource { CliConfigFile, ConfigDir, } @@ -1353,7 +1353,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { let mut config_files = if let Some(v) = cli.config_file { v.iter() .cloned() - .map(|path| (path, ConfigSource::CliConfigFile)) + .map(|path| (path, ConfigFileSource::CliConfigFile)) .collect() } else { vec![] @@ -1376,7 +1376,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { continue; } config_dir_file_count += 1; - config_files.push((path, ConfigSource::ConfigDir)); + config_files.push((path, ConfigFileSource::ConfigDir)); } } let config_file_count = config_files.len(); diff --git a/easytier/src/instance_manager.rs b/easytier/src/instance_manager.rs index 460c3772..26b257e3 100644 --- a/easytier/src/instance_manager.rs +++ b/easytier/src/instance_manager.rs @@ -4,7 +4,7 @@ use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; use crate::{ common::{ - config::{ConfigFileControl, ConfigLoader, TomlConfigLoader}, + config::{ConfigFileControl, ConfigLoader, ConfigSource, TomlConfigLoader}, global_ctx::{EventBusSubscriber, GlobalCtxEvent}, log, scoped_task::ScopedTask, @@ -217,6 +217,15 @@ impl NetworkInstanceManager { .map(|instance| instance.value().get_config_file_control().clone()) } + pub fn get_instance_network_config_source( + &self, + instance_id: &uuid::Uuid, + ) -> Option { + self.instance_map + .get(instance_id) + .map(|instance| instance.value().get_network_config_source()) + } + pub fn get_instance_service( &self, instance_id: &uuid::Uuid, diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 0d64bb84..6fbc67c6 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -1,4 +1,6 @@ -use crate::common::config::{ConfigFileControl, PortForwardConfig, process_secure_mode_cfg}; +use crate::common::config::{ + ConfigFileControl, ConfigSource, PortForwardConfig, process_secure_mode_cfg, +}; use crate::proto::api::{self, manage}; use crate::proto::rpc_types::controller::BaseController; use crate::rpc_service::InstanceRpcService; @@ -434,6 +436,10 @@ impl NetworkInstance { &self.config_file_control } + pub fn get_network_config_source(&self) -> ConfigSource { + self.config.get_network_config_source() + } + pub fn get_latest_error_msg(&self) -> Option { if let Some(launcher) = self.launcher.as_ref() { launcher.error_msg.read().unwrap().clone() diff --git a/easytier/src/peers/foreign_network_manager.rs b/easytier/src/peers/foreign_network_manager.rs index c9693e16..1fb6c22c 100644 --- a/easytier/src/peers/foreign_network_manager.rs +++ b/easytier/src/peers/foreign_network_manager.rs @@ -1575,6 +1575,41 @@ pub mod tests { ); } + #[tokio::test] + async fn secure_center_can_serve_legacy_and_secure_foreign_networks() { + let pm_center = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + set_secure_mode_cfg(&pm_center.get_global_ctx(), true); + + let legacy_a = create_mock_peer_manager_for_foreign_network("legacy-net").await; + let legacy_b = create_mock_peer_manager_for_foreign_network("legacy-net").await; + connect_peer_manager(legacy_a.clone(), pm_center.clone()).await; + connect_peer_manager(legacy_b.clone(), pm_center.clone()).await; + wait_route_appear(legacy_a.clone(), legacy_b.clone()) + .await + .unwrap(); + + let secure_a = create_mock_peer_manager_for_secure_foreign_network("secure-net").await; + let secure_b = create_mock_peer_manager_for_secure_foreign_network("secure-net").await; + connect_peer_manager(secure_a.clone(), pm_center.clone()).await; + connect_peer_manager(secure_b.clone(), pm_center.clone()).await; + wait_route_appear(secure_a.clone(), secure_b.clone()) + .await + .unwrap(); + + assert_eq!(2, legacy_a.list_routes().await.len()); + assert_eq!(2, legacy_b.list_routes().await.len()); + assert_eq!(2, secure_a.list_routes().await.len()); + assert_eq!(2, secure_b.list_routes().await.len()); + + let rpc_resp = pm_center + .get_foreign_network_manager() + .list_foreign_networks() + .await; + assert_eq!(2, rpc_resp.foreign_networks.len()); + assert_eq!(2, rpc_resp.foreign_networks["legacy-net"].peers.len()); + assert_eq!(2, rpc_resp.foreign_networks["secure-net"].peers.len()); + } + #[tokio::test] async fn credential_pubkey_trust_requires_ospf_credential_source() { let global_ctx = get_mock_global_ctx_with_network(Some(NetworkIdentity::new( diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index c2dcbc0f..3fa73f59 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -536,6 +536,21 @@ impl PeerManager { async fn add_new_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> { let my_identity = self.global_ctx.get_network_identity(); let peer_identity = peer_conn.get_network_identity(); + let conn_info = peer_conn.get_conn_info(); + let local_secure_mode = self + .global_ctx + .config + .get_secure_mode() + .as_ref() + .map(|cfg| cfg.enabled) + .unwrap_or(false); + let peer_secure_mode = !conn_info.noise_remote_static_pubkey.is_empty(); + + if local_secure_mode != peer_secure_mode { + return Err(Error::SecretKeyError( + "same-network peers must use the same secure mode".to_string(), + )); + } // For credential nodes, network_secret_digest is either None or all-zeros // (all-zeros when received over the wire via handshake). @@ -2717,7 +2732,7 @@ mod tests { } #[tokio::test] - async fn peer_manager_safe_server_accept_legacy_client() { + async fn peer_manager_same_network_secure_mode_mismatch_rejected() { let peer_mgr_client = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; let peer_mgr_server = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; @@ -2737,64 +2752,65 @@ mod tests { peer_mgr_client.add_client_tunnel(c_ring, false), peer_mgr_server.add_tunnel_as_server(s_ring, true) ); - let (server_id, _) = c_ret.unwrap(); - s_ret.unwrap(); + let _ = c_ret; + assert!( + s_ret.is_err(), + "same-network peer with mismatched secure mode should be rejected" + ); - wait_for_condition( - || { - let peer_mgr_client = peer_mgr_client.clone(); - async move { - if !peer_mgr_client - .get_peer_map() - .list_peers_with_conn() - .await - .contains(&server_id) - { - return false; - } - let Some(conns) = peer_mgr_client - .get_peer_map() - .list_peer_conns(server_id) - .await - else { - return false; - }; - conns.iter().any(|c| { - c.noise_local_static_pubkey.is_empty() - && c.noise_remote_static_pubkey.is_empty() - && c.secure_auth_level == SecureAuthLevel::None as i32 - }) - } - }, - Duration::from_secs(10), - ) - .await; - - let client_id = peer_mgr_client.my_peer_id(); wait_for_condition( || { let peer_mgr_server = peer_mgr_server.clone(); async move { - if !peer_mgr_server + peer_mgr_server .get_peer_map() .list_peers_with_conn() .await - .contains(&client_id) - { - return false; - } - let Some(conns) = peer_mgr_server + .is_empty() + } + }, + Duration::from_secs(5), + ) + .await; + } + + #[tokio::test] + async fn credential_node_rejects_legacy_client() { + let peer_mgr_client = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let peer_mgr_server = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + peer_mgr_client + .get_global_ctx() + .config + .set_network_identity(NetworkIdentity::new("net1".to_string(), "sec1".to_string())); + peer_mgr_server + .get_global_ctx() + .config + .set_network_identity(NetworkIdentity::new_credential("net1".to_string())); + + set_secure_mode_cfg(&peer_mgr_server.get_global_ctx(), true); + + let (c_ring, s_ring) = create_ring_tunnel_pair(); + let (c_ret, s_ret) = tokio::join!( + peer_mgr_client.add_client_tunnel(c_ring, false), + peer_mgr_server.add_tunnel_as_server(s_ring, true) + ); + + let _ = c_ret; + assert!( + s_ret.is_err(), + "credential server should reject legacy client" + ); + + wait_for_condition( + || { + let peer_mgr_server = peer_mgr_server.clone(); + async move { + peer_mgr_server .get_peer_map() - .list_peer_conns(client_id) + .list_peers_with_conn() .await - else { - return false; - }; - conns.iter().any(|c| { - c.noise_local_static_pubkey.is_empty() - && c.noise_remote_static_pubkey.is_empty() - && c.secure_auth_level == SecureAuthLevel::None as i32 - }) + .is_empty() } }, Duration::from_secs(5), diff --git a/easytier/src/proto/api_manage.proto b/easytier/src/proto/api_manage.proto index ab8a648c..1b5d9882 100644 --- a/easytier/src/proto/api_manage.proto +++ b/easytier/src/proto/api_manage.proto @@ -13,6 +13,12 @@ enum NetworkingMethod { Standalone = 2; } +enum ConfigSource { + ConfigSourceUnspecified = 0; + ConfigSourceUser = 1; + ConfigSourceWebhook = 2; +} + message NetworkConfig { optional string instance_id = 1; @@ -132,6 +138,7 @@ message NetworkMeta { string network_name = 2; uint32 config_permission = 3; string instance_name = 4; + ConfigSource source = 5; } message ValidateConfigRequest { NetworkConfig config = 1; } @@ -142,6 +149,7 @@ message RunNetworkInstanceRequest { common.UUID inst_id = 1; NetworkConfig config = 2; bool overwrite = 3; + ConfigSource source = 4; } message RunNetworkInstanceResponse { common.UUID inst_id = 1; } @@ -168,7 +176,10 @@ message DeleteNetworkInstanceResponse { message GetNetworkInstanceConfigRequest { common.UUID inst_id = 1; } -message GetNetworkInstanceConfigResponse { NetworkConfig config = 1; } +message GetNetworkInstanceConfigResponse { + NetworkConfig config = 1; + ConfigSource source = 2; +} message ListNetworkInstanceMetaRequest { repeated common.UUID inst_ids = 1; } diff --git a/easytier/src/proto/web.proto b/easytier/src/proto/web.proto index c28595e9..4468e10f 100644 --- a/easytier/src/proto/web.proto +++ b/easytier/src/proto/web.proto @@ -21,6 +21,7 @@ message HeartbeatRequest { repeated common.UUID running_network_instances = 7; DeviceOsInfo device_os = 8; + bool support_config_source = 9; } message HeartbeatResponse {} diff --git a/easytier/src/rpc_service/instance_manage.rs b/easytier/src/rpc_service/instance_manage.rs index 9e69b63a..af134cc9 100644 --- a/easytier/src/rpc_service/instance_manage.rs +++ b/easytier/src/rpc_service/instance_manage.rs @@ -1,10 +1,23 @@ use std::{collections::HashSet, sync::Arc}; use crate::{ - common::config::{ConfigFileControl, ConfigFilePermission, ConfigLoader}, + common::config::{ConfigFileControl, ConfigFilePermission, ConfigLoader, ConfigSource}, instance_manager::NetworkInstanceManager, proto::{ - api::{config::GetConfigRequest, manage::*}, + api::{ + config::GetConfigRequest, + manage::{ + CollectNetworkInfoRequest, CollectNetworkInfoResponse, + DeleteNetworkInstanceRequest, DeleteNetworkInstanceResponse, + GetNetworkInstanceConfigRequest, GetNetworkInstanceConfigResponse, + ListNetworkInstanceMetaRequest, ListNetworkInstanceMetaResponse, + ListNetworkInstanceRequest, ListNetworkInstanceResponse, + NetworkInstanceRunningInfoMap, NetworkMeta, RetainNetworkInstanceRequest, + RetainNetworkInstanceResponse, RunNetworkInstanceRequest, + RunNetworkInstanceResponse, ValidateConfigRequest, ValidateConfigResponse, + WebClientService, + }, + }, rpc_types::{self, controller::BaseController}, }, web_client::WebClientHooks, @@ -44,53 +57,64 @@ impl WebClientService for InstanceManageRpcService { return Err(anyhow::anyhow!("config is required").into()); } let cfg = req.config.unwrap().gen_config()?; - let id = cfg.get_id(); + let mut effective_id = cfg.get_id(); if let Some(inst_id) = req.inst_id { - cfg.set_id(inst_id.into()); + effective_id = inst_id.into(); + cfg.set_id(effective_id); } + let requested_source = ConfigSource::from_rpc(req.source); let resp = RunNetworkInstanceResponse { - inst_id: Some(id.into()), + inst_id: Some(effective_id.into()), }; - let mut control = if let Some(control) = self.manager.get_instance_config_control(&id) { - let error_msg = self - .manager - .get_network_info(&id) - .await - .and_then(|i| i.error_msg) - .unwrap_or_default(); + let mut control = + if let Some(control) = self.manager.get_instance_config_control(&effective_id) { + let existing_source = self + .manager + .get_instance_network_config_source(&effective_id); + let error_msg = self + .manager + .get_network_info(&effective_id) + .await + .and_then(|i| i.error_msg) + .unwrap_or_default(); - if !req.overwrite && error_msg.is_empty() { - return Ok(resp); - } - if control.is_read_only() { - return Err( - anyhow::anyhow!("instance {} is read-only, cannot be overwritten", id).into(), - ); - } - - if let Some(path) = control.path.as_ref() { - let real_control = ConfigFileControl::from_path(path.clone()).await; - if real_control.is_read_only() { + if !req.overwrite && error_msg.is_empty() { + return Ok(resp); + } + if control.is_read_only() { return Err(anyhow::anyhow!( - "config file {} is read-only, cannot be overwritten", - path.display() + "instance {} is read-only, cannot be overwritten", + effective_id ) .into()); } - } - self.manager.delete_network_instance(vec![id])?; + if let Some(path) = control.path.as_ref() { + let real_control = ConfigFileControl::from_path(path.clone()).await; + if real_control.is_read_only() { + return Err(anyhow::anyhow!( + "config file {} is read-only, cannot be overwritten", + path.display() + ) + .into()); + } + } - control.clone() - } else if let Some(config_dir) = self.manager.get_config_dir() { - ConfigFileControl::new( - Some(config_dir.join(format!("{}.toml", id))), - ConfigFilePermission::default(), - ) - } else { - ConfigFileControl::new(None, ConfigFilePermission::default()) - }; + self.manager.delete_network_instance(vec![effective_id])?; + + cfg.set_network_config_source(requested_source.or(existing_source)); + control.clone() + } else if let Some(config_dir) = self.manager.get_config_dir() { + cfg.set_network_config_source(requested_source); + ConfigFileControl::new( + Some(config_dir.join(format!("{}.toml", effective_id))), + ConfigFilePermission::default(), + ) + } else { + cfg.set_network_config_source(requested_source); + ConfigFileControl::new(None, ConfigFilePermission::default()) + }; if !control.is_read_only() && let Some(config_file) = control.path.as_ref() @@ -109,9 +133,9 @@ impl WebClientService for InstanceManageRpcService { } self.manager.run_network_instance(cfg, true, control)?; - println!("instance {} started", id); + println!("instance {} started", effective_id); - if let Err(e) = self.hooks.post_run_network_instance(&id).await { + if let Err(e) = self.hooks.post_run_network_instance(&effective_id).await { tracing::warn!("post-run hook failed: {}", e); } @@ -261,7 +285,14 @@ impl WebClientService for InstanceManageRpcService { .get_config(BaseController::default(), GetConfigRequest::default()) .await? .config; - Ok(GetNetworkInstanceConfigResponse { config }) + Ok(GetNetworkInstanceConfigResponse { + config, + source: self + .manager + .get_instance_network_config_source(&inst_id) + .unwrap_or(ConfigSource::User) + .to_rpc(), + }) } async fn list_network_instance_meta( @@ -286,6 +317,11 @@ impl WebClientService for InstanceManageRpcService { network_name, config_permission: control.permission.into(), instance_name, + source: self + .manager + .get_instance_network_config_source(&inst_id) + .unwrap_or(ConfigSource::User) + .to_rpc(), }; metas.push(meta); } diff --git a/easytier/src/rpc_service/remote_client.rs b/easytier/src/rpc_service/remote_client.rs index b088dde8..5cabe5f3 100644 --- a/easytier/src/rpc_service/remote_client.rs +++ b/easytier/src/rpc_service/remote_client.rs @@ -1,7 +1,18 @@ use async_trait::async_trait; use uuid::Uuid; -use crate::proto::{api::manage::*, rpc_types::controller::BaseController}; +use crate::{ + common::config::ConfigSource, + proto::{ + api::manage::{ + CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest, + GetNetworkInstanceConfigRequest, ListNetworkInstanceMetaRequest, + ListNetworkInstanceRequest, NetworkConfig, NetworkMeta, RunNetworkInstanceRequest, + ValidateConfigRequest, ValidateConfigResponse, WebClientService, + }, + rpc_types::controller::BaseController, + }, +}; #[async_trait] pub trait RemoteClientManager @@ -52,6 +63,7 @@ where inst_id: None, config: Some(config.clone()), overwrite: true, + source: ConfigSource::User.to_rpc(), }, ) .await?; @@ -62,6 +74,7 @@ where identify, resp.inst_id.unwrap_or_default().into(), config, + ConfigSource::User, ) .await .map_err(RemoteClientError::PersistentError)?; @@ -162,13 +175,18 @@ where .get_rpc_client(identify.clone()) .ok_or(RemoteClientError::ClientNotFound)?; - let cfg = self - .handle_get_network_config(identify.clone(), inst_id) + let (cfg, source) = self + .handle_get_network_config_with_source(identify.clone(), inst_id) .await?; if disabled { self.get_storage() - .insert_or_update_user_network_config(identify.clone(), inst_id, cfg.clone()) + .insert_or_update_user_network_config( + identify.clone(), + inst_id, + cfg.clone(), + source, + ) .await .map_err(RemoteClientError::PersistentError)?; @@ -188,6 +206,7 @@ where inst_id: Some(inst_id.into()), config: Some(cfg), overwrite: true, + source: source.to_rpc(), }, ) .await?; @@ -230,8 +249,8 @@ where if metas.contains_key(&instance_id) { continue; } - let config = self - .handle_get_network_config(identify.clone(), instance_id) + let (config, source) = self + .handle_get_network_config_with_source(identify.clone(), instance_id) .await?; let network_name = config.network_name.unwrap_or_default(); metas.insert( @@ -241,6 +260,7 @@ where network_name: network_name.clone(), config_permission: 0, instance_name: network_name, + source: source.to_rpc(), }, ); } @@ -255,7 +275,12 @@ where config: NetworkConfig, ) -> Result<(), RemoteClientError> { self.get_storage() - .insert_or_update_user_network_config(identify.clone(), inst_id, config) + .insert_or_update_user_network_config( + identify.clone(), + inst_id, + config, + ConfigSource::User, + ) .await .map_err(RemoteClientError::PersistentError)?; self.get_storage() @@ -270,6 +295,16 @@ where identify: T, inst_id: uuid::Uuid, ) -> Result> { + self.handle_get_network_config_with_source(identify, inst_id) + .await + .map(|(config, _)| config) + } + + async fn handle_get_network_config_with_source( + &self, + identify: T, + inst_id: uuid::Uuid, + ) -> Result<(NetworkConfig, ConfigSource), RemoteClientError> { if let Some(client) = self.get_rpc_client(identify.clone()) && let Ok(resp) = client .get_network_instance_config( @@ -281,7 +316,17 @@ where .await && let Some(config) = resp.config { - return Ok(config); + let source = if let Some(source) = ConfigSource::from_rpc(resp.source) { + source + } else { + self.get_storage() + .get_network_config(identify.clone(), &inst_id.to_string()) + .await + .map_err(RemoteClientError::PersistentError)? + .map(|cfg| cfg.get_runtime_network_config_source()) + .unwrap_or(ConfigSource::User) + }; + return Ok((config, source)); } let inst_id = inst_id.to_string(); @@ -296,9 +341,12 @@ where inst_id )))?; - Ok(db_row - .get_network_config() - .map_err(RemoteClientError::PersistentError)?) + Ok(( + db_row + .get_network_config() + .map_err(RemoteClientError::PersistentError)?, + db_row.get_runtime_network_config_source(), + )) } } @@ -336,6 +384,10 @@ pub struct GetNetworkMetasResponse { pub trait PersistentConfig { fn get_network_inst_id(&self) -> &str; fn get_network_config(&self) -> Result; + fn get_network_config_source(&self) -> ConfigSource; + fn get_runtime_network_config_source(&self) -> ConfigSource { + self.get_network_config_source() + } } #[async_trait] @@ -348,6 +400,7 @@ where identify: T, network_inst_id: Uuid, network_config: NetworkConfig, + source: ConfigSource, ) -> Result<(), E>; async fn delete_network_configs(&self, identify: T, network_inst_ids: &[Uuid]) diff --git a/easytier/src/web_client/session.rs b/easytier/src/web_client/session.rs index fd28961b..a81b3f80 100644 --- a/easytier/src/web_client/session.rs +++ b/easytier/src/web_client/session.rs @@ -93,6 +93,7 @@ impl Session { hostname: hostname.clone(), report_time: chrono::Local::now().to_rfc3339(), device_os: Some(device_os.clone()), + support_config_source: true, running_network_instances: controller .list_network_instance_ids() From ac112440c30f89d79f378897f0cb4829e90c98ef Mon Sep 17 00:00:00 2001 From: "james.zhang" <68689915+zenzenless@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:45:09 +0800 Subject: [PATCH 14/26] fix(UrlInput): update parseUrl and buildUrlValue to handle null ports correctly (#2146) --- .../frontend-lib/src/components/UrlInput.vue | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/easytier-web/frontend-lib/src/components/UrlInput.vue b/easytier-web/frontend-lib/src/components/UrlInput.vue index f80f9877..494a1e67 100644 --- a/easytier-web/frontend-lib/src/components/UrlInput.vue +++ b/easytier-web/frontend-lib/src/components/UrlInput.vue @@ -32,7 +32,7 @@ onMounted(() => { } }) -const parseUrl = (val: string | null | undefined) => { +const parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => { const getValidPort = (portStr: string, proto: string) => { const p = parseInt(portStr) return isNaN(p) ? (props.protos[proto] ?? 11010) : p @@ -55,13 +55,16 @@ const parseUrl = (val: string | null | undefined) => { if (ipv6End > 0) { const host = hostAndMaybePort.slice(0, ipv6End + 1) const remain = hostAndMaybePort.slice(ipv6End + 1) - const port = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : (props.protos[proto] ?? 11010) + // null = no explicit port in URL; do not fabricate a default + const port: number | null = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : null return { proto, host, port } } } const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/) const host = portMatch ? portMatch[1] : hostAndMaybePort - const port = portMatch ? parseInt(portMatch[2]) : (props.protos[proto] ?? 11010) + // null = no explicit port in URL; buildUrlValue will omit the port entirely, + // preserving the protocol's implied standard port (e.g. 443 for wss://). + const port: number | null = portMatch ? parseInt(portMatch[2]) : null return { proto, host, port } } @@ -72,28 +75,26 @@ const parseUrl = (val: string | null | undefined) => { if (parsedByPattern) { return parsedByPattern } - return { proto: 'tcp', host: '', port: 11010 } + return { proto: 'tcp', host: '', port: null } } const internalValue = ref(parseUrl(url.value)) const defaultHost = '0.0.0.0' -const buildUrlValue = (value: { proto: string, host: string, port: number }, forceDefaultHost = false) => { +const buildUrlValue = (value: { proto: string, host: string, port: number | null }, forceDefaultHost = false) => { const proto = value.proto || 'tcp' const rawHost = (value.host ?? '').trim() const host = rawHost || (forceDefaultHost ? defaultHost : '') if (!host) { return null } - let port = value.port - if (isNaN(parseInt(port as any))) { - port = props.protos[proto] ?? 11010 - } - - if (props.protos[proto] === 0) { + // Omit port when the protocol uses no port (protos value = 0), or when the + // original URL had no explicit port (port === null) – avoids overwriting an + // implicit standard port (e.g. 443 for wss) with an EasyTier default (11012). + if (props.protos[proto] === 0 || value.port === null) { return `${proto}://${host}` } - return `${proto}://${host}:${port}` + return `${proto}://${host}:${value.port}` } const syncUrlFromInternal = (forceDefaultHost = false) => { @@ -180,6 +181,7 @@ const onProtoChange = (newProto: string) => { : @@ -207,7 +209,8 @@ const onProtoChange = (newProto: string) => {
- +