From 772b87e7336ca4ea503659290a360fddc8b7229c Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Tue, 30 Dec 2025 15:12:09 +0800 Subject: [PATCH] feat: add GUI support for AnyTLS/Mieru/Sudoku and AnyTLS URI parsing --- Changelog.md | 2 + pnpm-lock.yaml | 8 +- .../profile/groups-editor-viewer.tsx | 3 + src/types/global.d.ts | 75 ++++++++++++++++- src/utils/uri-parser.ts | 84 +++++++++++++++++++ 5 files changed, 167 insertions(+), 5 deletions(-) diff --git a/Changelog.md b/Changelog.md index 5d2f9f239..8d5d7b50c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -18,6 +18,7 @@ - 支持收起导航栏(导航栏右键菜单 / 界面设置) - 允许将出站模式显示在托盘一级菜单 - 允许禁用在托盘中显示代理组 +- 支持在「编辑节点」中直接导入 AnyTLS URI 配置 @@ -31,5 +32,6 @@ - 改进托盘和窗口操作频率限制实现 - 使用「编辑节点」添加节点时,自动将节点添加到第一个 `select` 类型的代理组的第一位 - 隐藏侧边导航栏和悬浮跳转导航的滚动条 +- 完善对 AnyTLS / Mieru / Sudoku 的 GUI 支持 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48b1db172..78d450d86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,7 +130,7 @@ importers: version: 2.3.8(react@19.2.3) tauri-plugin-mihomo-api: specifier: github:clash-verge-rev/tauri-plugin-mihomo#main - version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/153f16f7b3f979aa130a2d3d7c39a52a39987288 + version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/a0e5095c1516229e8816096ae7017f950e6200dd types-pac: specifier: ^1.0.3 version: 1.0.3 @@ -3876,8 +3876,8 @@ packages: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/153f16f7b3f979aa130a2d3d7c39a52a39987288: - resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/153f16f7b3f979aa130a2d3d7c39a52a39987288} + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/a0e5095c1516229e8816096ae7017f950e6200dd: + resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/a0e5095c1516229e8816096ae7017f950e6200dd} version: 0.1.0 terser@5.44.1: @@ -8398,7 +8398,7 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/153f16f7b3f979aa130a2d3d7c39a52a39987288: + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/a0e5095c1516229e8816096ae7017f950e6200dd: dependencies: '@tauri-apps/api': 2.9.1 diff --git a/src/components/profile/groups-editor-viewer.tsx b/src/components/profile/groups-editor-viewer.tsx index ac4f619a6..ca498ffc4 100644 --- a/src/components/profile/groups-editor-viewer.tsx +++ b/src/components/profile/groups-editor-viewer.tsx @@ -828,6 +828,9 @@ export const GroupsEditorViewer = (props: Props) => { "Hysteria2", "WireGuard", "Tuic", + "Mieru", + "AnyTLS", + "Sudoku", "Relay", "Selector", "Fallback", diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 03eaf73f6..0387e4f3f 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -429,6 +429,16 @@ type CipherType = | "aez-384" | "deoxys-ii-256-128" | "rc4-md5"; +type MieruTransport = "TCP" | "UDP"; +type MieruMultiplexing = + | "MULTIPLEXING_OFF" + | "MULTIPLEXING_LOW" + | "MULTIPLEXING_MIDDLE" + | "MULTIPLEXING_HIGH"; +type SudokuAeadMethod = "chacha20-poly1305" | "aes-128-gcm" | "none"; +type SudokuTableType = "prefer_ascii" | "prefer_entropy"; +type SudokuHttpMaskMode = "legacy" | "stream" | "poll" | "auto"; +type SudokuHttpMaskStrategy = "random" | "post" | "websocket"; // base interface IProxyBaseConfig { tfo?: boolean; @@ -513,6 +523,29 @@ interface IProxyTrojanConfig extends IProxyBaseConfig { }; "client-fingerprint"?: ClientFingerprint; } +// anytls +interface IProxyAnyTLSConfig extends IProxyBaseConfig { + name: string; + type: "anytls"; + server?: string; + port?: number; + password?: string; + alpn?: string[]; + sni?: string; + "client-fingerprint"?: ClientFingerprint; + "skip-cert-verify"?: boolean; + fingerprint?: string; + certificate?: string; + "private-key"?: string; + "ech-opts"?: { + enable?: boolean; + config?: string; + }; + udp?: boolean; + "idle-session-check-interval"?: number; + "idle-session-timeout"?: number; + "min-idle-session"?: number; +} // tuic interface IProxyTuicConfig extends IProxyBaseConfig { name: string; @@ -546,6 +579,20 @@ interface IProxyTuicConfig extends IProxyBaseConfig { "udp-over-stream"?: boolean; "udp-over-stream-version"?: number; } +// mieru +interface IProxyMieruConfig extends IProxyBaseConfig { + name: string; + type: "mieru"; + server?: string; + port?: number; + "port-range"?: string; + transport?: MieruTransport; + udp?: boolean; + username?: string; + password?: string; + multiplexing?: MieruMultiplexing; + "handshake-mode"?: string; +} // vless interface IProxyVlessConfig extends IProxyBaseConfig { name: string; @@ -714,6 +761,26 @@ interface IProxyShadowsocksConfig extends IProxyBaseConfig { "client-fingerprint"?: ClientFingerprint; smux?: boolean; } +// sudoku +interface IProxySudokuConfig extends IProxyBaseConfig { + name: string; + type: "sudoku"; + server?: string; + port?: number; + key?: string; + "aead-method"?: SudokuAeadMethod; + "padding-min"?: number; + "padding-max"?: number; + "table-type"?: SudokuTableType; + "enable-pure-downlink"?: boolean; + "http-mask"?: boolean; + "http-mask-mode"?: SudokuHttpMaskMode; + "http-mask-tls"?: boolean; + "http-mask-host"?: string; + "http-mask-strategy"?: SudokuHttpMaskStrategy; + "custom-table"?: string; + "custom-tables"?: string[]; +} // shadowsocksR interface IProxyshadowsocksRConfig extends IProxyBaseConfig { name: string; @@ -765,13 +832,16 @@ interface IProxyConfig IProxySocks5Config, IProxySshConfig, IProxyTrojanConfig, + IProxyAnyTLSConfig, IProxyTuicConfig, + IProxyMieruConfig, IProxyVlessConfig, IProxyVmessConfig, IProxyWireguardConfig, IProxyHysteriaConfig, IProxyHysteria2Config, IProxyShadowsocksConfig, + IProxySudokuConfig, IProxyshadowsocksRConfig, IProxySmuxConfig, IProxySnellConfig { @@ -783,6 +853,7 @@ interface IProxyConfig | "snell" | "http" | "trojan" + | "anytls" | "hysteria" | "hysteria2" | "tuic" @@ -790,7 +861,9 @@ interface IProxyConfig | "ssh" | "socks5" | "vmess" - | "vless"; + | "vless" + | "mieru" + | "sudoku"; } interface IVergeConfig { diff --git a/src/utils/uri-parser.ts b/src/utils/uri-parser.ts index 6c0abfd26..d04551558 100644 --- a/src/utils/uri-parser.ts +++ b/src/utils/uri-parser.ts @@ -8,6 +8,7 @@ const URI_PARSERS: Record = { vmess: URI_VMESS, vless: URI_VLESS, trojan: URI_Trojan, + anytls: URI_AnyTLS, hysteria2: URI_Hysteria2, hy2: URI_Hysteria2, hysteria: URI_Hysteria, @@ -1071,6 +1072,89 @@ function URI_Trojan(line: string): IProxyTrojanConfig { return proxy; } +function URI_AnyTLS(line: string): IProxyAnyTLSConfig { + const afterScheme = stripUriScheme(line, "anytls", "Invalid anytls uri"); + if (!afterScheme) { + throw new Error("Invalid anytls uri"); + } + const { + auth: authRaw, + host: server, + port, + query: addons, + fragment: nameRaw, + } = parseUrlLike(afterScheme, { + errorMessage: "Invalid anytls uri", + }); + if (!server) { + throw new Error("Invalid anytls uri"); + } + const portNum = parsePortOrDefault(port, 443); + const auth = safeDecodeURIComponent(authRaw) ?? authRaw; + const decodedName = decodeAndTrim(nameRaw); + const name = decodedName ?? `AnyTLS ${server}:${portNum}`; + const proxy: IProxyAnyTLSConfig = { + type: "anytls", + name, + server, + port: portNum, + udp: true, + }; + + if (auth) { + const [username, password] = splitOnce(auth, ":"); + proxy.password = password ?? username; + } + + const params = parseQueryStringNormalized(addons); + if (params.sni) { + proxy.sni = params.sni; + } + if (params.alpn) { + const alpn = params.alpn + .split(",") + .map((item) => item.trim()) + .filter(Boolean); + if (alpn.length > 0) { + proxy.alpn = alpn; + } + } + + const fingerprint = params.fingerprint ?? params.hpkp; + if (fingerprint) { + proxy.fingerprint = fingerprint; + } + const clientFingerprint = params["client-fingerprint"] ?? params.fp; + if (clientFingerprint) { + proxy["client-fingerprint"] = clientFingerprint as ClientFingerprint; + } + + if (Object.prototype.hasOwnProperty.call(params, "skip-cert-verify")) { + proxy["skip-cert-verify"] = parseBoolOrPresence(params["skip-cert-verify"]); + } else if (Object.prototype.hasOwnProperty.call(params, "insecure")) { + proxy["skip-cert-verify"] = parseBoolOrPresence(params.insecure); + } + + if (Object.prototype.hasOwnProperty.call(params, "udp")) { + proxy.udp = parseBoolOrPresence(params.udp); + } + + const idleCheck = parseInteger(params["idle-session-check-interval"]); + if (idleCheck !== undefined) { + proxy["idle-session-check-interval"] = idleCheck; + } + const idleTimeout = parseInteger(params["idle-session-timeout"]); + if (idleTimeout !== undefined) { + proxy["idle-session-timeout"] = idleTimeout; + } + const minIdle = parseInteger(params["min-idle-session"]); + if (minIdle !== undefined) { + proxy["min-idle-session"] = minIdle; + } + + return proxy; +} + function URI_Hysteria2(line: string): IProxyHysteria2Config { const afterScheme = stripUriScheme( line,