diff --git a/fe-app-tachyon/.vitest/json/output.json b/fe-app-tachyon/.vitest/json/output.json new file mode 100644 index 00000000..475d2e6b --- /dev/null +++ b/fe-app-tachyon/.vitest/json/output.json @@ -0,0 +1 @@ +{"numTotalTestSuites":497,"numPassedTestSuites":497,"numFailedTestSuites":0,"numPendingTestSuites":0,"numTotalTests":591,"numPassedTests":591,"numFailedTests":0,"numPendingTests":0,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1789128885605,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["runtimeTags"],"fullName":"runtimeTags uses the same direct section collision format as the backend generator","status":"passed","title":"uses the same direct section collision format as the backend generator","duration":2.1036,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887392,"endTime":1789128887394.1035,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tests/runtimeTags.test.ts"},{"assertionResults":[{"ancestorTitles":["cascadeDeleteSection logic"],"fullName":"cascadeDeleteSection logic cascades deletion of all child sections for a given section","status":"passed","title":"cascades deletion of all child sections for a given section","duration":4.315899999999999,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["cascadeDeleteSection logic"],"fullName":"cascadeDeleteSection logic cleans references in settings","status":"passed","title":"cleans references in settings","duration":1.6803999999999633,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["cascadeDeleteSection logic"],"fullName":"cascadeDeleteSection logic cleans detour references in other sections","status":"passed","title":"cleans detour references in other sections","duration":0.5488000000000284,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["cascadeDeleteSection logic"],"fullName":"cascadeDeleteSection logic cleans server routing references and resets routing_mode","status":"passed","title":"cleans server routing references and resets routing_mode","duration":0.661200000000008,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["cascadeDeleteSection logic"],"fullName":"cascadeDeleteSection logic removes disk cache files","status":"passed","title":"removes disk cache files","duration":1.4033999999999764,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["cascadeDeleteSection logic"],"fullName":"cascadeDeleteSection logic re-creating a section with the same name starts clean","status":"passed","title":"re-creating a section with the same name starts clean","duration":1.3578999999999724,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886094,"endTime":1789128886104.358,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tests/sectionCascadeDelete.test.ts"},{"assertionResults":[{"ancestorTitles":["formatOutboundType"],"fullName":"formatOutboundType formats protocol with transport in uppercase parentheses","status":"passed","title":"formats protocol with transport in uppercase parentheses","duration":2.7076999999999884,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["formatOutboundType"],"fullName":"formatOutboundType leaves protocol untouched if transport is not provided","status":"passed","title":"leaves protocol untouched if transport is not provided","duration":0.4685000000000059,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["formatOutboundType"],"fullName":"formatOutboundType handles group types without adding transport","status":"passed","title":"handles group types without adding transport","duration":0.31639999999998736,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887343,"endTime":1789128887346.4685,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/helpers/tests/formatOutboundType.test.ts"},{"assertionResults":[{"ancestorTitles":["canUseDirectClashApi"],"fullName":"canUseDirectClashApi allows direct Clash API access from HTTP LuCI","status":"passed","title":"allows direct Clash API access from HTTP LuCI","duration":1.9793999999999983,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["canUseDirectClashApi"],"fullName":"canUseDirectClashApi blocks direct Clash API access from HTTPS LuCI","status":"passed","title":"blocks direct Clash API access from HTTPS LuCI","duration":0.2839999999999918,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["canUseDirectClashApi"],"fullName":"canUseDirectClashApi blocks direct Clash API access outside a browser location","status":"passed","title":"blocks direct Clash API access outside a browser location","duration":0.1969999999999743,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887365,"endTime":1789128887368.197,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/helpers/tests/getClashApiUrl.test.js"},{"assertionResults":[{"ancestorTitles":["getProxyUrlTransport"],"fullName":"getProxyUrlTransport extracts transport from vless query parameters","status":"passed","title":"extracts transport from vless query parameters","duration":2.9273000000000025,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getProxyUrlTransport"],"fullName":"getProxyUrlTransport extracts transport from vmess base64 net field","status":"passed","title":"extracts transport from vmess base64 net field","duration":0.558099999999996,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getProxyUrlTransport"],"fullName":"getProxyUrlTransport returns undefined for invalid or empty links","status":"passed","title":"returns undefined for invalid or empty links","duration":0.39620000000002165,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887327,"endTime":1789128887331.3962,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/helpers/tests/getProxyUrlTransport.test.ts"},{"assertionResults":[{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI vless://uuid@example.com:443"],"fullName":"isCopyableProxyLink copyable proxy URI vless://uuid@example.com:443 returns true","status":"passed","title":"returns true","duration":2.4013999999999953,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI vmess://encoded"],"fullName":"isCopyableProxyLink copyable proxy URI vmess://encoded returns true","status":"passed","title":"returns true","duration":0.2541999999999973,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI trojan://password@example.com:443"],"fullName":"isCopyableProxyLink copyable proxy URI trojan://password@example.com:443 returns true","status":"passed","title":"returns true","duration":0.24230000000000018,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI ss://encoded@example.com:443"],"fullName":"isCopyableProxyLink copyable proxy URI ss://encoded@example.com:443 returns true","status":"passed","title":"returns true","duration":0.17490000000000805,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI ssr://encoded"],"fullName":"isCopyableProxyLink copyable proxy URI ssr://encoded returns true","status":"passed","title":"returns true","duration":0.21359999999998536,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI hysteria2://password@example.com:443"],"fullName":"isCopyableProxyLink copyable proxy URI hysteria2://password@example.com:443 returns true","status":"passed","title":"returns true","duration":0.1793999999999869,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI hy2://password@example.com:443"],"fullName":"isCopyableProxyLink copyable proxy URI hy2://password@example.com:443 returns true","status":"passed","title":"returns true","duration":0.2616000000000156,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI tuic://uuid:password@example.com:443"],"fullName":"isCopyableProxyLink copyable proxy URI tuic://uuid:password@example.com:443 returns true","status":"passed","title":"returns true","duration":0.2021000000000015,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI socks4://example.com:1080"],"fullName":"isCopyableProxyLink copyable proxy URI socks4://example.com:1080 returns true","status":"passed","title":"returns true","duration":0.6756000000000029,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI socks4a://example.com:1080"],"fullName":"isCopyableProxyLink copyable proxy URI socks4a://example.com:1080 returns true","status":"passed","title":"returns true","duration":0.2675000000000125,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI socks5://user:pass@example.com:1080"],"fullName":"isCopyableProxyLink copyable proxy URI socks5://user:pass@example.com:1080 returns true","status":"passed","title":"returns true","duration":0.25260000000000105,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI http://example.com:80"],"fullName":"isCopyableProxyLink copyable proxy URI http://example.com:80 returns true","status":"passed","title":"returns true","duration":0.18469999999999231,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","copyable proxy URI https://user:pass@example.com:443"],"fullName":"isCopyableProxyLink copyable proxy URI https://user:pass@example.com:443 returns true","status":"passed","title":"returns true","duration":0.10450000000000159,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","non-copyable value "],"fullName":"isCopyableProxyLink non-copyable value returns false","status":"passed","title":"returns false","duration":0.13530000000000086,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","non-copyable value direct"],"fullName":"isCopyableProxyLink non-copyable value direct returns false","status":"passed","title":"returns false","duration":0.11379999999999768,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","non-copyable value block"],"fullName":"isCopyableProxyLink non-copyable value block returns false","status":"passed","title":"returns false","duration":0.09889999999998622,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","non-copyable value urltest"],"fullName":"isCopyableProxyLink non-copyable value urltest returns false","status":"passed","title":"returns false","duration":0.09519999999997708,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","non-copyable value wireguard://example.com"],"fullName":"isCopyableProxyLink non-copyable value wireguard://example.com returns false","status":"passed","title":"returns false","duration":0.11750000000000682,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isCopyableProxyLink","non-copyable value tcp://127.0.0.1:8080"],"fullName":"isCopyableProxyLink non-copyable value tcp://127.0.0.1:8080 returns false","status":"passed","title":"returns false","duration":0.08770000000001232,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886971,"endTime":1789128886978.1353,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/helpers/tests/isCopyableProxyLink.test.js"},{"assertionResults":[{"ancestorTitles":["buildHysteria2Link"],"fullName":"buildHysteria2Link exports a single certificate pin alias and omits stale obfs password when obfs is disabled","status":"passed","title":"exports a single certificate pin alias and omits stale obfs password when obfs is disabled","duration":75.30769999999995,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["buildHysteria2Link"],"fullName":"buildHysteria2Link exports salamander obfuscation together with its password","status":"passed","title":"exports salamander obfuscation together with its password","duration":42.45619999999997,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["buildHysteria2Link"],"fullName":"buildHysteria2Link brackets IPv6 public host in exported URI","status":"passed","title":"brackets IPv6 public host in exported URI","duration":32.34559999999999,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886146,"endTime":1789128886296.3457,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/helpers/tests/serverHysteria2Link.test.js"},{"assertionResults":[{"ancestorTitles":["validateIP IPv4","Valid IP: Private LAN"],"fullName":"validateIP IPv4 Valid IP: Private LAN returns {valid:true} for \"192.168.1.1\"","status":"passed","title":"returns {valid:true} for \"192.168.1.1\"","duration":2.5329000000000406,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Valid IP: All zeros"],"fullName":"validateIP IPv4 Valid IP: All zeros returns {valid:true} for \"0.0.0.0\"","status":"passed","title":"returns {valid:true} for \"0.0.0.0\"","duration":0.24230000000000018,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Valid IP: Broadcast"],"fullName":"validateIP IPv4 Valid IP: Broadcast returns {valid:true} for \"255.255.255.255\"","status":"passed","title":"returns {valid:true} for \"255.255.255.255\"","duration":0.25909999999998945,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Valid IP: Simple"],"fullName":"validateIP IPv4 Valid IP: Simple returns {valid:true} for \"1.2.3.4\"","status":"passed","title":"returns {valid:true} for \"1.2.3.4\"","duration":0.22270000000003165,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Valid IP: Loopback"],"fullName":"validateIP IPv4 Valid IP: Loopback returns {valid:true} for \"127.0.0.1\"","status":"passed","title":"returns {valid:true} for \"127.0.0.1\"","duration":0.24540000000001783,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Octet too large"],"fullName":"validateIP IPv4 Invalid IP: Octet too large returns {valid:false} for \"256.0.0.1\"","status":"passed","title":"returns {valid:false} for \"256.0.0.1\"","duration":0.21310000000005402,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Too few octets"],"fullName":"validateIP IPv4 Invalid IP: Too few octets returns {valid:false} for \"192.168.1\"","status":"passed","title":"returns {valid:false} for \"192.168.1\"","duration":0.2557000000000471,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Too many octets"],"fullName":"validateIP IPv4 Invalid IP: Too many octets returns {valid:false} for \"1.2.3.4.5\"","status":"passed","title":"returns {valid:false} for \"1.2.3.4.5\"","duration":0.19180000000005748,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Leading zero (1st octet)"],"fullName":"validateIP IPv4 Invalid IP: Leading zero (1st octet) returns {valid:false} for \"01.2.3.4\"","status":"passed","title":"returns {valid:false} for \"01.2.3.4\"","duration":0.7024999999999864,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Leading zero (2nd octet)"],"fullName":"validateIP IPv4 Invalid IP: Leading zero (2nd octet) returns {valid:false} for \"1.02.3.4\"","status":"passed","title":"returns {valid:false} for \"1.02.3.4\"","duration":0.2998999999999796,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Leading zero (3rd octet)"],"fullName":"validateIP IPv4 Invalid IP: Leading zero (3rd octet) returns {valid:false} for \"1.2.003.4\"","status":"passed","title":"returns {valid:false} for \"1.2.003.4\"","duration":0.23709999999994125,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Leading zero (4th octet)"],"fullName":"validateIP IPv4 Invalid IP: Leading zero (4th octet) returns {valid:false} for \"1.2.3.004\"","status":"passed","title":"returns {valid:false} for \"1.2.3.004\"","duration":0.147199999999998,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Four digits in octet"],"fullName":"validateIP IPv4 Invalid IP: Four digits in octet returns {valid:false} for \"1.2.3.0004\"","status":"passed","title":"returns {valid:false} for \"1.2.3.0004\"","duration":0.10450000000003001,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Trailing dot"],"fullName":"validateIP IPv4 Invalid IP: Trailing dot returns {valid:false} for \"1.2.3.\"","status":"passed","title":"returns {valid:false} for \"1.2.3.\"","duration":0.1150000000000091,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Valid IPv6: Loopback"],"fullName":"validateIP IPv6 Valid IPv6: Loopback returns {valid:true} for \"::1\"","status":"passed","title":"returns {valid:true} for \"::1\"","duration":0.1574000000000524,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Valid IPv6: ULA"],"fullName":"validateIP IPv6 Valid IPv6: ULA returns {valid:true} for \"fc00::1\"","status":"passed","title":"returns {valid:true} for \"fc00::1\"","duration":0.12859999999989213,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Valid IPv6: Documentation prefix"],"fullName":"validateIP IPv6 Valid IPv6: Documentation prefix returns {valid:true} for \"2001:db8::1\"","status":"passed","title":"returns {valid:true} for \"2001:db8::1\"","duration":0.12440000000003693,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Valid IPv6: Full form"],"fullName":"validateIP IPv6 Valid IPv6: Full form returns {valid:true} for \"2001:0db8:0000:0000:0000:ff00:0042:8329\"","status":"passed","title":"returns {valid:true} for \"2001:0db8:0000:0000:0000:ff00:0042:8329\"","duration":0.12580000000002656,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Invalid IPv6: Bad hex"],"fullName":"validateIP IPv6 Invalid IPv6: Bad hex returns {valid:false} for \"2001:db8::zzzz\"","status":"passed","title":"returns {valid:false} for \"2001:db8::zzzz\"","duration":0.19429999999999836,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Invalid IPv6: Two compression markers"],"fullName":"validateIP IPv6 Invalid IPv6: Two compression markers returns {valid:false} for \"2001::db8::1\"","status":"passed","title":"returns {valid:false} for \"2001::db8::1\"","duration":0.12580000000002656,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Invalid IPv6: Zone id"],"fullName":"validateIP IPv6 Invalid IPv6: Zone id returns {valid:false} for \"fe80::1%eth0\"","status":"passed","title":"returns {valid:false} for \"fe80::1%eth0\"","duration":0.09330000000011296,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Simple domain"],"fullName":"validateDomain Valid domain: Simple domain returns valid=true for \"example.com\"","status":"passed","title":"returns valid=true for \"example.com\"","duration":0.4520999999999731,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Subdomain"],"fullName":"validateDomain Valid domain: Subdomain returns valid=true for \"sub.example.com\"","status":"passed","title":"returns valid=true for \"sub.example.com\"","duration":0.1872000000000753,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: With dash"],"fullName":"validateDomain Valid domain: With dash returns valid=true for \"my-site.org\"","status":"passed","title":"returns valid=true for \"my-site.org\"","duration":0.13599999999996726,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: With numbers"],"fullName":"validateDomain Valid domain: With numbers returns valid=true for \"site123.net\"","status":"passed","title":"returns valid=true for \"site123.net\"","duration":0.1391000000000986,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Deep subdomain"],"fullName":"validateDomain Valid domain: Deep subdomain returns valid=true for \"a.b.c.example.co.uk\"","status":"passed","title":"returns valid=true for \"a.b.c.example.co.uk\"","duration":0.14179999999998927,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: With path"],"fullName":"validateDomain Valid domain: With path returns valid=true for \"example.com/path/to/resource\"","status":"passed","title":"returns valid=true for \"example.com/path/to/resource\"","duration":0.16150000000004638,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Punycode RU"],"fullName":"validateDomain Valid domain: Punycode RU returns valid=true for \"xn--d1acufc.xn--p1ai\"","status":"passed","title":"returns valid=true for \"xn--d1acufc.xn--p1ai\"","duration":0.16989999999998417,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Unicode RU"],"fullName":"validateDomain Valid domain: Unicode RU returns valid=true for \"сайт.рф\"","status":"passed","title":"returns valid=true for \"сайт.рф\"","duration":0.17240000000003874,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Unicode RU uppercase"],"fullName":"validateDomain Valid domain: Unicode RU uppercase returns valid=true for \"САЙТ.РФ\"","status":"passed","title":"returns valid=true for \"САЙТ.РФ\"","duration":0.19619999999997617,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Unicode test domain"],"fullName":"validateDomain Valid domain: Unicode test domain returns valid=true for \"пример.испытание\"","status":"passed","title":"returns valid=true for \"пример.испытание\"","duration":0.14569999999991978,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Unicode latin label"],"fullName":"validateDomain Valid domain: Unicode latin label returns valid=true for \"münich.example\"","status":"passed","title":"returns valid=true for \"münich.example\"","duration":0.1372000000000071,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Adguard dns"],"fullName":"validateDomain Valid domain: Adguard dns returns valid=true for \"dns.adguard-dns.com\"","status":"passed","title":"returns valid=true for \"dns.adguard-dns.com\"","duration":0.10759999999993397,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Nextdns dns"],"fullName":"validateDomain Valid domain: Nextdns dns returns valid=true for \"dns.nextdns.io/xxxxxxx\"","status":"passed","title":"returns valid=true for \"dns.nextdns.io/xxxxxxx\"","duration":0.15380000000004657,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Long domain (63 chars in label)"],"fullName":"validateDomain Valid domain: Long domain (63 chars in label) returns valid=true for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","status":"passed","title":"returns valid=true for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","duration":0.125,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: No TLD"],"fullName":"validateDomain Invalid domain: No TLD returns valid=false for \"localhost\"","status":"passed","title":"returns valid=false for \"localhost\"","duration":0.14960000000007767,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Only TLD"],"fullName":"validateDomain Invalid domain: Only TLD returns valid=false for \".com\"","status":"passed","title":"returns valid=false for \".com\"","duration":0.10190000000000055,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Double dot"],"fullName":"validateDomain Invalid domain: Double dot returns valid=false for \"example..com\"","status":"passed","title":"returns valid=false for \"example..com\"","duration":1.7666999999999007,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Illegal chars"],"fullName":"validateDomain Invalid domain: Illegal chars returns valid=false for \"exa!mple.com\"","status":"passed","title":"returns valid=false for \"exa!mple.com\"","duration":0.11230000000000473,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Space inside"],"fullName":"validateDomain Invalid domain: Space inside returns valid=false for \"exa mple.com\"","status":"passed","title":"returns valid=false for \"exa mple.com\"","duration":0.07529999999997017,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Ending with dash"],"fullName":"validateDomain Invalid domain: Ending with dash returns valid=false for \"example-.com\"","status":"passed","title":"returns valid=false for \"example-.com\"","duration":0.08000000000004093,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Starting with dash"],"fullName":"validateDomain Invalid domain: Starting with dash returns valid=false for \"-example.com\"","status":"passed","title":"returns valid=false for \"-example.com\"","duration":0.0946999999999889,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Trailing dot"],"fullName":"validateDomain Invalid domain: Trailing dot returns valid=false for \"example.com.\"","status":"passed","title":"returns valid=false for \"example.com.\"","duration":0.08900000000005548,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Too short TLD"],"fullName":"validateDomain Invalid domain: Too short TLD returns valid=false for \"example.c\"","status":"passed","title":"returns valid=false for \"example.c\"","duration":0.11159999999995307,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: With protocol (not allowed)"],"fullName":"validateDomain Invalid domain: With protocol (not allowed) returns valid=false for \"http://example.com\"","status":"passed","title":"returns valid=false for \"http://example.com\"","duration":0.10529999999994288,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Too long label (>63 chars)"],"fullName":"validateDomain Invalid domain: Too long label (>63 chars) returns valid=false for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","status":"passed","title":"returns valid=false for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","duration":0.11599999999998545,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Too long domain (>253 chars)"],"fullName":"validateDomain Invalid domain: Too long domain (>253 chars) returns valid=false for \"abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.com\"","status":"passed","title":"returns valid=false for \"abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.com\"","duration":0.2520999999999276,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Dot TLD allowed (.net)"],"fullName":"validateDomain Dot TLD toggle: Dot TLD allowed (.net) \".net\" with allowDotTLD=true → valid=true","status":"passed","title":"\".net\" with allowDotTLD=true → valid=true","duration":0.1590000000001055,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Dot TLD allowed (.рф)"],"fullName":"validateDomain Dot TLD toggle: Dot TLD allowed (.рф) \".рф\" with allowDotTLD=true → valid=true","status":"passed","title":"\".рф\" with allowDotTLD=true → valid=true","duration":0.12570000000005166,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Dot TLD not allowed (.net)"],"fullName":"validateDomain Dot TLD toggle: Dot TLD not allowed (.net) \".net\" with allowDotTLD=false → valid=false","status":"passed","title":"\".net\" with allowDotTLD=false → valid=false","duration":0.0806999999999789,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Invalid with double dot"],"fullName":"validateDomain Dot TLD toggle: Invalid with double dot \"..net\" with allowDotTLD=true → valid=false","status":"passed","title":"\"..net\" with allowDotTLD=true → valid=false","duration":0.07690000000002328,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Invalid dot domain"],"fullName":"validateDomain Dot TLD toggle: Invalid dot domain \".example.com\" with allowDotTLD=true → valid=false","status":"passed","title":"\".example.com\" with allowDotTLD=true → valid=false","duration":0.084699999999998,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Invalid single word TLD (net)"],"fullName":"validateDomain Dot TLD toggle: Invalid single word TLD (net) \"net\" with allowDotTLD=true → valid=false","status":"passed","title":"\"net\" with allowDotTLD=true → valid=false","duration":0.10040000000003602,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Private LAN"],"fullName":"validateDns Valid dns: Private LAN returns valid=true for \"192.168.1.1\"","status":"passed","title":"returns valid=true for \"192.168.1.1\"","duration":0.8882999999999583,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: All zeros"],"fullName":"validateDns Valid dns: All zeros returns valid=true for \"0.0.0.0\"","status":"passed","title":"returns valid=true for \"0.0.0.0\"","duration":0.1852999999999838,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Broadcast"],"fullName":"validateDns Valid dns: Broadcast returns valid=true for \"255.255.255.255\"","status":"passed","title":"returns valid=true for \"255.255.255.255\"","duration":0.11810000000002674,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Simple"],"fullName":"validateDns Valid dns: Simple returns valid=true for \"1.2.3.4\"","status":"passed","title":"returns valid=true for \"1.2.3.4\"","duration":0.10540000000003147,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Loopback"],"fullName":"validateDns Valid dns: Loopback returns valid=true for \"127.0.0.1\"","status":"passed","title":"returns valid=true for \"127.0.0.1\"","duration":0.13350000000002638,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Simple domain"],"fullName":"validateDns Valid dns: Simple domain returns valid=true for \"example.com\"","status":"passed","title":"returns valid=true for \"example.com\"","duration":0.11519999999995889,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Subdomain"],"fullName":"validateDns Valid dns: Subdomain returns valid=true for \"sub.example.com\"","status":"passed","title":"returns valid=true for \"sub.example.com\"","duration":0.11149999999997817,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: With dash"],"fullName":"validateDns Valid dns: With dash returns valid=true for \"my-site.org\"","status":"passed","title":"returns valid=true for \"my-site.org\"","duration":0.11590000000001055,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: With numbers"],"fullName":"validateDns Valid dns: With numbers returns valid=true for \"site123.net\"","status":"passed","title":"returns valid=true for \"site123.net\"","duration":0.11820000000000164,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Deep subdomain"],"fullName":"validateDns Valid dns: Deep subdomain returns valid=true for \"a.b.c.example.co.uk\"","status":"passed","title":"returns valid=true for \"a.b.c.example.co.uk\"","duration":0.20060000000000855,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: With path"],"fullName":"validateDns Valid dns: With path returns valid=true for \"example.com/path/to/resource\"","status":"passed","title":"returns valid=true for \"example.com/path/to/resource\"","duration":0.24080000000003565,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Punycode RU"],"fullName":"validateDns Valid dns: Punycode RU returns valid=true for \"xn--d1acufc.xn--p1ai\"","status":"passed","title":"returns valid=true for \"xn--d1acufc.xn--p1ai\"","duration":0.12220000000002074,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Unicode RU"],"fullName":"validateDns Valid dns: Unicode RU returns valid=true for \"сайт.рф\"","status":"passed","title":"returns valid=true for \"сайт.рф\"","duration":0.21449999999992997,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Unicode RU uppercase"],"fullName":"validateDns Valid dns: Unicode RU uppercase returns valid=true for \"САЙТ.РФ\"","status":"passed","title":"returns valid=true for \"САЙТ.РФ\"","duration":0.11399999999991905,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Unicode test domain"],"fullName":"validateDns Valid dns: Unicode test domain returns valid=true for \"пример.испытание\"","status":"passed","title":"returns valid=true for \"пример.испытание\"","duration":0.14650000000006003,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Unicode latin label"],"fullName":"validateDns Valid dns: Unicode latin label returns valid=true for \"münich.example\"","status":"passed","title":"returns valid=true for \"münich.example\"","duration":0.15689999999995052,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Adguard dns"],"fullName":"validateDns Valid dns: Adguard dns returns valid=true for \"dns.adguard-dns.com\"","status":"passed","title":"returns valid=true for \"dns.adguard-dns.com\"","duration":0.11249999999995453,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Nextdns dns"],"fullName":"validateDns Valid dns: Nextdns dns returns valid=true for \"dns.nextdns.io/xxxxxxx\"","status":"passed","title":"returns valid=true for \"dns.nextdns.io/xxxxxxx\"","duration":0.10550000000000637,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Long domain (63 chars in label)"],"fullName":"validateDns Valid dns: Long domain (63 chars in label) returns valid=true for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","status":"passed","title":"returns valid=true for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","duration":0.12590000000000146,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Google DNS (port 53)"],"fullName":"validateDns Valid dns: Google DNS (port 53) returns valid=true for \"8.8.8.8:53\"","status":"passed","title":"returns valid=true for \"8.8.8.8:53\"","duration":0.24350000000004002,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Google DNS (port 5353)"],"fullName":"validateDns Valid dns: Google DNS (port 5353) returns valid=true for \"8.8.8.8:5353\"","status":"passed","title":"returns valid=true for \"8.8.8.8:5353\"","duration":0.14950000000010277,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Cloudflare DNS (port 853)"],"fullName":"validateDns Valid dns: Cloudflare DNS (port 853) returns valid=true for \"1.1.1.1:853\"","status":"passed","title":"returns valid=true for \"1.1.1.1:853\"","duration":0.09429999999997563,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: Cloudflare domain (port 853)"],"fullName":"validateDns Valid dns: Cloudflare domain (port 853) returns valid=true for \"cloudflare-dns.com:853\"","status":"passed","title":"returns valid=true for \"cloudflare-dns.com:853\"","duration":0.15179999999998017,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: DoH IP"],"fullName":"validateDns Valid dns: DoH IP returns valid=true for \"1.1.1.1/dns-query\"","status":"passed","title":"returns valid=true for \"1.1.1.1/dns-query\"","duration":0.10059999999998581,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: DoH IP with port 443"],"fullName":"validateDns Valid dns: DoH IP with port 443 returns valid=true for \"1.1.1.1:443/dns-query\"","status":"passed","title":"returns valid=true for \"1.1.1.1:443/dns-query\"","duration":0.1027000000000271,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: DoH domain"],"fullName":"validateDns Valid dns: DoH domain returns valid=true for \"cloudflare-dns.com/dns-query\"","status":"passed","title":"returns valid=true for \"cloudflare-dns.com/dns-query\"","duration":0.11519999999995889,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: DoH domain with port 443"],"fullName":"validateDns Valid dns: DoH domain with port 443 returns valid=true for \"cloudflare-dns.com:443/dns-query\"","status":"passed","title":"returns valid=true for \"cloudflare-dns.com:443/dns-query\"","duration":0.13859999999999673,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: IPv6 DNS"],"fullName":"validateDns Valid dns: IPv6 DNS returns valid=true for \"2606:4700:4700::1111\"","status":"passed","title":"returns valid=true for \"2606:4700:4700::1111\"","duration":0.10419999999999163,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: IPv6 DNS with port"],"fullName":"validateDns Valid dns: IPv6 DNS with port returns valid=true for \"[2606:4700:4700::1111]:853\"","status":"passed","title":"returns valid=true for \"[2606:4700:4700::1111]:853\"","duration":0.12819999999999254,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Valid dns: IPv6 DoH with port"],"fullName":"validateDns Valid dns: IPv6 DoH with port returns valid=true for \"[2606:4700:4700::1111]:443/dns-query\"","status":"passed","title":"returns valid=true for \"[2606:4700:4700::1111]:443/dns-query\"","duration":0.10170000000005075,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Octet too large"],"fullName":"validateDns Invalid dns: Octet too large returns valid=false for \"256.0.0.1\"","status":"passed","title":"returns valid=false for \"256.0.0.1\"","duration":0.16049999999995634,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Too few octets"],"fullName":"validateDns Invalid dns: Too few octets returns valid=false for \"192.168.1\"","status":"passed","title":"returns valid=false for \"192.168.1\"","duration":0.10520000000008167,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Too many octets"],"fullName":"validateDns Invalid dns: Too many octets returns valid=false for \"1.2.3.4.5\"","status":"passed","title":"returns valid=false for \"1.2.3.4.5\"","duration":0.10730000000000928,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Leading zero (1st octet)"],"fullName":"validateDns Invalid dns: Leading zero (1st octet) returns valid=false for \"01.2.3.4\"","status":"passed","title":"returns valid=false for \"01.2.3.4\"","duration":0.08820000000002892,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Leading zero (2nd octet)"],"fullName":"validateDns Invalid dns: Leading zero (2nd octet) returns valid=false for \"1.02.3.4\"","status":"passed","title":"returns valid=false for \"1.02.3.4\"","duration":0.08989999999994325,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Leading zero (3rd octet)"],"fullName":"validateDns Invalid dns: Leading zero (3rd octet) returns valid=false for \"1.2.003.4\"","status":"passed","title":"returns valid=false for \"1.2.003.4\"","duration":0.08690000000001419,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Leading zero (4th octet)"],"fullName":"validateDns Invalid dns: Leading zero (4th octet) returns valid=false for \"1.2.3.004\"","status":"passed","title":"returns valid=false for \"1.2.3.004\"","duration":0.09760000000005675,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Four digits in octet"],"fullName":"validateDns Invalid dns: Four digits in octet returns valid=false for \"1.2.3.0004\"","status":"passed","title":"returns valid=false for \"1.2.3.0004\"","duration":0.08149999999989177,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Trailing dot"],"fullName":"validateDns Invalid dns: Trailing dot returns valid=false for \"1.2.3.\"","status":"passed","title":"returns valid=false for \"1.2.3.\"","duration":0.07999999999992724,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: No TLD"],"fullName":"validateDns Invalid dns: No TLD returns valid=false for \"localhost\"","status":"passed","title":"returns valid=false for \"localhost\"","duration":0.088799999999992,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Only TLD"],"fullName":"validateDns Invalid dns: Only TLD returns valid=false for \".com\"","status":"passed","title":"returns valid=false for \".com\"","duration":0.07429999999999382,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Double dot"],"fullName":"validateDns Invalid dns: Double dot returns valid=false for \"example..com\"","status":"passed","title":"returns valid=false for \"example..com\"","duration":0.08209999999996853,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Illegal chars"],"fullName":"validateDns Invalid dns: Illegal chars returns valid=false for \"exa!mple.com\"","status":"passed","title":"returns valid=false for \"exa!mple.com\"","duration":0.08579999999994925,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Space inside"],"fullName":"validateDns Invalid dns: Space inside returns valid=false for \"exa mple.com\"","status":"passed","title":"returns valid=false for \"exa mple.com\"","duration":0.07079999999996289,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Ending with dash"],"fullName":"validateDns Invalid dns: Ending with dash returns valid=false for \"example-.com\"","status":"passed","title":"returns valid=false for \"example-.com\"","duration":0.07439999999996871,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Starting with dash"],"fullName":"validateDns Invalid dns: Starting with dash returns valid=false for \"-example.com\"","status":"passed","title":"returns valid=false for \"-example.com\"","duration":0.07569999999998345,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Trailing dot"],"fullName":"validateDns Invalid dns: Trailing dot returns valid=false for \"example.com.\"","status":"passed","title":"returns valid=false for \"example.com.\"","duration":0.08609999999998763,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Too short TLD"],"fullName":"validateDns Invalid dns: Too short TLD returns valid=false for \"example.c\"","status":"passed","title":"returns valid=false for \"example.c\"","duration":0.07799999999997453,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: With protocol (not allowed)"],"fullName":"validateDns Invalid dns: With protocol (not allowed) returns valid=false for \"http://example.com\"","status":"passed","title":"returns valid=false for \"http://example.com\"","duration":0.10430000000008022,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Too long label (>63 chars)"],"fullName":"validateDns Invalid dns: Too long label (>63 chars) returns valid=false for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","status":"passed","title":"returns valid=false for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","duration":0.08289999999999509,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Too long domain (>253 chars)"],"fullName":"validateDns Invalid dns: Too long domain (>253 chars) returns valid=false for \"abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.com\"","status":"passed","title":"returns valid=false for \"abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.com\"","duration":0.0746000000000322,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Non-numeric port"],"fullName":"validateDns Invalid dns: Non-numeric port returns valid=false for \"8.8.8.8:abc\"","status":"passed","title":"returns valid=false for \"8.8.8.8:abc\"","duration":0.08570000000008804,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Port too high"],"fullName":"validateDns Invalid dns: Port too high returns valid=false for \"dns.example.com:99999\"","status":"passed","title":"returns valid=false for \"dns.example.com:99999\"","duration":0.0907999999999447,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDns","Invalid dns: Zero IPv6 port"],"fullName":"validateDns Invalid dns: Zero IPv6 port returns valid=false for \"[2606:4700:4700::1111]:0\"","status":"passed","title":"returns valid=false for \"[2606:4700:4700::1111]:0\"","duration":0.0745000000000573,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886321,"endTime":1789128886344.1042,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateDns.test.js"},{"assertionResults":[{"ancestorTitles":["validateDomain","Valid domain: Simple domain"],"fullName":"validateDomain Valid domain: Simple domain returns valid=true for \"example.com\"","status":"passed","title":"returns valid=true for \"example.com\"","duration":3.033799999999985,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Subdomain"],"fullName":"validateDomain Valid domain: Subdomain returns valid=true for \"sub.example.com\"","status":"passed","title":"returns valid=true for \"sub.example.com\"","duration":0.30100000000001614,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: With dash"],"fullName":"validateDomain Valid domain: With dash returns valid=true for \"my-site.org\"","status":"passed","title":"returns valid=true for \"my-site.org\"","duration":0.28069999999999595,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: With numbers"],"fullName":"validateDomain Valid domain: With numbers returns valid=true for \"site123.net\"","status":"passed","title":"returns valid=true for \"site123.net\"","duration":0.22810000000001196,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Deep subdomain"],"fullName":"validateDomain Valid domain: Deep subdomain returns valid=true for \"a.b.c.example.co.uk\"","status":"passed","title":"returns valid=true for \"a.b.c.example.co.uk\"","duration":0.30109999999999104,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: With path"],"fullName":"validateDomain Valid domain: With path returns valid=true for \"example.com/path/to/resource\"","status":"passed","title":"returns valid=true for \"example.com/path/to/resource\"","duration":0.19540000000000646,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Punycode RU"],"fullName":"validateDomain Valid domain: Punycode RU returns valid=true for \"xn--d1acufc.xn--p1ai\"","status":"passed","title":"returns valid=true for \"xn--d1acufc.xn--p1ai\"","duration":0.24580000000000268,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Unicode RU"],"fullName":"validateDomain Valid domain: Unicode RU returns valid=true for \"сайт.рф\"","status":"passed","title":"returns valid=true for \"сайт.рф\"","duration":0.23089999999999122,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Unicode RU uppercase"],"fullName":"validateDomain Valid domain: Unicode RU uppercase returns valid=true for \"САЙТ.РФ\"","status":"passed","title":"returns valid=true for \"САЙТ.РФ\"","duration":0.8575000000000159,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Unicode test domain"],"fullName":"validateDomain Valid domain: Unicode test domain returns valid=true for \"пример.испытание\"","status":"passed","title":"returns valid=true for \"пример.испытание\"","duration":0.38990000000001146,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Unicode latin label"],"fullName":"validateDomain Valid domain: Unicode latin label returns valid=true for \"münich.example\"","status":"passed","title":"returns valid=true for \"münich.example\"","duration":0.2394000000000176,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Adguard dns"],"fullName":"validateDomain Valid domain: Adguard dns returns valid=true for \"dns.adguard-dns.com\"","status":"passed","title":"returns valid=true for \"dns.adguard-dns.com\"","duration":0.09590000000000032,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Nextdns dns"],"fullName":"validateDomain Valid domain: Nextdns dns returns valid=true for \"dns.nextdns.io/xxxxxxx\"","status":"passed","title":"returns valid=true for \"dns.nextdns.io/xxxxxxx\"","duration":0.11019999999999186,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Valid domain: Long domain (63 chars in label)"],"fullName":"validateDomain Valid domain: Long domain (63 chars in label) returns valid=true for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","status":"passed","title":"returns valid=true for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","duration":0.08299999999999841,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: No TLD"],"fullName":"validateDomain Invalid domain: No TLD returns valid=false for \"localhost\"","status":"passed","title":"returns valid=false for \"localhost\"","duration":0.11590000000001055,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Only TLD"],"fullName":"validateDomain Invalid domain: Only TLD returns valid=false for \".com\"","status":"passed","title":"returns valid=false for \".com\"","duration":0.06940000000000168,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Double dot"],"fullName":"validateDomain Invalid domain: Double dot returns valid=false for \"example..com\"","status":"passed","title":"returns valid=false for \"example..com\"","duration":0.06749999999999545,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Illegal chars"],"fullName":"validateDomain Invalid domain: Illegal chars returns valid=false for \"exa!mple.com\"","status":"passed","title":"returns valid=false for \"exa!mple.com\"","duration":0.06729999999998881,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Space inside"],"fullName":"validateDomain Invalid domain: Space inside returns valid=false for \"exa mple.com\"","status":"passed","title":"returns valid=false for \"exa mple.com\"","duration":0.073599999999999,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Ending with dash"],"fullName":"validateDomain Invalid domain: Ending with dash returns valid=false for \"example-.com\"","status":"passed","title":"returns valid=false for \"example-.com\"","duration":0.06770000000000209,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Starting with dash"],"fullName":"validateDomain Invalid domain: Starting with dash returns valid=false for \"-example.com\"","status":"passed","title":"returns valid=false for \"-example.com\"","duration":0.06379999999998631,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Trailing dot"],"fullName":"validateDomain Invalid domain: Trailing dot returns valid=false for \"example.com.\"","status":"passed","title":"returns valid=false for \"example.com.\"","duration":0.056199999999989814,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Too short TLD"],"fullName":"validateDomain Invalid domain: Too short TLD returns valid=false for \"example.c\"","status":"passed","title":"returns valid=false for \"example.c\"","duration":0.06569999999999254,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: With protocol (not allowed)"],"fullName":"validateDomain Invalid domain: With protocol (not allowed) returns valid=false for \"http://example.com\"","status":"passed","title":"returns valid=false for \"http://example.com\"","duration":0.06470000000001619,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Too long label (>63 chars)"],"fullName":"validateDomain Invalid domain: Too long label (>63 chars) returns valid=false for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","status":"passed","title":"returns valid=false for \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com\"","duration":0.09189999999998122,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Invalid domain: Too long domain (>253 chars)"],"fullName":"validateDomain Invalid domain: Too long domain (>253 chars) returns valid=false for \"abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.com\"","status":"passed","title":"returns valid=false for \"abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.abcdef.com\"","duration":0.08249999999998181,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Dot TLD allowed (.net)"],"fullName":"validateDomain Dot TLD toggle: Dot TLD allowed (.net) \".net\" with allowDotTLD=true → valid=true","status":"passed","title":"\".net\" with allowDotTLD=true → valid=true","duration":0.113900000000001,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Dot TLD allowed (.рф)"],"fullName":"validateDomain Dot TLD toggle: Dot TLD allowed (.рф) \".рф\" with allowDotTLD=true → valid=true","status":"passed","title":"\".рф\" with allowDotTLD=true → valid=true","duration":0.0882000000000005,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Dot TLD not allowed (.net)"],"fullName":"validateDomain Dot TLD toggle: Dot TLD not allowed (.net) \".net\" with allowDotTLD=false → valid=false","status":"passed","title":"\".net\" with allowDotTLD=false → valid=false","duration":0.06019999999998049,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Invalid with double dot"],"fullName":"validateDomain Dot TLD toggle: Invalid with double dot \"..net\" with allowDotTLD=true → valid=false","status":"passed","title":"\"..net\" with allowDotTLD=true → valid=false","duration":0.07940000000002101,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Invalid dot domain"],"fullName":"validateDomain Dot TLD toggle: Invalid dot domain \".example.com\" with allowDotTLD=true → valid=false","status":"passed","title":"\".example.com\" with allowDotTLD=true → valid=false","duration":0.11969999999999459,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateDomain","Dot TLD toggle: Invalid single word TLD (net)"],"fullName":"validateDomain Dot TLD toggle: Invalid single word TLD (net) \"net\" with allowDotTLD=true → valid=false","status":"passed","title":"\"net\" with allowDotTLD=true → valid=false","duration":0.09709999999998331,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886484,"endTime":1789128886494.1196,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateDomain.test.js"},{"assertionResults":[{"ancestorTitles":["validateHttpProxyUrl","valid URL: http with port"],"fullName":"validateHttpProxyUrl valid URL: http with port returns valid=true","status":"passed","title":"returns valid=true","duration":3.4069000000000074,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","valid URL: https with port"],"fullName":"validateHttpProxyUrl valid URL: https with port returns valid=true","status":"passed","title":"returns valid=true","duration":0.41390000000001237,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","valid URL: http with auth"],"fullName":"validateHttpProxyUrl valid URL: http with auth returns valid=true","status":"passed","title":"returns valid=true","duration":0.33449999999999136,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","valid URL: http username only"],"fullName":"validateHttpProxyUrl valid URL: http username only returns valid=true","status":"passed","title":"returns valid=true","duration":0.2804000000000144,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","valid URL: http ip with port"],"fullName":"validateHttpProxyUrl valid URL: http ip with port returns valid=true","status":"passed","title":"returns valid=true","duration":0.29179999999999495,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","valid URL: http ipv6"],"fullName":"validateHttpProxyUrl valid URL: http ipv6 returns valid=true","status":"passed","title":"returns valid=true","duration":0.22129999999998518,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","valid URL: https ipv6"],"fullName":"validateHttpProxyUrl valid URL: https ipv6 returns valid=true","status":"passed","title":"returns valid=true","duration":0.262800000000027,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: missing port"],"fullName":"validateHttpProxyUrl invalid URL: missing port returns valid=false","status":"passed","title":"returns valid=false","duration":0.23850000000001614,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: missing host"],"fullName":"validateHttpProxyUrl invalid URL: missing host returns valid=false","status":"passed","title":"returns valid=false","duration":0.7502999999999815,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: invalid port"],"fullName":"validateHttpProxyUrl invalid URL: invalid port returns valid=false","status":"passed","title":"returns valid=false","duration":0.37949999999997885,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: path present"],"fullName":"validateHttpProxyUrl invalid URL: path present returns valid=false","status":"passed","title":"returns valid=false","duration":0.1862000000000137,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: query present"],"fullName":"validateHttpProxyUrl invalid URL: query present returns valid=false","status":"passed","title":"returns valid=false","duration":0.10970000000000368,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: space present"],"fullName":"validateHttpProxyUrl invalid URL: space present returns valid=false","status":"passed","title":"returns valid=false","duration":0.13120000000000687,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: empty username"],"fullName":"validateHttpProxyUrl invalid URL: empty username returns valid=false","status":"passed","title":"returns valid=false","duration":0.10969999999997526,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: unsupported scheme"],"fullName":"validateHttpProxyUrl invalid URL: unsupported scheme returns valid=false","status":"passed","title":"returns valid=false","duration":0.11100000000001842,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHttpProxyUrl","invalid URL: unbracketed ipv6 with port"],"fullName":"validateHttpProxyUrl invalid URL: unbracketed ipv6 with port returns valid=false","status":"passed","title":"returns valid=false","duration":0.10500000000001819,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886747,"endTime":1789128886756.131,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateHttpProxyUrl.test.js"},{"assertionResults":[{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: password basic"],"fullName":"validateHysteria2Url Valid HY2 URL: password basic returns valid=true for \"hysteria2://pass@example.com:443/#hy2-basic\"","status":"passed","title":"returns valid=true for \"hysteria2://pass@example.com:443/#hy2-basic\"","duration":3.45920000000001,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: insecure allowed"],"fullName":"validateHysteria2Url Valid HY2 URL: insecure allowed returns valid=true for \"hysteria2://pass@example.com:443/?insecure=1#hy2-insecure\"","status":"passed","title":"returns valid=true for \"hysteria2://pass@example.com:443/?insecure=1#hy2-insecure\"","duration":0.47680000000002565,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: SNI param"],"fullName":"validateHysteria2Url Valid HY2 URL: SNI param returns valid=true for \"hysteria2://pass@example.com:443/?sni=google.com#hy2-sni\"","status":"passed","title":"returns valid=true for \"hysteria2://pass@example.com:443/?sni=google.com#hy2-sni\"","duration":0.29129999999997835,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: Obfs + password"],"fullName":"validateHysteria2Url Valid HY2 URL: Obfs + password returns valid=true for \"hysteria2://mypassword@1.1.1.1:8443/?obfs=salamander&obfs-password=abc123#hy2-obfs\"","status":"passed","title":"returns valid=true for \"hysteria2://mypassword@1.1.1.1:8443/?obfs=salamander&obfs-password=abc123#hy2-obfs\"","duration":0.22399999999998954,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: All options combined"],"fullName":"validateHysteria2Url Valid HY2 URL: All options combined returns valid=true for \"hysteria2://pw@8.8.8.8:8443/?sni=example.com&obfs=salamander&obfs-password=hello&insecure=1#hy2-full\"","status":"passed","title":"returns valid=true for \"hysteria2://pw@8.8.8.8:8443/?sni=example.com&obfs=salamander&obfs-password=hello&insecure=1#hy2-full\"","duration":0.29539999999997235,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: obfs none = ok"],"fullName":"validateHysteria2Url Valid HY2 URL: obfs none = ok returns valid=true for \"hysteria2://pw@example.com:443/?obfs=none#hy2-none\"","status":"passed","title":"returns valid=true for \"hysteria2://pw@example.com:443/?obfs=none#hy2-none\"","duration":0.1997000000000071,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: port range 123,5000-6000"],"fullName":"validateHysteria2Url Valid HY2 URL: port range 123,5000-6000 returns valid=true for \"hysteria2://letmein@example.com:123,5000-6000/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com\"","status":"passed","title":"returns valid=true for \"hysteria2://letmein@example.com:123,5000-6000/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com\"","duration":0.2691000000000372,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: mport range"],"fullName":"validateHysteria2Url Valid HY2 URL: mport range returns valid=true for \"hysteria2://letmein@example.com:2053/?mport=20000-50000&insecure=1&obfs=salamander&obfs-password=gawrgura&sni=real.example.com\"","status":"passed","title":"returns valid=true for \"hysteria2://letmein@example.com:2053/?mport=20000-50000&insecure=1&obfs=salamander&obfs-password=gawrgura&sni=real.example.com\"","duration":0.1963000000000079,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Valid HY2 URL: IPv6 host"],"fullName":"validateHysteria2Url Valid HY2 URL: IPv6 host returns valid=true for \"hysteria2://pass@[2001:db8::1]:443/#hy2-ipv6\"","status":"passed","title":"returns valid=true for \"hysteria2://pass@[2001:db8::1]:443/#hy2-ipv6\"","duration":0.7500999999999749,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: No prefix"],"fullName":"validateHysteria2Url Invalid HY2 URL: No prefix returns valid=false for \"pw@example.com:443\"","status":"passed","title":"returns valid=false for \"pw@example.com:443\"","duration":0.244700000000023,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Missing password"],"fullName":"validateHysteria2Url Invalid HY2 URL: Missing password returns valid=false for \"hysteria2://@example.com:443/\"","status":"passed","title":"returns valid=false for \"hysteria2://@example.com:443/\"","duration":0.19400000000001683,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Missing host"],"fullName":"validateHysteria2Url Invalid HY2 URL: Missing host returns valid=false for \"hysteria2://pw@:443/\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@:443/\"","duration":0.10700000000002774,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Missing port"],"fullName":"validateHysteria2Url Invalid HY2 URL: Missing port returns valid=false for \"hysteria2://pw@example.com/\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com/\"","duration":0.0735000000000241,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Invalid host"],"fullName":"validateHysteria2Url Invalid HY2 URL: Invalid host returns valid=false for \"hysteria2://pw@bad_host:443/\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@bad_host:443/\"","duration":0.09520000000003392,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Non-numeric port"],"fullName":"validateHysteria2Url Invalid HY2 URL: Non-numeric port returns valid=false for \"hysteria2://pw@example.com:port/\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:port/\"","duration":0.117999999999995,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Port out of range"],"fullName":"validateHysteria2Url Invalid HY2 URL: Port out of range returns valid=false for \"hysteria2://pw@example.com:99999/\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:99999/\"","duration":0.10219999999998208,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Invalid port range order"],"fullName":"validateHysteria2Url Invalid HY2 URL: Invalid port range order returns valid=false for \"hysteria2://pw@example.com:6000-5000/\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:6000-5000/\"","duration":0.09499999999997044,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Invalid port range end"],"fullName":"validateHysteria2Url Invalid HY2 URL: Invalid port range end returns valid=false for \"hysteria2://pw@example.com:5000-/\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:5000-/\"","duration":0.13339999999999463,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Unbracketed IPv6 with port"],"fullName":"validateHysteria2Url Invalid HY2 URL: Unbracketed IPv6 with port returns valid=false for \"hysteria2://pw@2001:db8::1:443/\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@2001:db8::1:443/\"","duration":0.08440000000001646,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Unknown obfs type"],"fullName":"validateHysteria2Url Invalid HY2 URL: Unknown obfs type returns valid=false for \"hysteria2://pw@example.com:443/?obfs=weird\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:443/?obfs=weird\"","duration":0.09960000000000946,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: obfs without obfs-password"],"fullName":"validateHysteria2Url Invalid HY2 URL: obfs without obfs-password returns valid=false for \"hysteria2://pw@example.com:443/?obfs=salamander\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:443/?obfs=salamander\"","duration":0.10120000000000573,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: invalid insecure"],"fullName":"validateHysteria2Url Invalid HY2 URL: invalid insecure returns valid=false for \"hysteria2://pw@example.com:443/?insecure=5\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:443/?insecure=5\"","duration":0.10509999999999309,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: empty sni"],"fullName":"validateHysteria2Url Invalid HY2 URL: empty sni returns valid=false for \"hysteria2://pw@example.com:443/?sni=\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:443/?sni=\"","duration":0.09829999999999472,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url","Invalid HY2 URL: Invalid mport range end"],"fullName":"validateHysteria2Url Invalid HY2 URL: Invalid mport range end returns valid=false for \"hysteria2://pw@example.com:443/?mport=5000-\"","status":"passed","title":"returns valid=false for \"hysteria2://pw@example.com:443/?mport=5000-\"","duration":0.09429999999997563,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateHysteria2Url"],"fullName":"validateHysteria2Url detects invalid port range","status":"passed","title":"detects invalid port range","duration":0.11910000000000309,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886576,"endTime":1789128886585.1191,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateHysteriaUrl.test.js"},{"assertionResults":[{"ancestorTitles":["validateIP IPv4","Valid IP: Private LAN"],"fullName":"validateIP IPv4 Valid IP: Private LAN returns {valid:true} for \"192.168.1.1\"","status":"passed","title":"returns {valid:true} for \"192.168.1.1\"","duration":2.3906000000000063,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Valid IP: All zeros"],"fullName":"validateIP IPv4 Valid IP: All zeros returns {valid:true} for \"0.0.0.0\"","status":"passed","title":"returns {valid:true} for \"0.0.0.0\"","duration":0.25320000000002096,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Valid IP: Broadcast"],"fullName":"validateIP IPv4 Valid IP: Broadcast returns {valid:true} for \"255.255.255.255\"","status":"passed","title":"returns {valid:true} for \"255.255.255.255\"","duration":0.2599999999999909,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Valid IP: Simple"],"fullName":"validateIP IPv4 Valid IP: Simple returns {valid:true} for \"1.2.3.4\"","status":"passed","title":"returns {valid:true} for \"1.2.3.4\"","duration":0.23179999999999268,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Valid IP: Loopback"],"fullName":"validateIP IPv4 Valid IP: Loopback returns {valid:true} for \"127.0.0.1\"","status":"passed","title":"returns {valid:true} for \"127.0.0.1\"","duration":0.2780999999999949,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Octet too large"],"fullName":"validateIP IPv4 Invalid IP: Octet too large returns {valid:false} for \"256.0.0.1\"","status":"passed","title":"returns {valid:false} for \"256.0.0.1\"","duration":0.20989999999997622,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Too few octets"],"fullName":"validateIP IPv4 Invalid IP: Too few octets returns {valid:false} for \"192.168.1\"","status":"passed","title":"returns {valid:false} for \"192.168.1\"","duration":0.2295000000000016,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Too many octets"],"fullName":"validateIP IPv4 Invalid IP: Too many octets returns {valid:false} for \"1.2.3.4.5\"","status":"passed","title":"returns {valid:false} for \"1.2.3.4.5\"","duration":0.20319999999998117,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Leading zero (1st octet)"],"fullName":"validateIP IPv4 Invalid IP: Leading zero (1st octet) returns {valid:false} for \"01.2.3.4\"","status":"passed","title":"returns {valid:false} for \"01.2.3.4\"","duration":0.8079000000000178,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Leading zero (2nd octet)"],"fullName":"validateIP IPv4 Invalid IP: Leading zero (2nd octet) returns {valid:false} for \"1.02.3.4\"","status":"passed","title":"returns {valid:false} for \"1.02.3.4\"","duration":0.2615000000000123,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Leading zero (3rd octet)"],"fullName":"validateIP IPv4 Invalid IP: Leading zero (3rd octet) returns {valid:false} for \"1.2.003.4\"","status":"passed","title":"returns {valid:false} for \"1.2.003.4\"","duration":0.21739999999999782,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Leading zero (4th octet)"],"fullName":"validateIP IPv4 Invalid IP: Leading zero (4th octet) returns {valid:false} for \"1.2.3.004\"","status":"passed","title":"returns {valid:false} for \"1.2.3.004\"","duration":0.15379999999998972,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Four digits in octet"],"fullName":"validateIP IPv4 Invalid IP: Four digits in octet returns {valid:false} for \"1.2.3.0004\"","status":"passed","title":"returns {valid:false} for \"1.2.3.0004\"","duration":0.09809999999998809,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv4","Invalid IP: Trailing dot"],"fullName":"validateIP IPv4 Invalid IP: Trailing dot returns {valid:false} for \"1.2.3.\"","status":"passed","title":"returns {valid:false} for \"1.2.3.\"","duration":0.11369999999999436,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Valid IPv6: Loopback"],"fullName":"validateIP IPv6 Valid IPv6: Loopback returns {valid:true} for \"::1\"","status":"passed","title":"returns {valid:true} for \"::1\"","duration":0.13939999999999486,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Valid IPv6: ULA"],"fullName":"validateIP IPv6 Valid IPv6: ULA returns {valid:true} for \"fc00::1\"","status":"passed","title":"returns {valid:true} for \"fc00::1\"","duration":0.09720000000001505,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Valid IPv6: Documentation prefix"],"fullName":"validateIP IPv6 Valid IPv6: Documentation prefix returns {valid:true} for \"2001:db8::1\"","status":"passed","title":"returns {valid:true} for \"2001:db8::1\"","duration":0.08650000000000091,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Valid IPv6: Full form"],"fullName":"validateIP IPv6 Valid IPv6: Full form returns {valid:true} for \"2001:0db8:0000:0000:0000:ff00:0042:8329\"","status":"passed","title":"returns {valid:true} for \"2001:0db8:0000:0000:0000:ff00:0042:8329\"","duration":0.11840000000000828,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Invalid IPv6: Bad hex"],"fullName":"validateIP IPv6 Invalid IPv6: Bad hex returns {valid:false} for \"2001:db8::zzzz\"","status":"passed","title":"returns {valid:false} for \"2001:db8::zzzz\"","duration":0.20330000000001291,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Invalid IPv6: Two compression markers"],"fullName":"validateIP IPv6 Invalid IPv6: Two compression markers returns {valid:false} for \"2001::db8::1\"","status":"passed","title":"returns {valid:false} for \"2001::db8::1\"","duration":0.11400000000000432,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateIP IPv6","Invalid IPv6: Zone id"],"fullName":"validateIP IPv6 Invalid IPv6: Zone id returns {valid:false} for \"fe80::1%eth0\"","status":"passed","title":"returns {valid:false} for \"fe80::1%eth0\"","duration":0.09860000000000468,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886832,"endTime":1789128886840.2034,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateIp.test.js"},{"assertionResults":[{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson accepts a direct outbound with type and tag","status":"passed","title":"accepts a direct outbound with type and tag","duration":2.5751999999999953,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson accepts a server outbound with server and numeric server_port","status":"passed","title":"accepts a server outbound with server and numeric server_port","duration":0.46809999999999263,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson accepts HTTP only as a JSON outbound","status":"passed","title":"accepts HTTP only as a JSON outbound","duration":0.22580000000002087,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson accepts a URLTest outbound with referenced outbounds","status":"passed","title":"accepts a URLTest outbound with referenced outbounds","duration":0.3264999999999816,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson rejects empty input","status":"passed","title":"rejects empty input","duration":0.27649999999999864,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson rejects arrays","status":"passed","title":"rejects arrays","duration":0.3470000000000084,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson requires a non-empty type field","status":"passed","title":"requires a non-empty type field","duration":0.4072000000000173,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson requires a non-empty tag field","status":"passed","title":"requires a non-empty tag field","duration":0.2524999999999977,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson rejects a tag already used by another JSON outbound","status":"passed","title":"rejects a tag already used by another JSON outbound","duration":0.8582999999999856,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson requires server fields for common server outbound types","status":"passed","title":"requires server fields for common server outbound types","duration":0.377900000000011,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateOutboundJson"],"fullName":"validateOutboundJson requires group outbounds for selector-like types","status":"passed","title":"requires group outbounds for selector-like types","duration":0.30060000000000286,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887071,"endTime":1789128887078.3005,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateOutboundJson.test.js"},{"assertionResults":[{"ancestorTitles":["validatePath","Valid path: Single level"],"fullName":"validatePath Valid path: Single level returns valid=true for \"/etc\"","status":"passed","title":"returns valid=true for \"/etc\"","duration":2.318700000000007,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Valid path: Nested path"],"fullName":"validatePath Valid path: Nested path returns valid=true for \"/usr/local/bin\"","status":"passed","title":"returns valid=true for \"/usr/local/bin\"","duration":0.2429000000000201,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Valid path: With dash"],"fullName":"validatePath Valid path: With dash returns valid=true for \"/var/log/nginx-access\"","status":"passed","title":"returns valid=true for \"/var/log/nginx-access\"","duration":0.28000000000000114,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Valid path: With underscore"],"fullName":"validatePath Valid path: With underscore returns valid=true for \"/opt/my_app/config\"","status":"passed","title":"returns valid=true for \"/opt/my_app/config\"","duration":0.21210000000002083,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Valid path: With numbers"],"fullName":"validatePath Valid path: With numbers returns valid=true for \"/data123/files\"","status":"passed","title":"returns valid=true for \"/data123/files\"","duration":0.26730000000000587,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Valid path: With dots"],"fullName":"validatePath Valid path: With dots returns valid=true for \"/home/user/.config\"","status":"passed","title":"returns valid=true for \"/home/user/.config\"","duration":0.1810000000000116,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Valid path: Deep nested"],"fullName":"validatePath Valid path: Deep nested returns valid=true for \"/a/b/c/d/e/f/g\"","status":"passed","title":"returns valid=true for \"/a/b/c/d/e/f/g\"","duration":0.4309000000000083,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Invalid path: Empty string"],"fullName":"validatePath Invalid path: Empty string returns valid=false for \"\"","status":"passed","title":"returns valid=false for \"\"","duration":0.21399999999999864,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Invalid path: Missing starting slash"],"fullName":"validatePath Invalid path: Missing starting slash returns valid=false for \"usr/local\"","status":"passed","title":"returns valid=false for \"usr/local\"","duration":0.7088999999999999,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Invalid path: Only dot"],"fullName":"validatePath Invalid path: Only dot returns valid=false for \".\"","status":"passed","title":"returns valid=false for \".\"","duration":0.3032000000000039,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Invalid path: Space inside"],"fullName":"validatePath Invalid path: Space inside returns valid=false for \"/path with space\"","status":"passed","title":"returns valid=false for \"/path with space\"","duration":0.2768999999999835,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Invalid path: Illegal char"],"fullName":"validatePath Invalid path: Illegal char returns valid=false for \"/path$\"","status":"passed","title":"returns valid=false for \"/path$\"","duration":0.12309999999999377,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Invalid path: Backslash not allowed"],"fullName":"validatePath Invalid path: Backslash not allowed returns valid=false for \"\\windows\\path\"","status":"passed","title":"returns valid=false for \"\\windows\\path\"","duration":0.12970000000001392,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Invalid path: Relative path ./"],"fullName":"validatePath Invalid path: Relative path ./ returns valid=false for \"./relative\"","status":"passed","title":"returns valid=false for \"./relative\"","duration":0.11109999999999332,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validatePath","Invalid path: Relative path ../"],"fullName":"validatePath Invalid path: Relative path ../ returns valid=false for \"../parent\"","status":"passed","title":"returns valid=false for \"../parent\"","duration":0.11119999999999663,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887076,"endTime":1789128887083.2769,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validatePath.test.js"},{"assertionResults":[{"ancestorTitles":["validateProxyUrl","Valid proxy URL: shadowsocks"],"fullName":"validateProxyUrl Valid proxy URL: shadowsocks returns valid=true for \"ss://YWVzLTI1Ni1nY206cGFzcw@example.com:8388\"","status":"passed","title":"returns valid=true for \"ss://YWVzLTI1Ni1nY206cGFzcw@example.com:8388\"","duration":3.6458999999999833,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: vless"],"fullName":"validateProxyUrl Valid proxy URL: vless returns valid=true for \"vless://94792286-7bbe-4f33-8b36-18d1bbf70723@example.com:443?type=tcp&encryption=none&security=none\"","status":"passed","title":"returns valid=true for \"vless://94792286-7bbe-4f33-8b36-18d1bbf70723@example.com:443?type=tcp&encryption=none&security=none\"","duration":0.7008999999999901,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: vmess"],"fullName":"validateProxyUrl Valid proxy URL: vmess returns valid=true for \"vmess://eyJ2IjoiMiIsInBzIjoiRXhhbXBsZSBWTWVzcyIsImFkZCI6ImV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiOTQ3OTIyODYtN2JiZS00ZjMzLThiMzYtMThkMWJiZjcwNzIzIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ3cyIsInR5cGUiOiJub25lIiwiaG9zdCI6ImV4YW1wbGUuY29tIiwicGF0aCI6Ii93cyIsInRscyI6InRscyIsInNuaSI6ImV4YW1wbGUuY29tIn0=\"","status":"passed","title":"returns valid=true for \"vmess://eyJ2IjoiMiIsInBzIjoiRXhhbXBsZSBWTWVzcyIsImFkZCI6ImV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiOTQ3OTIyODYtN2JiZS00ZjMzLThiMzYtMThkMWJiZjcwNzIzIiwiYWlkIjoiMCIsInNjeSI6ImF1dG8iLCJuZXQiOiJ3cyIsInR5cGUiOiJub25lIiwiaG9zdCI6ImV4YW1wbGUuY29tIiwicGF0aCI6Ii93cyIsInRscyI6InRscyIsInNuaSI6ImV4YW1wbGUuY29tIn0=\"","duration":0.5466999999999871,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: trojan"],"fullName":"validateProxyUrl Valid proxy URL: trojan returns valid=true for \"trojan://password@example.com:443\"","status":"passed","title":"returns valid=true for \"trojan://password@example.com:443\"","duration":0.36779999999998836,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: socks4"],"fullName":"validateProxyUrl Valid proxy URL: socks4 returns valid=true for \"socks4://127.0.0.1:1080\"","status":"passed","title":"returns valid=true for \"socks4://127.0.0.1:1080\"","duration":0.47750000000002046,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: socks4a"],"fullName":"validateProxyUrl Valid proxy URL: socks4a returns valid=true for \"socks4a://example.com:1080\"","status":"passed","title":"returns valid=true for \"socks4a://example.com:1080\"","duration":0.21399999999999864,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: socks5"],"fullName":"validateProxyUrl Valid proxy URL: socks5 returns valid=true for \"socks5://user:pass@example.com:1080\"","status":"passed","title":"returns valid=true for \"socks5://user:pass@example.com:1080\"","duration":0.262800000000027,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: http"],"fullName":"validateProxyUrl Valid proxy URL: http returns valid=true for \"http://127.0.0.1:80\"","status":"passed","title":"returns valid=true for \"http://127.0.0.1:80\"","duration":0.39620000000002165,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: https"],"fullName":"validateProxyUrl Valid proxy URL: https returns valid=true for \"https://user:pass@example.com:443\"","status":"passed","title":"returns valid=true for \"https://user:pass@example.com:443\"","duration":0.7907000000000153,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: hysteria2"],"fullName":"validateProxyUrl Valid proxy URL: hysteria2 returns valid=true for \"hysteria2://password@example.com:443\"","status":"passed","title":"returns valid=true for \"hysteria2://password@example.com:443\"","duration":0.8089999999999691,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl","Valid proxy URL: hy2"],"fullName":"validateProxyUrl Valid proxy URL: hy2 returns valid=true for \"hy2://password@example.com:443\"","status":"passed","title":"returns valid=true for \"hy2://password@example.com:443\"","duration":0.22179999999997335,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl"],"fullName":"validateProxyUrl trims surrounding whitespace before dispatching","status":"passed","title":"trims surrounding whitespace before dispatching","duration":0.16779999999999973,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl"],"fullName":"validateProxyUrl returns a clear error for unsupported protocols","status":"passed","title":"returns a clear error for unsupported protocols","duration":0.37999999999999545,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl"],"fullName":"validateProxyUrl accepts native HTTP and HTTPS proxy URLs","status":"passed","title":"accepts native HTTP and HTTPS proxy URLs","duration":0.28000000000002956,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateProxyUrl"],"fullName":"validateProxyUrl rejects VMess configs with an invalid server host","status":"passed","title":"rejects VMess configs with an invalid server host","duration":0.2522000000000162,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886668,"endTime":1789128886679.2522,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateProxyUrl.test.js"},{"assertionResults":[{"ancestorTitles":["validateShadowsocksUrl","Valid URL: no-client"],"fullName":"validateShadowsocksUrl Valid URL: no-client returns valid=true for \"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client\"","status":"passed","title":"returns valid=true for \"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client\"","duration":2.991199999999992,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Valid URL: client"],"fullName":"validateShadowsocksUrl Valid URL: client returns valid=true for \"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206S3FiWXZiNkhwb1RmTUt0N2VGcUZQSmJNNXBXaHlFU0ZKTXY2dEp1Ym1Fdz06dzRNMEx5RU9OTGQ5SWlkSGc0endTbzN2R3h4NS9aQ3hId0FpaWlxck5hcz0@127.0.0.1:26627?type=tcp#shadowsocks-client\"","status":"passed","title":"returns valid=true for \"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206S3FiWXZiNkhwb1RmTUt0N2VGcUZQSmJNNXBXaHlFU0ZKTXY2dEp1Ym1Fdz06dzRNMEx5RU9OTGQ5SWlkSGc0endTbzN2R3h4NS9aQ3hId0FpaWlxck5hcz0@127.0.0.1:26627?type=tcp#shadowsocks-client\"","duration":0.32320000000001414,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Valid URL: plain-user"],"fullName":"validateShadowsocksUrl Valid URL: plain-user returns valid=true for \"ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@127.0.0.1:27214?type=tcp#shadowsocks-plain-user\"","status":"passed","title":"returns valid=true for \"ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@127.0.0.1:27214?type=tcp#shadowsocks-plain-user\"","duration":0.3088999999999942,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Valid URL: plain-user-ipv6"],"fullName":"validateShadowsocksUrl Valid URL: plain-user-ipv6 returns valid=true for \"ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@[2001:db8::1]:27214?type=tcp#shadowsocks-ipv6\"","status":"passed","title":"returns valid=true for \"ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@[2001:db8::1]:27214?type=tcp#shadowsocks-ipv6\"","duration":0.3545999999999765,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Invalid URL: No prefix"],"fullName":"validateShadowsocksUrl Invalid URL: No prefix returns valid=false for \"uuid@127.0.0.1:443?type=tcp\"","status":"passed","title":"returns valid=false for \"uuid@127.0.0.1:443?type=tcp\"","duration":0.29719999999997526,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Invalid URL: No host"],"fullName":"validateShadowsocksUrl Invalid URL: No host returns valid=false for \"ss://password@:443?type=tcp\"","status":"passed","title":"returns valid=false for \"ss://password@:443?type=tcp\"","duration":0.1863999999999919,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Invalid URL: No port"],"fullName":"validateShadowsocksUrl Invalid URL: No port returns valid=false for \"ss://password@127.0.0.1?type=tcp\"","status":"passed","title":"returns valid=false for \"ss://password@127.0.0.1?type=tcp\"","duration":0.2565999999999917,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Invalid URL: Invalid host"],"fullName":"validateShadowsocksUrl Invalid URL: Invalid host returns valid=false for \"ss://password@bad_host:443?type=tcp\"","status":"passed","title":"returns valid=false for \"ss://password@bad_host:443?type=tcp\"","duration":0.22339999999999804,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Invalid URL: Invalid port"],"fullName":"validateShadowsocksUrl Invalid URL: Invalid port returns valid=false for \"ss://password@127.0.0.1:abc?type=tcp\"","status":"passed","title":"returns valid=false for \"ss://password@127.0.0.1:abc?type=tcp\"","duration":0.7922000000000082,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Invalid URL: Missing type"],"fullName":"validateShadowsocksUrl Invalid URL: Missing type returns valid=false for \"ss://password@127.0.0.1:443\"","status":"passed","title":"returns valid=false for \"ss://password@127.0.0.1:443\"","duration":0.2948000000000093,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Invalid URL: Contains space"],"fullName":"validateShadowsocksUrl Invalid URL: Contains space returns valid=false for \"ss://password@127.0.0.1:443?type=tcp #extra\"","status":"passed","title":"returns valid=false for \"ss://password@127.0.0.1:443?type=tcp #extra\"","duration":0.25079999999999814,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl","Invalid URL: Unbracketed IPv6 with port"],"fullName":"validateShadowsocksUrl Invalid URL: Unbracketed IPv6 with port returns valid=false for \"ss://password@2001:db8::1:443?type=tcp\"","status":"passed","title":"returns valid=false for \"ss://password@2001:db8::1:443?type=tcp\"","duration":0.16059999999998809,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateShadowsocksUrl"],"fullName":"validateShadowsocksUrl detects invalid port range","status":"passed","title":"detects invalid port range","duration":0.1897000000000162,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886916,"endTime":1789128886924.1897,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateShadowsocksUrl.test.js"},{"assertionResults":[{"ancestorTitles":["validateSocksUrl","Valid URL: socks4 basic"],"fullName":"validateSocksUrl Valid URL: socks4 basic returns valid=true for \"socks4://127.0.0.1:1080\"","status":"passed","title":"returns valid=true for \"socks4://127.0.0.1:1080\"","duration":2.713300000000004,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Valid URL: socks4a basic"],"fullName":"validateSocksUrl Valid URL: socks4a basic returns valid=true for \"socks4a://127.0.0.1:1080\"","status":"passed","title":"returns valid=true for \"socks4a://127.0.0.1:1080\"","duration":0.28820000000001755,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Valid URL: socks5 basic"],"fullName":"validateSocksUrl Valid URL: socks5 basic returns valid=true for \"socks5://127.0.0.1:1080\"","status":"passed","title":"returns valid=true for \"socks5://127.0.0.1:1080\"","duration":0.23519999999999186,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Valid URL: socks5 with username"],"fullName":"validateSocksUrl Valid URL: socks5 with username returns valid=true for \"socks5://user@127.0.0.1:1080\"","status":"passed","title":"returns valid=true for \"socks5://user@127.0.0.1:1080\"","duration":0.17379999999999995,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Valid URL: socks5 with username/password"],"fullName":"validateSocksUrl Valid URL: socks5 with username/password returns valid=true for \"socks5://user:pass@127.0.0.1:1080\"","status":"passed","title":"returns valid=true for \"socks5://user:pass@127.0.0.1:1080\"","duration":0.2243999999999744,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Valid URL: socks5 with domain"],"fullName":"validateSocksUrl Valid URL: socks5 with domain returns valid=true for \"socks5://user:pass@my.proxy.com:1080\"","status":"passed","title":"returns valid=true for \"socks5://user:pass@my.proxy.com:1080\"","duration":0.4865000000000066,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Valid URL: socks5 with dash in domain"],"fullName":"validateSocksUrl Valid URL: socks5 with dash in domain returns valid=true for \"socks5://user:pass@fast-proxy.net:8080\"","status":"passed","title":"returns valid=true for \"socks5://user:pass@fast-proxy.net:8080\"","duration":0.2775000000000034,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Valid URL: socks5 with uppercase domain"],"fullName":"validateSocksUrl Valid URL: socks5 with uppercase domain returns valid=true for \"socks5://USER:PASSWORD@Example.COM:1080\"","status":"passed","title":"returns valid=true for \"socks5://USER:PASSWORD@Example.COM:1080\"","duration":0.17980000000000018,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Valid URL: socks5 with ipv6"],"fullName":"validateSocksUrl Valid URL: socks5 with ipv6 returns valid=true for \"socks5://user:pass@[2001:db8::1]:1080\"","status":"passed","title":"returns valid=true for \"socks5://user:pass@[2001:db8::1]:1080\"","duration":0.7550000000000239,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: no prefix"],"fullName":"validateSocksUrl Invalid URL: no prefix returns valid=false for \"127.0.0.1:1080\"","status":"passed","title":"returns valid=false for \"127.0.0.1:1080\"","duration":0.2682000000000073,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: wrong prefix"],"fullName":"validateSocksUrl Invalid URL: wrong prefix returns valid=false for \"http://127.0.0.1:1080\"","status":"passed","title":"returns valid=false for \"http://127.0.0.1:1080\"","duration":0.20250000000001478,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: missing host"],"fullName":"validateSocksUrl Invalid URL: missing host returns valid=false for \"socks5://user:pass@:1080\"","status":"passed","title":"returns valid=false for \"socks5://user:pass@:1080\"","duration":0.1272999999999911,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: missing port"],"fullName":"validateSocksUrl Invalid URL: missing port returns valid=false for \"socks5://127.0.0.1\"","status":"passed","title":"returns valid=false for \"socks5://127.0.0.1\"","duration":0.08830000000000382,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: invalid port (non-numeric)"],"fullName":"validateSocksUrl Invalid URL: invalid port (non-numeric) returns valid=false for \"socks5://127.0.0.1:abc\"","status":"passed","title":"returns valid=false for \"socks5://127.0.0.1:abc\"","duration":0.10369999999997503,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: invalid port (too high)"],"fullName":"validateSocksUrl Invalid URL: invalid port (too high) returns valid=false for \"socks5://127.0.0.1:99999\"","status":"passed","title":"returns valid=false for \"socks5://127.0.0.1:99999\"","duration":0.07749999999998636,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: space in url"],"fullName":"validateSocksUrl Invalid URL: space in url returns valid=false for \"socks5://127.0. 0.1:1080\"","status":"passed","title":"returns valid=false for \"socks5://127.0. 0.1:1080\"","duration":0.07019999999999982,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: missing username when auth provided"],"fullName":"validateSocksUrl Invalid URL: missing username when auth provided returns valid=false for \"socks5://:pass@127.0.0.1:1080\"","status":"passed","title":"returns valid=false for \"socks5://:pass@127.0.0.1:1080\"","duration":0.07070000000001642,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: invalid domain chars"],"fullName":"validateSocksUrl Invalid URL: invalid domain chars returns valid=false for \"socks5://user:pass@exa_mple.com:1080\"","status":"passed","title":"returns valid=false for \"socks5://user:pass@exa_mple.com:1080\"","duration":0.17449999999999477,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: extra symbol"],"fullName":"validateSocksUrl Invalid URL: extra symbol returns valid=false for \"socks5:///127.0.0.1:1080\"","status":"passed","title":"returns valid=false for \"socks5:///127.0.0.1:1080\"","duration":0.13329999999999131,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl","Invalid URL: unbracketed ipv6 with port"],"fullName":"validateSocksUrl Invalid URL: unbracketed ipv6 with port returns valid=false for \"socks5://2001:db8::1:1080\"","status":"passed","title":"returns valid=false for \"socks5://2001:db8::1:1080\"","duration":0.08279999999999177,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl"],"fullName":"validateSocksUrl detects invalid port range (0)","status":"passed","title":"detects invalid port range (0)","duration":0.11639999999999873,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSocksUrl"],"fullName":"validateSocksUrl detects invalid port range (65536)","status":"passed","title":"detects invalid port range (65536)","duration":0.11879999999999313,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886832,"endTime":1789128886840.1746,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateSocksUrl.test.js"},{"assertionResults":[{"ancestorTitles":["validateSubnet","Valid subnet: Simple IP"],"fullName":"validateSubnet Valid subnet: Simple IP returns {valid:true} for \"192.168.1.1\"","status":"passed","title":"returns {valid:true} for \"192.168.1.1\"","duration":2.8680999999999983,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: With CIDR /24"],"fullName":"validateSubnet Valid subnet: With CIDR /24 returns {valid:true} for \"192.168.1.1/24\"","status":"passed","title":"returns {valid:true} for \"192.168.1.1/24\"","duration":0.474899999999991,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: CIDR /0"],"fullName":"validateSubnet Valid subnet: CIDR /0 returns {valid:true} for \"10.0.0.1/0\"","status":"passed","title":"returns {valid:true} for \"10.0.0.1/0\"","duration":0.27889999999999304,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: Zero network with CIDR"],"fullName":"validateSubnet Valid subnet: Zero network with CIDR returns {valid:true} for \"0.0.0.0/3\"","status":"passed","title":"returns {valid:true} for \"0.0.0.0/3\"","duration":0.23680000000001655,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: CIDR /32"],"fullName":"validateSubnet Valid subnet: CIDR /32 returns {valid:true} for \"172.16.0.1/32\"","status":"passed","title":"returns {valid:true} for \"172.16.0.1/32\"","duration":0.316599999999994,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: Loopback"],"fullName":"validateSubnet Valid subnet: Loopback returns {valid:true} for \"127.0.0.1\"","status":"passed","title":"returns {valid:true} for \"127.0.0.1\"","duration":0.1828999999999894,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: Broadcast with mask"],"fullName":"validateSubnet Valid subnet: Broadcast with mask returns {valid:true} for \"255.255.255.255/32\"","status":"passed","title":"returns {valid:true} for \"255.255.255.255/32\"","duration":0.26149999999998386,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: IPv6 address"],"fullName":"validateSubnet Valid subnet: IPv6 address returns {valid:true} for \"2001:db8::1\"","status":"passed","title":"returns {valid:true} for \"2001:db8::1\"","duration":0.19880000000000564,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: IPv6 CIDR /64"],"fullName":"validateSubnet Valid subnet: IPv6 CIDR /64 returns {valid:true} for \"2001:db8::/64\"","status":"passed","title":"returns {valid:true} for \"2001:db8::/64\"","duration":0.8192999999999984,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: IPv6 default route"],"fullName":"validateSubnet Valid subnet: IPv6 default route returns {valid:true} for \"::/0\"","status":"passed","title":"returns {valid:true} for \"::/0\"","duration":0.28770000000000095,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Valid subnet: IPv6 host /128"],"fullName":"validateSubnet Valid subnet: IPv6 host /128 returns {valid:true} for \"fc00::1/128\"","status":"passed","title":"returns {valid:true} for \"fc00::1/128\"","duration":0.23550000000000182,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Empty string"],"fullName":"validateSubnet Invalid subnet: Empty string returns {valid:false} for \"\"","status":"passed","title":"returns {valid:false} for \"\"","duration":0.141399999999976,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Bad format letters"],"fullName":"validateSubnet Invalid subnet: Bad format letters returns {valid:false} for \"abc.def.ghi.jkl\"","status":"passed","title":"returns {valid:false} for \"abc.def.ghi.jkl\"","duration":0.08689999999998577,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Octet too large"],"fullName":"validateSubnet Invalid subnet: Octet too large returns {valid:false} for \"300.1.1.1\"","status":"passed","title":"returns {valid:false} for \"300.1.1.1\"","duration":0.08020000000001914,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Negative octet"],"fullName":"validateSubnet Invalid subnet: Negative octet returns {valid:false} for \"-1.2.3.4\"","status":"passed","title":"returns {valid:false} for \"-1.2.3.4\"","duration":0.08190000000001874,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Too many octets"],"fullName":"validateSubnet Invalid subnet: Too many octets returns {valid:false} for \"1.2.3.4.5\"","status":"passed","title":"returns {valid:false} for \"1.2.3.4.5\"","duration":0.06929999999999836,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Not enough octets"],"fullName":"validateSubnet Invalid subnet: Not enough octets returns {valid:false} for \"192.168.1\"","status":"passed","title":"returns {valid:false} for \"192.168.1\"","duration":0.06520000000000437,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Leading zero octet"],"fullName":"validateSubnet Invalid subnet: Leading zero octet returns {valid:false} for \"01.2.3.4\"","status":"passed","title":"returns {valid:false} for \"01.2.3.4\"","duration":0.09659999999999513,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Invalid CIDR (too high)"],"fullName":"validateSubnet Invalid subnet: Invalid CIDR (too high) returns {valid:false} for \"192.168.1.1/33\"","status":"passed","title":"returns {valid:false} for \"192.168.1.1/33\"","duration":0.0800000000000125,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Invalid CIDR (negative)"],"fullName":"validateSubnet Invalid subnet: Invalid CIDR (negative) returns {valid:false} for \"192.168.1.1/-1\"","status":"passed","title":"returns {valid:false} for \"192.168.1.1/-1\"","duration":0.07230000000001269,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: CIDR not number"],"fullName":"validateSubnet Invalid subnet: CIDR not number returns {valid:false} for \"192.168.1.1/abc\"","status":"passed","title":"returns {valid:false} for \"192.168.1.1/abc\"","duration":0.07549999999997681,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Forbidden 0.0.0.0"],"fullName":"validateSubnet Invalid subnet: Forbidden 0.0.0.0 returns {valid:false} for \"0.0.0.0\"","status":"passed","title":"returns {valid:false} for \"0.0.0.0\"","duration":0.06750000000002387,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Forbidden ::"],"fullName":"validateSubnet Invalid subnet: Forbidden :: returns {valid:false} for \"::\"","status":"passed","title":"returns {valid:false} for \"::\"","duration":0.08500000000000796,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Invalid IPv6 CIDR"],"fullName":"validateSubnet Invalid subnet: Invalid IPv6 CIDR returns {valid:false} for \"2001:db8::1/129\"","status":"passed","title":"returns {valid:false} for \"2001:db8::1/129\"","duration":0.08080000000001064,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateSubnet","Invalid subnet: Invalid IPv6 address"],"fullName":"validateSubnet Invalid subnet: Invalid IPv6 address returns {valid:false} for \"2001:db8::zzzz/64\"","status":"passed","title":"returns {valid:false} for \"2001:db8::zzzz/64\"","duration":0.11400000000000432,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886665,"endTime":1789128886674.114,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateSubnet.test.js"},{"assertionResults":[{"ancestorTitles":["validateTrojanUrl","Valid URL: tcp + none"],"fullName":"validateTrojanUrl Valid URL: tcp + none returns valid=true for \"trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none\"","status":"passed","title":"returns valid=true for \"trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none\"","duration":2.8991000000000042,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: tcp + reality"],"fullName":"validateTrojanUrl Valid URL: tcp + reality returns valid=true for \"trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni=google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality\"","status":"passed","title":"returns valid=true for \"trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni=google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality\"","duration":0.29640000000000555,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: tcp + tls"],"fullName":"validateTrojanUrl Valid URL: tcp + tls returns valid=true for \"trojan://EJjpAj02lg@127.0.0.1:11381?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-tcp-tls\"","status":"passed","title":"returns valid=true for \"trojan://EJjpAj02lg@127.0.0.1:11381?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-tcp-tls\"","duration":0.26739999999998076,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: tcp + tls + insecure"],"fullName":"validateTrojanUrl Valid URL: tcp + tls + insecure returns valid=true for \"trojan://ZP2Ik5sxN3@127.0.0.1:16247?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-tcp-tls-insecure\"","status":"passed","title":"returns valid=true for \"trojan://ZP2Ik5sxN3@127.0.0.1:16247?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-tcp-tls-insecure\"","duration":0.1728999999999985,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: tcp + tls + ech"],"fullName":"validateTrojanUrl Valid URL: tcp + tls + ech returns valid=true for \"trojan://90caP481ay@127.0.0.1:59708?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACC2y%2BAe4dqthLNpfvmtE6g%2BnaJ%2FciK6P%2BREbRLkR%2Fg%2FEgAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-tcp-tls-ech\"","status":"passed","title":"returns valid=true for \"trojan://90caP481ay@127.0.0.1:59708?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACC2y%2BAe4dqthLNpfvmtE6g%2BnaJ%2FciK6P%2BREbRLkR%2Fg%2FEgAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-tcp-tls-ech\"","duration":0.20180000000001996,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: mKCP + none"],"fullName":"validateTrojanUrl Valid URL: mKCP + none returns valid=true for \"trojan://N5v7iIOe9G@127.0.0.1:36319?type=kcp&headerType=none&seed=P91wFIfjzZ&security=none#trojan-mKCP\"","status":"passed","title":"returns valid=true for \"trojan://N5v7iIOe9G@127.0.0.1:36319?type=kcp&headerType=none&seed=P91wFIfjzZ&security=none#trojan-mKCP\"","duration":0.1272000000000162,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: ws + none"],"fullName":"validateTrojanUrl Valid URL: ws + none returns valid=true for \"trojan://G3cE9phv1g@127.0.0.1:57370?type=ws&path=%2Fwspath&host=google.com&security=none#trojan-websocket-none\"","status":"passed","title":"returns valid=true for \"trojan://G3cE9phv1g@127.0.0.1:57370?type=ws&path=%2Fwspath&host=google.com&security=none#trojan-websocket-none\"","duration":0.18059999999999832,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: ws + tls"],"fullName":"validateTrojanUrl Valid URL: ws + tls returns valid=true for \"trojan://FBok41WczO@127.0.0.1:59919?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-websocket-tls\"","status":"passed","title":"returns valid=true for \"trojan://FBok41WczO@127.0.0.1:59919?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-websocket-tls\"","duration":0.152499999999975,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: ws + tls + insecure"],"fullName":"validateTrojanUrl Valid URL: ws + tls + insecure returns valid=true for \"trojan://bhwvndUBPA@127.0.0.1:22969?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-websocket-tls-insecure\"","status":"passed","title":"returns valid=true for \"trojan://bhwvndUBPA@127.0.0.1:22969?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-websocket-tls-insecure\"","duration":0.6825000000000045,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: ws + tls + ech"],"fullName":"validateTrojanUrl Valid URL: ws + tls + ech returns valid=true for \"trojan://pwiduqFUWO@127.0.0.1:46765?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCFcQYEtwrFOidJJLYHvSiN%2BljRgaAIrNHoVnio3uXAOwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-websocket-tls-ech\"","status":"passed","title":"returns valid=true for \"trojan://pwiduqFUWO@127.0.0.1:46765?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCFcQYEtwrFOidJJLYHvSiN%2BljRgaAIrNHoVnio3uXAOwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-websocket-tls-ech\"","duration":0.23570000000000846,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: grpc + none"],"fullName":"validateTrojanUrl Valid URL: grpc + none returns valid=true for \"trojan://WMR7qkKhsV@127.0.0.1:27897?type=grpc&serviceName=TunService&authority=authority&security=none#trojan-gRPC-none\"","status":"passed","title":"returns valid=true for \"trojan://WMR7qkKhsV@127.0.0.1:27897?type=grpc&serviceName=TunService&authority=authority&security=none#trojan-gRPC-none\"","duration":0.1904999999999859,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: grpc + reality"],"fullName":"validateTrojanUrl Valid URL: grpc + reality returns valid=true for \"trojan://KVuRNsu6KG@127.0.0.1:46077?type=grpc&serviceName=TunService&authority=authority&security=reality&pbk=Xn59i4gum3ppCICS6-_NuywrhHIVVAH54b2mjd5CFkE&fp=chrome&sni=google.com&sid=e5be&spx=%2F#trojan-gRPC-reality\"","status":"passed","title":"returns valid=true for \"trojan://KVuRNsu6KG@127.0.0.1:46077?type=grpc&serviceName=TunService&authority=authority&security=reality&pbk=Xn59i4gum3ppCICS6-_NuywrhHIVVAH54b2mjd5CFkE&fp=chrome&sni=google.com&sid=e5be&spx=%2F#trojan-gRPC-reality\"","duration":0.08680000000001087,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: grpc + tls"],"fullName":"validateTrojanUrl Valid URL: grpc + tls returns valid=true for \"trojan://7BJtbywy8h@127.0.0.1:10627?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-gRPC-tls\"","status":"passed","title":"returns valid=true for \"trojan://7BJtbywy8h@127.0.0.1:10627?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-gRPC-tls\"","duration":0.07220000000000937,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: grpc + tls + insecure"],"fullName":"validateTrojanUrl Valid URL: grpc + tls + insecure returns valid=true for \"trojan://TI3PakvtP4@127.0.0.1:10435?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-gRPC-tls-insecure\"","status":"passed","title":"returns valid=true for \"trojan://TI3PakvtP4@127.0.0.1:10435?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-gRPC-tls-insecure\"","duration":0.06690000000000396,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: grpc + tls + ech"],"fullName":"validateTrojanUrl Valid URL: grpc + tls + ech returns valid=true for \"trojan://mbzoVKL27h@127.0.0.1:38681?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCq72Ru3VbFlDpKttl3LccmInu8R2oAsCr8wzyxB0vZZQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-gRPC-tls-ech\"","status":"passed","title":"returns valid=true for \"trojan://mbzoVKL27h@127.0.0.1:38681?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCq72Ru3VbFlDpKttl3LccmInu8R2oAsCr8wzyxB0vZZQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-gRPC-tls-ech\"","duration":0.06430000000000291,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: httpupgrade + none"],"fullName":"validateTrojanUrl Valid URL: httpupgrade + none returns valid=true for \"trojan://uc44gBwOKQ@127.0.0.1:29085?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=none#trojan-httpupgrade-none\"","status":"passed","title":"returns valid=true for \"trojan://uc44gBwOKQ@127.0.0.1:29085?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=none#trojan-httpupgrade-none\"","duration":0.06560000000001764,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: httpupgrade + tls"],"fullName":"validateTrojanUrl Valid URL: httpupgrade + tls returns valid=true for \"trojan://MhNxbcVB14@127.0.0.1:32700?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-httpupgrade-tls\"","status":"passed","title":"returns valid=true for \"trojan://MhNxbcVB14@127.0.0.1:32700?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-httpupgrade-tls\"","duration":0.0660000000000025,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: httpupgrade + tls + insecure"],"fullName":"validateTrojanUrl Valid URL: httpupgrade + tls + insecure returns valid=true for \"trojan://7SOQFUpLob@127.0.0.1:28474?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-httpupgrade-tls-insecure\"","status":"passed","title":"returns valid=true for \"trojan://7SOQFUpLob@127.0.0.1:28474?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-httpupgrade-tls-insecure\"","duration":0.06310000000001992,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: httpupgrade + tls + ech"],"fullName":"validateTrojanUrl Valid URL: httpupgrade + tls + ech returns valid=true for \"trojan://ou8pLSyx9N@127.0.0.1:17737?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACB%2FlkIkit%2BblFzE7PtbYDVF3NXK8olXJ5a7YwY%2Biy9QQwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-httpupgrade-tls-ech\"","status":"passed","title":"returns valid=true for \"trojan://ou8pLSyx9N@127.0.0.1:17737?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACB%2FlkIkit%2BblFzE7PtbYDVF3NXK8olXJ5a7YwY%2Biy9QQwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-httpupgrade-tls-ech\"","duration":0.06299999999998818,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: xhttp + none"],"fullName":"validateTrojanUrl Valid URL: xhttp + none returns valid=true for \"trojan://VEetltxLtw@127.0.0.1:59072?type=xhttp&path=%2Fxhttppath&host=google.com&mode=auto&security=none#trojan-xhttp\"","status":"passed","title":"returns valid=true for \"trojan://VEetltxLtw@127.0.0.1:59072?type=xhttp&path=%2Fxhttppath&host=google.com&mode=auto&security=none#trojan-xhttp\"","duration":0.06279999999998154,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Valid URL: tcp + ipv6"],"fullName":"validateTrojanUrl Valid URL: tcp + ipv6 returns valid=true for \"trojan://04agAQapcl@[2001:db8::1]:33641?type=tcp&security=none#trojan-ipv6\"","status":"passed","title":"returns valid=true for \"trojan://04agAQapcl@[2001:db8::1]:33641?type=tcp&security=none#trojan-ipv6\"","duration":0.14430000000001542,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Invalid URL: No prefix"],"fullName":"validateTrojanUrl Invalid URL: No prefix returns valid=false for \"uuid@host:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"uuid@host:443?type=tcp&security=tls\"","duration":0.11019999999999186,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Invalid URL: No password"],"fullName":"validateTrojanUrl Invalid URL: No password returns valid=false for \"trojan://@127.0.0.1:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"trojan://@127.0.0.1:443?type=tcp&security=tls\"","duration":0.10349999999999682,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Invalid URL: No host"],"fullName":"validateTrojanUrl Invalid URL: No host returns valid=false for \"trojan://pass@:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"trojan://pass@:443?type=tcp&security=tls\"","duration":0.2831000000000188,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Invalid URL: No port"],"fullName":"validateTrojanUrl Invalid URL: No port returns valid=false for \"trojan://pass@127.0.0.1?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"trojan://pass@127.0.0.1?type=tcp&security=tls\"","duration":0.10609999999999786,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Invalid URL: Invalid host"],"fullName":"validateTrojanUrl Invalid URL: Invalid host returns valid=false for \"trojan://pass@bad_host:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"trojan://pass@bad_host:443?type=tcp&security=tls\"","duration":0.3002999999999929,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Invalid URL: Invalid port"],"fullName":"validateTrojanUrl Invalid URL: Invalid port returns valid=false for \"trojan://pass@127.0.0.1:abc?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"trojan://pass@127.0.0.1:abc?type=tcp&security=tls\"","duration":0.09099999999997976,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Invalid URL: Unbracketed IPv6 with port"],"fullName":"validateTrojanUrl Invalid URL: Unbracketed IPv6 with port returns valid=false for \"trojan://pass@2001:db8::1:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"trojan://pass@2001:db8::1:443?type=tcp&security=tls\"","duration":0.08789999999999054,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl","Invalid URL: tcp + reality + unexpected spaces"],"fullName":"validateTrojanUrl Invalid URL: tcp + reality + unexpected spaces returns valid=false for \"trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni= google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality\"","status":"passed","title":"returns valid=false for \"trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni= google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality\"","duration":0.08429999999998472,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateTrojanUrl"],"fullName":"validateTrojanUrl detects invalid port range","status":"passed","title":"detects invalid port range","duration":0.13880000000000337,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886542,"endTime":1789128886551.139,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateTrojanUrl.test.js"},{"assertionResults":[{"ancestorTitles":["validateUrl","Valid URL: Simple HTTP"],"fullName":"validateUrl Valid URL: Simple HTTP returns valid=true for \"http://example.com\"","status":"passed","title":"returns valid=true for \"http://example.com\"","duration":3.0615999999999985,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: Simple HTTPS"],"fullName":"validateUrl Valid URL: Simple HTTPS returns valid=true for \"https://example.com\"","status":"passed","title":"returns valid=true for \"https://example.com\"","duration":0.3527999999999736,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: With path"],"fullName":"validateUrl Valid URL: With path returns valid=true for \"https://example.com/path/to/page\"","status":"passed","title":"returns valid=true for \"https://example.com/path/to/page\"","duration":0.28890000000001237,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: With query"],"fullName":"validateUrl Valid URL: With query returns valid=true for \"https://example.com/?q=test\"","status":"passed","title":"returns valid=true for \"https://example.com/?q=test\"","duration":0.25370000000000914,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: With port"],"fullName":"validateUrl Valid URL: With port returns valid=true for \"http://example.com:8080\"","status":"passed","title":"returns valid=true for \"http://example.com:8080\"","duration":0.32390000000000896,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: With subdomain"],"fullName":"validateUrl Valid URL: With subdomain returns valid=true for \"https://sub.example.com\"","status":"passed","title":"returns valid=true for \"https://sub.example.com\"","duration":0.1897999999999911,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: IPv4 host"],"fullName":"validateUrl Valid URL: IPv4 host returns valid=true for \"http://127.0.0.1/tachyon-subscription-test.json\"","status":"passed","title":"returns valid=true for \"http://127.0.0.1/tachyon-subscription-test.json\"","duration":0.23580000000001178,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: IPv4 host with port"],"fullName":"validateUrl Valid URL: IPv4 host with port returns valid=true for \"http://192.168.1.1:8080/sub\"","status":"passed","title":"returns valid=true for \"http://192.168.1.1:8080/sub\"","duration":0.21000000000000796,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: IPv6 host"],"fullName":"validateUrl Valid URL: IPv6 host returns valid=true for \"https://[2001:db8::1]/sub\"","status":"passed","title":"returns valid=true for \"https://[2001:db8::1]/sub\"","duration":0.7331999999999823,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: IPv6 host with port"],"fullName":"validateUrl Valid URL: IPv6 host with port returns valid=true for \"https://[2001:db8::1]:8443/sub\"","status":"passed","title":"returns valid=true for \"https://[2001:db8::1]:8443/sub\"","duration":0.33960000000001855,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Valid URL: localhost host"],"fullName":"validateUrl Valid URL: localhost host returns valid=true for \"http://localhost/sub\"","status":"passed","title":"returns valid=true for \"http://localhost/sub\"","duration":0.23519999999999186,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Invalid URL: Invalid format"],"fullName":"validateUrl Invalid URL: Invalid format returns valid=false for \"not a url\"","status":"passed","title":"returns valid=false for \"not a url\"","duration":0.1636999999999773,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Invalid URL: Missing protocol"],"fullName":"validateUrl Invalid URL: Missing protocol returns valid=false for \"example.com\"","status":"passed","title":"returns valid=false for \"example.com\"","duration":0.09270000000000778,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Invalid URL: Unsupported protocol (ftp)"],"fullName":"validateUrl Invalid URL: Unsupported protocol (ftp) returns valid=false for \"ftp://example.com\"","status":"passed","title":"returns valid=false for \"ftp://example.com\"","duration":0.08759999999998058,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Invalid URL: Unsupported protocol (ws)"],"fullName":"validateUrl Invalid URL: Unsupported protocol (ws) returns valid=false for \"ws://example.com\"","status":"passed","title":"returns valid=false for \"ws://example.com\"","duration":0.07119999999997617,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Invalid URL: Empty string"],"fullName":"validateUrl Invalid URL: Empty string returns valid=false for \"\"","status":"passed","title":"returns valid=false for \"\"","duration":0.09239999999999782,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Invalid URL: Without tld"],"fullName":"validateUrl Invalid URL: Without tld returns valid=false for \"https://google\"","status":"passed","title":"returns valid=false for \"https://google\"","duration":0.10050000000001091,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Invalid URL: Invalid IPv4 host"],"fullName":"validateUrl Invalid URL: Invalid IPv4 host returns valid=false for \"http://999.0.0.1/sub\"","status":"passed","title":"returns valid=false for \"http://999.0.0.1/sub\"","duration":0.126199999999983,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl","Invalid URL: Unbracketed IPv6 host with port"],"fullName":"validateUrl Invalid URL: Unbracketed IPv6 host with port returns valid=false for \"http://2001:db8::1:8080/sub\"","status":"passed","title":"returns valid=false for \"http://2001:db8::1:8080/sub\"","duration":0.09569999999999368,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateUrl"],"fullName":"validateUrl allows custom protocol list (ftp)","status":"passed","title":"allows custom protocol list (ftp)","duration":0.9034999999999798,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886977,"endTime":1789128886986.9036,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateUrl.test.js"},{"assertionResults":[{"ancestorTitles":["validateVlessUrl","Valid URL: tcp + none"],"fullName":"validateVlessUrl Valid URL: tcp + none returns valid=true for \"vless://94792286-7bbe-4f33-8b36-18d1bbf70723@127.0.0.1:34520?type=tcp&encryption=none&security=none#vless-tcp-none\"","status":"passed","title":"returns valid=true for \"vless://94792286-7bbe-4f33-8b36-18d1bbf70723@127.0.0.1:34520?type=tcp&encryption=none&security=none#vless-tcp-none\"","duration":3.2151000000000067,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: tcp + reality"],"fullName":"validateVlessUrl Valid URL: tcp + reality returns valid=true for \"vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni=google.com&sid=f6&spx=%2F#vless-tcp-reality\"","status":"passed","title":"returns valid=true for \"vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni=google.com&sid=f6&spx=%2F#vless-tcp-reality\"","duration":0.3947000000000003,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: tcp implied + Amnezia reality"],"fullName":"validateVlessUrl Valid URL: tcp implied + Amnezia reality returns valid=true for \"vless://2b173dc1-e7e7-4824-aadc-d072dcefeaca@127.0.0.1:443?encryption=none&security=reality&flow=xtls-rprx-vision&sni=www.googletagmanager.com&fp=chrome&pbk=R3BKE4AvPMYy_JPKB2mB1cLEL8jX-qXR1hYE3Iarsmk&sid=92ca63541ff03a57#AmneziaVPN\"","status":"passed","title":"returns valid=true for \"vless://2b173dc1-e7e7-4824-aadc-d072dcefeaca@127.0.0.1:443?encryption=none&security=reality&flow=xtls-rprx-vision&sni=www.googletagmanager.com&fp=chrome&pbk=R3BKE4AvPMYy_JPKB2mB1cLEL8jX-qXR1hYE3Iarsmk&sid=92ca63541ff03a57#AmneziaVPN\"","duration":0.2464999999999975,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: tcp + tls"],"fullName":"validateVlessUrl Valid URL: tcp + tls returns valid=true for \"vless://2e9e8288-060e-4da2-8b9f-a1c81826feb7@127.0.0.1:19316?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-tcp-tls\"","status":"passed","title":"returns valid=true for \"vless://2e9e8288-060e-4da2-8b9f-a1c81826feb7@127.0.0.1:19316?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-tcp-tls\"","duration":0.32289999999997576,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: mKCP + none"],"fullName":"validateVlessUrl Valid URL: mKCP + none returns valid=true for \"vless://72e201d7-7841-4a32-b266-4aa3eb776d51@127.0.0.1:17270?type=kcp&encryption=none&headerType=none&seed=AirziWi4ng&security=none#vless-mKCP\"","status":"passed","title":"returns valid=true for \"vless://72e201d7-7841-4a32-b266-4aa3eb776d51@127.0.0.1:17270?type=kcp&encryption=none&headerType=none&seed=AirziWi4ng&security=none#vless-mKCP\"","duration":0.32220000000000937,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: ws + none"],"fullName":"validateVlessUrl Valid URL: ws + none returns valid=true for \"vless://d86daef7-565b-4ecd-a9ee-bac847ad38e6@127.0.0.1:12928?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=none#vless-websocket-none\"","status":"passed","title":"returns valid=true for \"vless://d86daef7-565b-4ecd-a9ee-bac847ad38e6@127.0.0.1:12928?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=none#vless-websocket-none\"","duration":0.23540000000002692,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: ws + tls"],"fullName":"validateVlessUrl Valid URL: ws + tls returns valid=true for \"vless://fe0f0941-09a9-4e46-bc69-e00190d7bb9c@127.0.0.1:10156?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&sni=google.com#vless-websocket-tls\"","status":"passed","title":"returns valid=true for \"vless://fe0f0941-09a9-4e46-bc69-e00190d7bb9c@127.0.0.1:10156?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&sni=google.com#vless-websocket-tls\"","duration":0.29549999999997567,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: grpc + none"],"fullName":"validateVlessUrl Valid URL: grpc + none returns valid=true for \"vless://974b39e3-f7bf-42b9-933c-16699c635e77@127.0.0.1:15633?type=grpc&encryption=none&serviceName=TunService&security=none#vless-gRPC-none\"","status":"passed","title":"returns valid=true for \"vless://974b39e3-f7bf-42b9-933c-16699c635e77@127.0.0.1:15633?type=grpc&encryption=none&serviceName=TunService&security=none#vless-gRPC-none\"","duration":0.2324999999999875,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: grpc + reality"],"fullName":"validateVlessUrl Valid URL: grpc + reality returns valid=true for \"vless://651e7eca-5152-46f1-baf2-d502e0af7b27@127.0.0.1:28535?type=grpc&encryption=none&serviceName=TunService&security=reality&pbk=nhZ7NiKfcqESa5ZeBFfsq9o18W-OWOAHLln9UmuVXSk&fp=chrome&sni=google.com&sid=11cbaeaa&spx=%2F#vless-gRPC-reality\"","status":"passed","title":"returns valid=true for \"vless://651e7eca-5152-46f1-baf2-d502e0af7b27@127.0.0.1:28535?type=grpc&encryption=none&serviceName=TunService&security=reality&pbk=nhZ7NiKfcqESa5ZeBFfsq9o18W-OWOAHLln9UmuVXSk&fp=chrome&sni=google.com&sid=11cbaeaa&spx=%2F#vless-gRPC-reality\"","duration":0.9517999999999915,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: httpupgrade + none"],"fullName":"validateVlessUrl Valid URL: httpupgrade + none returns valid=true for \"vless://2b98f144-847f-42f7-8798-e1a32d27bdc7@127.0.0.1:47154?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=none#vless-httpupgrade-none\"","status":"passed","title":"returns valid=true for \"vless://2b98f144-847f-42f7-8798-e1a32d27bdc7@127.0.0.1:47154?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=none#vless-httpupgrade-none\"","duration":0.3897000000000048,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: httpupgrade + tls"],"fullName":"validateVlessUrl Valid URL: httpupgrade + tls returns valid=true for \"vless://76dbd0ff-1a35-4f0c-a9ba-3c5890b7dea6@127.0.0.1:50639?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&sni=google.com#vless-httpupgrade-tls\"","status":"passed","title":"returns valid=true for \"vless://76dbd0ff-1a35-4f0c-a9ba-3c5890b7dea6@127.0.0.1:50639?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&sni=google.com#vless-httpupgrade-tls\"","duration":0.27840000000000487,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: xhttp + none"],"fullName":"validateVlessUrl Valid URL: xhttp + none returns valid=true for \"vless://c2841505-ec32-4b8d-b6dd-3e19d648c321@127.0.0.1:45507?type=xhttp&encryption=none&path=%2Fxhttppath&host=xhttp&mode=auto&security=none#vless-xhttp\"","status":"passed","title":"returns valid=true for \"vless://c2841505-ec32-4b8d-b6dd-3e19d648c321@127.0.0.1:45507?type=xhttp&encryption=none&path=%2Fxhttppath&host=xhttp&mode=auto&security=none#vless-xhttp\"","duration":0.17130000000000223,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Valid URL: tcp + ipv6"],"fullName":"validateVlessUrl Valid URL: tcp + ipv6 returns valid=true for \"vless://94792286-7bbe-4f33-8b36-18d1bbf70723@[2001:db8::1]:34520?type=tcp&encryption=none&security=none#vless-ipv6\"","status":"passed","title":"returns valid=true for \"vless://94792286-7bbe-4f33-8b36-18d1bbf70723@[2001:db8::1]:34520?type=tcp&encryption=none&security=none#vless-ipv6\"","duration":0.20849999999998658,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: No prefix"],"fullName":"validateVlessUrl Invalid URL: No prefix returns valid=false for \"uuid@host:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"uuid@host:443?type=tcp&security=tls\"","duration":0.1383999999999901,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: No uuid"],"fullName":"validateVlessUrl Invalid URL: No uuid returns valid=false for \"vless://@127.0.0.1:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"vless://@127.0.0.1:443?type=tcp&security=tls\"","duration":0.11240000000000805,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: No host"],"fullName":"validateVlessUrl Invalid URL: No host returns valid=false for \"vless://uuid@:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"vless://uuid@:443?type=tcp&security=tls\"","duration":0.27129999999999654,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: No port"],"fullName":"validateVlessUrl Invalid URL: No port returns valid=false for \"vless://uuid@127.0.0.1?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"vless://uuid@127.0.0.1?type=tcp&security=tls\"","duration":0.09759999999999991,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: Invalid host"],"fullName":"validateVlessUrl Invalid URL: Invalid host returns valid=false for \"vless://uuid@bad_host:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"vless://uuid@bad_host:443?type=tcp&security=tls\"","duration":0.2560999999999751,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: Invalid port"],"fullName":"validateVlessUrl Invalid URL: Invalid port returns valid=false for \"vless://uuid@127.0.0.1:abc?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"vless://uuid@127.0.0.1:abc?type=tcp&security=tls\"","duration":0.1027000000000271,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: Unbracketed IPv6 with port"],"fullName":"validateVlessUrl Invalid URL: Unbracketed IPv6 with port returns valid=false for \"vless://uuid@2001:db8::1:443?type=tcp&security=tls\"","status":"passed","title":"returns valid=false for \"vless://uuid@2001:db8::1:443?type=tcp&security=tls\"","duration":0.09560000000001878,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: Unsupported type"],"fullName":"validateVlessUrl Invalid URL: Unsupported type returns valid=false for \"vless://uuid@127.0.0.1:443?type=quic&security=tls\"","status":"passed","title":"returns valid=false for \"vless://uuid@127.0.0.1:443?type=quic&security=tls\"","duration":0.11840000000000828,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: Missing security"],"fullName":"validateVlessUrl Invalid URL: Missing security returns valid=false for \"vless://uuid@127.0.0.1:443?type=tcp\"","status":"passed","title":"returns valid=false for \"vless://uuid@127.0.0.1:443?type=tcp\"","duration":0.10770000000002256,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: reality without pbk"],"fullName":"validateVlessUrl Invalid URL: reality without pbk returns valid=false for \"vless://uuid@127.0.0.1:443?type=tcp&security=reality&fp=chrome\"","status":"passed","title":"returns valid=false for \"vless://uuid@127.0.0.1:443?type=tcp&security=reality&fp=chrome\"","duration":0.11410000000000764,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: reality without fp"],"fullName":"validateVlessUrl Invalid URL: reality without fp returns valid=false for \"vless://uuid@127.0.0.1:443?type=tcp&security=reality&pbk=abc\"","status":"passed","title":"returns valid=false for \"vless://uuid@127.0.0.1:443?type=tcp&security=reality&pbk=abc\"","duration":0.11259999999998627,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl","Invalid URL: tcp + reality + unexpected spaces"],"fullName":"validateVlessUrl Invalid URL: tcp + reality + unexpected spaces returns valid=false for \"vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni= google.com&sid=f6&spx=%2F#vless-tcp-reality\"","status":"passed","title":"returns valid=false for \"vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni= google.com&sid=f6&spx=%2F#vless-tcp-reality\"","duration":0.10200000000000387,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["validateVlessUrl"],"fullName":"validateVlessUrl detects invalid port range","status":"passed","title":"detects invalid port range","duration":0.1530999999999949,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886414,"endTime":1789128886425.153,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/validators/tests/validateVlessUrl.test.js"},{"assertionResults":[{"ancestorTitles":["fetchNetworkInterfaces"],"fullName":"fetchNetworkInterfaces returns empty array when rpc and network are not available","status":"passed","title":"returns empty array when rpc and network are not available","duration":3.349400000000003,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887436,"endTime":1789128887439.3494,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/fetchers/tests/fetchNetworkInterfaces.test.ts"},{"assertionResults":[{"ancestorTitles":["fetchServicesInfo"],"fullName":"fetchServicesInfo returns the fast UI state after applying it to the shared store","status":"passed","title":"returns the fast UI state after applying it to the shared store","duration":7.148599999999988,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["fetchServicesInfo"],"fullName":"fetchServicesInfo keeps the previous service state when the fallback status fetch fails","status":"passed","title":"keeps the previous service state when the fallback status fetch fails","duration":3.125699999999995,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886666,"endTime":1789128886676.1257,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/fetchers/tests/fetchServicesInfo.test.ts"},{"assertionResults":[{"ancestorTitles":["formatServiceStatusMessage"],"fullName":"formatServiceStatusMessage formats normal provider status","status":"passed","title":"formats normal provider status","duration":2.9647999999999968,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["formatServiceStatusMessage"],"fullName":"formatServiceStatusMessage formats action not ready with details","status":"passed","title":"formats action not ready with details","duration":0.3459000000000003,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["formatServiceStatusMessage"],"fullName":"formatServiceStatusMessage formats missing binary path","status":"passed","title":"formats missing binary path","duration":0.35410000000001673,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["formatServiceStatusMessage"],"fullName":"formatServiceStatusMessage formats empty or unknown string","status":"passed","title":"formats empty or unknown string","duration":0.5701000000000249,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887267,"endTime":1789128887271.57,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/helpers/tests/formatServiceStatusMessage.test.ts"},{"assertionResults":[{"ancestorTitles":["isTransientRpcError"],"fullName":"isTransientRpcError detects LuCI RPC reply races caused by interrupted page requests","status":"passed","title":"detects LuCI RPC reply races caused by interrupted page requests","duration":2.566100000000006,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["isTransientRpcError"],"fullName":"isTransientRpcError does not treat backend action failures as transient transport errors","status":"passed","title":"does not treat backend action failures as transient transport errors","duration":0.33789999999999054,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887392,"endTime":1789128887395.338,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/helpers/tests/isTransientRpcError.test.ts"},{"assertionResults":[{"ancestorTitles":["restored action loading"],"fullName":"restored action loading shows loading only for actions that are still running","status":"passed","title":"shows loading only for actions that are still running","duration":2.2156000000000233,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887405,"endTime":1789128887407.2156,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/helpers/tests/restoredActionLoading.test.ts"},{"assertionResults":[{"ancestorTitles":["getServiceAvailability"],"fullName":"getServiceAvailability does not treat an unresolved service status as stopped","status":"passed","title":"does not treat an unresolved service status as stopped","duration":2.085800000000006,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getServiceAvailability"],"fullName":"getServiceAvailability distinguishes a stopped service from an unavailable status request","status":"passed","title":"distinguishes a stopped service from an unavailable status request","duration":0.29699999999999704,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getServiceAvailability"],"fullName":"getServiceAvailability accepts the numeric running flag returned by the backend","status":"passed","title":"accepts the numeric running flag returned by the backend","duration":0.20120000000000005,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887367,"endTime":1789128887369.297,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/helpers/tests/serviceAvailability.test.ts"},{"assertionResults":[{"ancestorTitles":["LogNotificationDeduper"],"fullName":"LogNotificationDeduper accepts error, fatal, and component update log lines","status":"passed","title":"accepts error, fatal, and component update log lines","duration":3.9802999999999997,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["LogNotificationDeduper"],"fullName":"LogNotificationDeduper suppresses transient/expected error patterns","status":"passed","title":"suppresses transient/expected error patterns","duration":0.4381999999999948,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["LogNotificationDeduper"],"fullName":"LogNotificationDeduper dedupes already shown log lines through session storage","status":"passed","title":"dedupes already shown log lines through session storage","duration":0.6097000000000037,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["LogNotificationDeduper"],"fullName":"LogNotificationDeduper keeps the full log line as the replay key","status":"passed","title":"keeps the full log line as the replay key","duration":0.2863999999999862,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887151,"endTime":1789128887156.2864,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/services/tests/logNotificationDeduper.service.test.ts"},{"assertionResults":[{"ancestorTitles":["refreshRuntimeUiState"],"fullName":"refreshRuntimeUiState applies current UI state to the shared store","status":"passed","title":"applies current UI state to the shared store","duration":7.673999999999978,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["refreshRuntimeUiState"],"fullName":"refreshRuntimeUiState coalesces concurrent refreshes into one RPC call","status":"passed","title":"coalesces concurrent refreshes into one RPC call","duration":2.2210000000000036,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["refreshRuntimeUiState"],"fullName":"refreshRuntimeUiState notifies subscribers after applying fresh state","status":"passed","title":"notifies subscribers after applying fresh state","duration":2.5207000000000335,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["refreshRuntimeUiState"],"fullName":"refreshRuntimeUiState replays cached state to a new subscriber without another RPC","status":"passed","title":"replays cached state to a new subscriber without another RPC","duration":1.7595999999999776,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["refreshRuntimeUiState"],"fullName":"refreshRuntimeUiState exposes the cached state without another RPC","status":"passed","title":"exposes the cached state without another RPC","duration":2.61850000000004,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886379,"endTime":1789128886395.6184,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/services/tests/runtimeUiState.service.test.ts"},{"assertionResults":[{"ancestorTitles":["socket service"],"fullName":"socket service keeps the initial subscriber when the first connection fails","status":"passed","title":"keeps the initial subscriber when the first connection fails","duration":30.38549999999998,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886242,"endTime":1789128886272.3855,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/services/tests/socket.service.test.ts"},{"assertionResults":[{"ancestorTitles":["StoreService"],"fullName":"StoreService diffs only keys provided to set","status":"passed","title":"diffs only keys provided to set","duration":4.631299999999982,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["StoreService"],"fullName":"StoreService does not notify when provided values are unchanged","status":"passed","title":"does not notify when provided values are unchanged","duration":0.729299999999995,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887222,"endTime":1789128887227.7292,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/services/tests/store.service.test.ts"},{"assertionResults":[{"ancestorTitles":["TachyonLogWatcher"],"fullName":"TachyonLogWatcher emits the initial logread snapshot","status":"passed","title":"emits the initial logread snapshot","duration":5.163700000000006,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonLogWatcher"],"fullName":"TachyonLogWatcher does not emit the same log line twice","status":"passed","title":"does not emit the same log line twice","duration":0.8716999999999757,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonLogWatcher"],"fullName":"TachyonLogWatcher forgets old tracked lines only after the tracked window is exceeded","status":"passed","title":"forgets old tracked lines only after the tracked window is exceeded","duration":0.8556999999999846,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887174,"endTime":1789128887180.8557,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/services/tests/tachyonLogWatcher.service.test.ts"},{"assertionResults":[{"ancestorTitles":["UiActionNotificationTracker"],"fullName":"UiActionNotificationTracker does not notify jobs that were not started by this UI session","status":"passed","title":"does not notify jobs that were not started by this UI session","duration":2.8821000000000083,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["UiActionNotificationTracker"],"fullName":"UiActionNotificationTracker notifies an owned job exactly once","status":"passed","title":"notifies an owned job exactly once","duration":0.6129000000000246,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["UiActionNotificationTracker"],"fullName":"UiActionNotificationTracker keeps ownership and notification state across reloads","status":"passed","title":"keeps ownership and notification state across reloads","duration":0.4111999999999796,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887333,"endTime":1789128887336.6128,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/services/tests/uiActionNotification.service.test.ts"},{"assertionResults":[{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore applies service, capability, and running action state before first render","status":"passed","title":"applies service, capability, and running action state before first render","duration":6.997799999999984,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore clears finished persisted action flags while preserving local enable actions","status":"passed","title":"clears finished persisted action flags while preserving local enable actions","duration":1.6402000000000498,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore keeps locally started actions loading until their owner clears them","status":"passed","title":"keeps locally started actions loading until their owner clears them","duration":1.7980000000000018,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore preserves local latency progress while waiting for runtime progress","status":"passed","title":"preserves local latency progress while waiting for runtime progress","duration":1.4233000000000402,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore preserves latency progress for running runtime actions without progress","status":"passed","title":"preserves latency progress for running runtime actions without progress","duration":1.264099999999985,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore maps a running reload action to the restart control","status":"passed","title":"maps a running reload action to the restart control","duration":1.0399999999999636,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore does not combine a stale extended version with tiny capabilities","status":"passed","title":"does not combine a stale extended version with tiny capabilities","duration":1.5684999999999718,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore keeps current sing-box variant while a different sing-box install action is running","status":"passed","title":"keeps current sing-box variant while a different sing-box install action is running","duration":2.448199999999929,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["applyUiStateToStore"],"fullName":"applyUiStateToStore marks the running sing-box install action without pretending it is installed","status":"passed","title":"marks the running sing-box install action without pretending it is installed","duration":1.7234999999999445,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886269,"endTime":1789128886288.7234,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/services/tests/uiState.service.test.ts"},{"assertionResults":[{"ancestorTitles":["callBaseMethod"],"fullName":"callBaseMethod fails on non-zero exit code even when stdout is present by default","status":"passed","title":"fails on non-zero exit code even when stdout is present by default","duration":3.9793999999999983,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["callBaseMethod"],"fullName":"callBaseMethod can return stdout for diagnostic commands with non-zero exit code","status":"passed","title":"can return stdout for diagnostic commands with non-zero exit code","duration":0.8295999999999992,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["callBaseMethod"],"fullName":"callBaseMethod passes a custom timeout to long-running commands","status":"passed","title":"passes a custom timeout to long-running commands","duration":0.7840999999999951,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887271,"endTime":1789128887276.7842,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/methods/shell/tests/callBaseMethod.test.ts"},{"assertionResults":[{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction does not fail Tachyon self-update when status polling disappears after package replacement","status":"passed","title":"does not fail Tachyon self-update when status polling disappears after package replacement","duration":12.051500000000033,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction returns the backend component action start error message","status":"passed","title":"returns the backend component action start error message","duration":1.276200000000017,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction keeps following a component job after the former browser-side wait timeout while the backend reports it running","status":"passed","title":"keeps following a component job after the former browser-side wait timeout while the backend reports it running","duration":16.44279999999992,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction keeps waiting when status polling briefly fails but UI state still reports the job running","status":"passed","title":"keeps waiting when status polling briefly fails but UI state still reports the job running","duration":1.6369999999999436,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction keeps waiting through a transient RPC reply loss until the action state settles","status":"passed","title":"keeps waiting through a transient RPC reply loss until the action state settles","duration":0.8713999999999942,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction completes a Tachyon self-update when the backend is stuck reporting the job running (worker pid recycled, RPC eventually fails)","status":"passed","title":"completes a Tachyon self-update when the backend is stuck reporting the job running (worker pid recycled, RPC eventually fails)","duration":1.1224999999999454,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction treats a stale job state as success for a Tachyon self-update when the installed version matches","status":"passed","title":"treats a stale job state as success for a Tachyon self-update when the installed version matches","duration":1.3473000000000184,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction keeps reporting the stale error when the installed version did not change","status":"passed","title":"keeps reporting the stale error when the installed version did not change","duration":0.8452000000000908,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction completes a same-version Tachyon reinstall as success when the target version is unknown and the job is stale","status":"passed","title":"completes a same-version Tachyon reinstall as success when the target version is unknown and the job is stale","duration":1.6228999999999587,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction completes a same-version Tachyon install as success when target version matches baseline and job is stale","status":"passed","title":"completes a same-version Tachyon install as success when target version matches baseline and job is stale","duration":1.1340000000000146,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction closes the modal with success when a self-update never confirms and the hard timeout is reached","status":"passed","title":"closes the modal with success when a self-update never confirms and the hard timeout is reached","duration":0.8957000000000335,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction fails a self-update on the hard timeout when the target version is known but never reached","status":"passed","title":"fails a self-update on the hard timeout when the target version is known but never reached","duration":4.506899999999973,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.componentAction"],"fullName":"TachyonShellMethods.componentAction does not prematurely confirm same-release Tachyon update while the background job is still running","status":"passed","title":"does not prematurely confirm same-release Tachyon update while the background job is still running","duration":1.3052999999999884,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886259,"endTime":1789128886304.3054,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/methods/shell/tests/componentAction.test.ts"},{"assertionResults":[{"ancestorTitles":["TachyonShellMethods DNS Benchmark Actions"],"fullName":"TachyonShellMethods DNS Benchmark Actions starts DNS benchmark asynchronously","status":"passed","title":"starts DNS benchmark asynchronously","duration":4.982100000000003,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods DNS Benchmark Actions"],"fullName":"TachyonShellMethods DNS Benchmark Actions handles start DNS benchmark failure","status":"passed","title":"handles start DNS benchmark failure","duration":0.6479999999999961,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods DNS Benchmark Actions"],"fullName":"TachyonShellMethods DNS Benchmark Actions retrieves DNS benchmark status and results","status":"passed","title":"retrieves DNS benchmark status and results","duration":2.0637999999999863,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods DNS Benchmark Actions"],"fullName":"TachyonShellMethods DNS Benchmark Actions stops running DNS benchmark","status":"passed","title":"stops running DNS benchmark","duration":0.5819999999999936,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods DNS Benchmark Actions"],"fullName":"TachyonShellMethods DNS Benchmark Actions applies DNS benchmark recommendation","status":"passed","title":"applies DNS benchmark recommendation","duration":0.9979999999999905,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886902,"endTime":1789128886911.998,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/methods/shell/tests/dnsBenchmarkAction.test.ts"},{"assertionResults":[{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions starts fuzzer with specified engine and target","status":"passed","title":"starts fuzzer with specified engine and target","duration":4.32140000000004,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions starts fuzzer with combinatorial mode","status":"passed","title":"starts fuzzer with combinatorial mode","duration":0.772899999999936,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions gets fuzzer status and results","status":"passed","title":"gets fuzzer status and results","duration":1.7916999999999916,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions applies winning strategy","status":"passed","title":"applies winning strategy","duration":0.7264000000000124,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions synthesizes AI strategies","status":"passed","title":"synthesizes AI strategies","duration":0.7142000000000053,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions gets fuzzer patterns configuration","status":"passed","title":"gets fuzzer patterns configuration","duration":0.5475000000000136,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions saves fuzzer patterns configuration","status":"passed","title":"saves fuzzer patterns configuration","duration":0.5855000000000246,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions resets fuzzer patterns configuration","status":"passed","title":"resets fuzzer patterns configuration","duration":0.39430000000004384,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods Fuzzer Actions"],"fullName":"TachyonShellMethods Fuzzer Actions stops running fuzzer","status":"passed","title":"stops running fuzzer","duration":1.0966999999999416,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886259,"endTime":1789128886270.0967,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/methods/shell/tests/fuzzerAction.test.ts"},{"assertionResults":[{"ancestorTitles":["TachyonShellMethods.latencyAction"],"fullName":"TachyonShellMethods.latencyAction keeps waiting through a transient RPC reply loss","status":"passed","title":"keeps waiting through a transient RPC reply loss","duration":9.036699999999996,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.latencyAction"],"fullName":"TachyonShellMethods.latencyAction passes an optional timeout to latency jobs","status":"passed","title":"passes an optional timeout to latency jobs","duration":1.3626000000000147,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886515,"endTime":1789128886525.3625,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/methods/shell/tests/latencyAction.test.ts"},{"assertionResults":[{"ancestorTitles":["TachyonShellMethods.serviceAction"],"fullName":"TachyonShellMethods.serviceAction keeps failed finished service state available to low-level waiters","status":"passed","title":"keeps failed finished service state available to low-level waiters","duration":9.903199999999998,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.serviceAction"],"fullName":"TachyonShellMethods.serviceAction returns the backend service action start error message","status":"passed","title":"returns the backend service action start error message","duration":1.2295999999999765,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886781,"endTime":1789128886792.2295,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/methods/shell/tests/serviceAction.test.ts"},{"assertionResults":[{"ancestorTitles":["TachyonShellMethods.subscriptionUpdate"],"fullName":"TachyonShellMethods.subscriptionUpdate starts a targeted background subscription update and waits for its state","status":"passed","title":"starts a targeted background subscription update and waits for its state","duration":9.04320000000007,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.subscriptionUpdate"],"fullName":"TachyonShellMethods.subscriptionUpdate returns the backend subscription start error message","status":"passed","title":"returns the backend subscription start error message","duration":1.3260999999999967,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.subscriptionUpdate"],"fullName":"TachyonShellMethods.subscriptionUpdate returns failed finished job state from the low-level waiter for UI restoration","status":"passed","title":"returns failed finished job state from the low-level waiter for UI restoration","duration":2.207899999999995,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.subscriptionUpdate"],"fullName":"TachyonShellMethods.subscriptionUpdate keeps following a subscription job after the former browser-side wait timeout while the backend reports it running","status":"passed","title":"keeps following a subscription job after the former browser-side wait timeout while the backend reports it running","duration":11.856800000000021,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["TachyonShellMethods.subscriptionUpdate"],"fullName":"TachyonShellMethods.subscriptionUpdate keeps waiting through a transient RPC reply loss","status":"passed","title":"keeps waiting through a transient RPC reply loss","duration":0.9316000000000031,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886257,"endTime":1789128886282.9316,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/methods/shell/tests/subscriptionUpdate.test.ts"},{"assertionResults":[{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections shows the full selector group by default","status":"passed","title":"shows the full selector group by default","duration":6.99129999999991,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections hydrates URLTest details from the section cache and Clash API","status":"passed","title":"hydrates URLTest details from the section cache and Clash API","duration":2.5386000000000877,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections marks URLTest group as manually selected when a hidden child is active in the selector","status":"passed","title":"marks URLTest group as manually selected when a hidden child is active in the selector","duration":1.1065000000000964,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections keeps an empty configured URLTest visible when sing-box skipped the group","status":"passed","title":"keeps an empty configured URLTest visible when sing-box skipped the group","duration":1.1145999999999958,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections hydrates Priority dashboard details from config, cache and Clash API","status":"passed","title":"hydrates Priority dashboard details from config, cache and Clash API","duration":2.6112999999999147,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections keeps an empty Priority group visible when sing-box skipped the selector","status":"passed","title":"keeps an empty Priority group visible when sing-box skipped the selector","duration":0.7921000000000049,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections keeps legacy URLTest sections compatible before migration","status":"passed","title":"keeps legacy URLTest sections compatible before migration","duration":1.6516000000000304,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections supports multiple configured URLTest groups","status":"passed","title":"supports multiple configured URLTest groups","duration":0.9850000000000136,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections keeps non-built-in URLTest groups in selector order when latency sorting is disabled","status":"passed","title":"keeps non-built-in URLTest groups in selector order when latency sorting is disabled","duration":1.3464999999999918,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections sorts outbounds by latency only when enabled and does not prioritize URLTest groups except Fastest","status":"passed","title":"sorts outbounds by latency only when enabled and does not prioritize URLTest groups except Fastest","duration":0.8858000000000175,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections sorts an unpinned configured URLTest group by latency","status":"passed","title":"sorts an unpinned configured URLTest group by latency","duration":0.6208999999998923,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections does not expose flag-emoji detected countries on dashboard outbounds","status":"passed","title":"does not expose flag-emoji detected countries on dashboard outbounds","duration":0.48509999999998854,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections exposes country.is detected countries on dashboard outbounds","status":"passed","title":"exposes country.is detected countries on dashboard outbounds","duration":0.4311000000000149,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections ignores legacy link refs without a resolved proxy link","status":"passed","title":"ignores legacy link refs without a resolved proxy link","duration":0.9844000000000506,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections uses cached subscription links without a click-time backend lookup","status":"passed","title":"uses cached subscription links without a click-time backend lookup","duration":0.5444999999999709,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections does not expose countries when URLTest filtering is set to all servers","status":"passed","title":"does not expose countries when URLTest filtering is set to all servers","duration":1.408900000000017,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections uses allocated tags for section names that collide with system tags","status":"passed","title":"uses allocated tags for section names that collide with system tags","duration":0.5032999999999674,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections shows legacy VPN interface sections as a Connection selector group","status":"passed","title":"shows legacy VPN interface sections as a Connection selector group","duration":0.6750000000000682,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections hydrates migrated interface children into the Connection selector group","status":"passed","title":"hydrates migrated interface children into the Connection selector group","duration":0.5729000000000042,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections exposes ByeDPI sections on the dashboard as service cards","status":"passed","title":"exposes ByeDPI sections on the dashboard as service cards","duration":0.7234999999999445,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections exposes single-endpoint protocol sections (Mieru, Sudoku, AnyTLS, Snell, MASQUE, OpenVPN) on the dashboard","status":"passed","title":"exposes single-endpoint protocol sections (Mieru, Sudoku, AnyTLS, Snell, MASQUE, OpenVPN) on the dashboard","duration":1.0061999999999216,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections fetches Clash API proxies directly in the browser to avoid rpcd output limits","status":"passed","title":"fetches Clash API proxies directly in the browser to avoid rpcd output limits","duration":1.5497000000000298,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections uses rpcd fallback when direct Clash API access times out","status":"passed","title":"uses rpcd fallback when direct Clash API access times out","duration":4.786399999999958,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDashboardSections"],"fullName":"getDashboardSections uses rpcd fallback when direct Clash API access is unsafe","status":"passed","title":"uses rpcd fallback when direct Clash API access is unsafe","duration":0.6327999999999747,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886236,"endTime":1789128886271.7864,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/methods/custom/tests/getDashboardSections.test.ts"},{"assertionResults":[{"ancestorTitles":["dashboard initController"],"fullName":"dashboard initController should export initController function","status":"passed","title":"should export initController function","duration":1063.3658,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886017,"endTime":1789128887080.3657,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/dashboard/tests/initController.test.ts"},{"assertionResults":[{"ancestorTitles":["diagnostic run persistence"],"fullName":"diagnostic run persistence stores and restores a running diagnostic snapshot","status":"passed","title":"stores and restores a running diagnostic snapshot","duration":4.264700000000005,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic run persistence"],"fullName":"diagnostic run persistence removes expired snapshots","status":"passed","title":"removes expired snapshots","duration":3.050799999999981,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic run persistence"],"fullName":"diagnostic run persistence removes invalid snapshots","status":"passed","title":"removes invalid snapshots","duration":0.28589999999999804,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic run persistence"],"fullName":"diagnostic run persistence clears persisted snapshots","status":"passed","title":"clears persisted snapshots","duration":0.3609000000000151,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic run persistence"],"fullName":"diagnostic run persistence removes snapshots with unsupported check shape","status":"passed","title":"removes snapshots with unsupported check shape","duration":0.3348000000000013,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic run persistence"],"fullName":"diagnostic run persistence removes snapshots with invalid numeric fields","status":"passed","title":"removes snapshots with invalid numeric fields","duration":0.4941000000000031,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886566,"endTime":1789128886574.4941,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/tests/diagnosticRunPersistence.test.ts"},{"assertionResults":[{"ancestorTitles":["diagnostic store provider checks"],"fullName":"diagnostic store provider checks adds Zapret2 as a separate provider check between Zapret and ByeDPI","status":"passed","title":"adds Zapret2 as a separate provider check between Zapret and ByeDPI","duration":3.274499999999989,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic store provider checks"],"fullName":"diagnostic store provider checks keeps Zapret2 hidden when the provider is not available","status":"passed","title":"keeps Zapret2 hidden when the provider is not available","duration":1.2226,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic store provider checks"],"fullName":"diagnostic store provider checks keeps Inbounds hidden until enabled inbounds are confirmed","status":"passed","title":"keeps Inbounds hidden until enabled inbounds are confirmed","duration":0.32670000000001664,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic store provider checks"],"fullName":"diagnostic store provider checks shows Inbounds when enabled inbounds are confirmed","status":"passed","title":"shows Inbounds when enabled inbounds are confirmed","duration":0.37579999999999814,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887160,"endTime":1789128887165.3757,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/tests/diagnosticStoreZapret2.test.ts"},{"assertionResults":[{"ancestorTitles":["renderLeakCheckModal"],"fullName":"renderLeakCheckModal renders and displays the IP & DNS Leak Detection modal","status":"passed","title":"renders and displays the IP & DNS Leak Detection modal","duration":6.504000000000019,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderLeakCheckModal"],"fullName":"renderLeakCheckModal calls TachyonShellMethods.leakCheck on start","status":"passed","title":"calls TachyonShellMethods.leakCheck on start","duration":1.1170000000000755,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderLeakCheckModal"],"fullName":"renderLeakCheckModal handles leak detection when IP is leaked","status":"passed","title":"handles leak detection when IP is leaked","duration":0.8581000000000358,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderLeakCheckModal"],"fullName":"renderLeakCheckModal handles offline proxy gracefully","status":"passed","title":"handles offline proxy gracefully","duration":1.2726999999999862,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887089,"endTime":1789128887098.2727,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/tests/leakCheckModal.test.ts"},{"assertionResults":[{"ancestorTitles":["diagnostic masking"],"fullName":"diagnostic masking masks sensitive sing-box keys without mutating the original config","status":"passed","title":"masks sensitive sing-box keys without mutating the original config","duration":3.602000000000004,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic masking"],"fullName":"diagnostic masking formats masked sing-box config from a raw JSON string","status":"passed","title":"formats masked sing-box config from a raw JSON string","duration":0.5865000000000009,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic masking"],"fullName":"diagnostic masking masks sensitive global check UCI values while keeping visible structure stable","status":"passed","title":"masks sensitive global check UCI values while keeping visible structure stable","duration":2.087100000000021,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887033,"endTime":1789128887039.0872,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/tests/maskDiagnostics.test.ts"},{"assertionResults":[{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions treats service transition status as a UI transition","status":"passed","title":"treats service transition status as a UI transition","duration":2.852499999999992,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions maps reload to the restart/reload control state","status":"passed","title":"maps reload to the restart/reload control state","duration":1.078300000000013,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions detects only local button loading as local mutation","status":"passed","title":"detects only local button loading as local mutation","duration":0.29879999999999995,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions does not let backend transition status block polling forever","status":"passed","title":"does not let backend transition status block polling forever","duration":0.2400000000000091,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions does not reset diagnostics checks while a run is active","status":"passed","title":"does not reset diagnostics checks while a run is active","duration":0.4977999999999838,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions still lets local button actions suppress non-forced polling","status":"passed","title":"still lets local button actions suppress non-forced polling","duration":0.25970000000000937,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions shows restart while the service is running even when autostart is disabled","status":"passed","title":"shows restart while the service is running even when autostart is disabled","duration":0.37579999999999814,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions does not show restart from transient running state while start or stop is active","status":"passed","title":"does not show restart from transient running state while start or stop is active","duration":0.26019999999999754,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions allows diagnostics while the service is running even when autostart is disabled","status":"passed","title":"allows diagnostics while the service is running even when autostart is disabled","duration":0.8487999999999829,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions detects running component actions","status":"passed","title":"detects running component actions","duration":0.3918999999999926,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions blocks mutating available actions while component actions are running","status":"passed","title":"blocks mutating available actions while component actions are running","duration":0.34199999999998454,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions keeps logs available during service-only mutations","status":"passed","title":"keeps logs available during service-only mutations","duration":0.179300000000012,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions keeps the lower service action visible while restart is running","status":"passed","title":"keeps the lower service action visible while restart is running","duration":0.20640000000000214,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["diagnostic service transitions"],"fullName":"diagnostic service transitions keeps the normal start and stop visibility outside restart","status":"passed","title":"keeps the normal start and stop visibility outside restart","duration":0.1741000000000099,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886727,"endTime":1789128886735.342,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/tests/serviceTransition.test.ts"},{"assertionResults":[{"ancestorTitles":["renderStrategyFuzzerModal"],"fullName":"renderStrategyFuzzerModal renders and displays the Strategy Fuzzer modal","status":"passed","title":"renders and displays the Strategy Fuzzer modal","duration":7.311899999999923,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderStrategyFuzzerModal"],"fullName":"renderStrategyFuzzerModal handles fuzzer status with successful probe results and best badge","status":"passed","title":"handles fuzzer status with successful probe results and best badge","duration":21.536699999999882,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887090,"endTime":1789128887118.5366,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/tests/strategyFuzzerModal.test.ts"},{"assertionResults":[{"ancestorTitles":["monitoring initController"],"fullName":"monitoring initController should export initController function","status":"passed","title":"should export initController function","duration":1040.1535,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["monitoring initController"],"fullName":"monitoring initController should resolve route display names correctly for built-in and section tags","status":"passed","title":"should resolve route display names correctly for built-in and section tags","duration":2.241600000000062,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886039,"endTime":1789128887081.2417,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/monitoring/tests/initController.test.ts"},{"assertionResults":[{"ancestorTitles":["updates check result lifecycle"],"fullName":"updates check result lifecycle preserves a check result that completed while the Components tab was hidden","status":"passed","title":"preserves a check result that completed while the Components tab was hidden","duration":2.7220000000000084,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["updates check result lifecycle"],"fullName":"updates check result lifecycle resets check results on the next ordinary reopen","status":"passed","title":"resets check results on the next ordinary reopen","duration":0.2994000000000199,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["updates check result lifecycle"],"fullName":"updates check result lifecycle does not reset while an action is still loading","status":"passed","title":"does not reset while an action is still loading","duration":0.238900000000001,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["updates check result lifecycle"],"fullName":"updates check result lifecycle keeps cached results on every reopen when automatic checks are enabled","status":"passed","title":"keeps cached results on every reopen when automatic checks are enabled","duration":0.3007000000000062,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["updates check result lifecycle"],"fullName":"updates check result lifecycle requires a fresh state read before rendering cached running component actions","status":"passed","title":"requires a fresh state read before rendering cached running component actions","duration":0.3125,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["updates check result lifecycle"],"fullName":"updates check result lifecycle exposes check results immediately on remount once cache state is resolved","status":"passed","title":"exposes check results immediately on remount once cache state is resolved","duration":0.5098000000000127,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887304,"endTime":1789128887308.5098,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/updates/tests/checkResultLifecycle.test.ts"},{"assertionResults":[{"ancestorTitles":["shouldApplyCompletedComponentActionResult"],"fullName":"shouldApplyCompletedComponentActionResult does not restore completed check results that were already handled before reload","status":"passed","title":"does not restore completed check results that were already handled before reload","duration":2.5160999999999945,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["shouldApplyCompletedComponentActionResult"],"fullName":"shouldApplyCompletedComponentActionResult applies completed check results when this UI still owns the pending notification","status":"passed","title":"applies completed check results when this UI still owns the pending notification","duration":0.28759999999999764,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["shouldApplyCompletedComponentActionResult"],"fullName":"shouldApplyCompletedComponentActionResult always applies completed mutating actions because they reflect real system state","status":"passed","title":"always applies completed mutating actions because they reflect real system state","duration":0.20680000000001542,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887353,"endTime":1789128887356.2876,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/updates/tests/componentActionCompletion.test.ts"},{"assertionResults":[{"ancestorTitles":["same-release build description"],"fullName":"same-release build description shows a truncated transition when both builds are identified","status":"passed","title":"shows a truncated transition when both builds are identified","duration":3.886099999999999,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["same-release build description"],"fullName":"same-release build description shows the single known build when only one side has a sha","status":"passed","title":"shows the single known build when only one side has a sha","duration":0.511400000000009,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["same-release build description"],"fullName":"same-release build description falls back when the release carries no sha at all","status":"passed","title":"falls back when the release carries no sha at all","duration":0.4549000000000092,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887332,"endTime":1789128887337.4548,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/updates/tests/sameReleaseBuild.test.ts"},{"assertionResults":[{"ancestorTitles":["sessionJobs"],"fullName":"sessionJobs saves and loads handled jobs from session storage","status":"passed","title":"saves and loads handled jobs from session storage","duration":5.115800000000007,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["sessionJobs"],"fullName":"sessionJobs loads legacy tachyon_post_update_job if present","status":"passed","title":"loads legacy tachyon_post_update_job if present","duration":0.9240999999999815,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["sessionJobs"],"fullName":"sessionJobs does not duplicate job IDs in session storage","status":"passed","title":"does not duplicate job IDs in session storage","duration":1.1336000000000013,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["sessionJobs"],"fullName":"sessionJobs safeReloadPage calls window.location.reload only once","status":"passed","title":"safeReloadPage calls window.location.reload only once","duration":0.7819999999999823,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887010,"endTime":1789128887017.782,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/updates/tests/sessionJobs.test.ts"},{"assertionResults":[{"ancestorTitles":["renderUpdateProgressModal"],"fullName":"renderUpdateProgressModal renders install button and does not auto-close when onInstall is provided","status":"passed","title":"renders install button and does not auto-close when onInstall is provided","duration":1099.0711000000001,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderUpdateProgressModal"],"fullName":"renderUpdateProgressModal keeps completed installation open until user closes or autoCloseMs is explicit","status":"passed","title":"keeps completed installation open until user closes or autoCloseMs is explicit","duration":1.4265000000000327,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderUpdateProgressModal"],"fullName":"renderUpdateProgressModal polls the operation log while the job runs and stops after completion","status":"passed","title":"polls the operation log while the job runs and stops after completion","duration":4.644399999999905,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderUpdateProgressModal"],"fullName":"renderUpdateProgressModal does not poll the log when no job id was provided","status":"passed","title":"does not poll the log when no job id was provided","duration":1.1626999999998588,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886027,"endTime":1789128887133.1626,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/updates/tests/updateProgressModal.test.ts"},{"assertionResults":[{"ancestorTitles":["renderSections"],"fullName":"renderSections renders service section without serviceStatus as a clean service card without latency button","status":"passed","title":"renders service section without serviceStatus as a clean service card without latency button","duration":4.968600000000151,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderSections"],"fullName":"renderSections renders service section with serviceStatus as a service card without latency button","status":"passed","title":"renders service section with serviceStatus as a service card without latency button","duration":1.2349999999999,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderSections"],"fullName":"renderSections renders zapret and byedpi service sections without latency button","status":"passed","title":"renders zapret and byedpi service sections without latency button","duration":0.9308000000000902,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderSections"],"fullName":"renderSections renders latency test button for proxy outbound sections with outbounds","status":"passed","title":"renders latency test button for proxy outbound sections with outbounds","duration":1.944800000000214,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderSections"],"fullName":"renderSections does not render latency test button for empty proxy section","status":"passed","title":"does not render latency test button for empty proxy section","duration":0.7111999999999625,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["renderSections"],"fullName":"renderSections renders latency test button and connection node item for Mieru section","status":"passed","title":"renders latency test button and connection node item for Mieru section","duration":0.9035000000001219,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887124,"endTime":1789128887134.9036,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/dashboard/partials/tests/renderSections.test.ts"},{"assertionResults":[{"ancestorTitles":["getDnsCheckPresentation"],"fullName":"getDnsCheckPresentation keeps manual DHCP mode as a warning instead of a DNS failure","status":"passed","title":"keeps manual DHCP mode as a warning instead of a DNS failure","duration":4.021899999999988,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["getDnsCheckPresentation"],"fullName":"getDnsCheckPresentation keeps a mismatched managed DHCP config as an error item","status":"passed","title":"keeps a mismatched managed DHCP config as an error item","duration":0.5407000000000153,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128887226,"endTime":1789128887230.5408,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/tests/getDnsCheckPresentation.test.ts"},{"assertionResults":[{"ancestorTitles":["provider diagnostics checks"],"fullName":"provider diagnostics checks shows Zapret provider path and does not fail on hidden route status","status":"passed","title":"shows Zapret provider path and does not fail on hidden route status","duration":6.439099999999996,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["provider diagnostics checks"],"fullName":"provider diagnostics checks keeps Zapret outbound failures visible and red","status":"passed","title":"keeps Zapret outbound failures visible and red","duration":0.9512999999999465,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["provider diagnostics checks"],"fullName":"provider diagnostics checks shows Zapret2 route rules check and keeps provider path visible","status":"passed","title":"shows Zapret2 route rules check and keeps provider path visible","duration":1.1609000000000265,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["provider diagnostics checks"],"fullName":"provider diagnostics checks shows Zapret2 route rules failure when routes_configured is false","status":"passed","title":"shows Zapret2 route rules failure when routes_configured is false","duration":0.8599000000000387,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["provider diagnostics checks"],"fullName":"provider diagnostics checks removes ByeDPI route checks from the visible and aggregate status","status":"passed","title":"removes ByeDPI route checks from the visible and aggregate status","duration":0.919399999999996,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886714,"endTime":1789128886723.9194,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/tests/runProviderChecks.test.ts"},{"assertionResults":[{"ancestorTitles":["runSectionsCheck"],"fullName":"runSectionsCheck keeps VPN interface probe failures as warnings when the runtime outbound exists","status":"passed","title":"keeps VPN interface probe failures as warnings when the runtime outbound exists","duration":6.122100000000017,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["runSectionsCheck"],"fullName":"runSectionsCheck checks only the selected concrete outbound for selectable proxy sections","status":"passed","title":"checks only the selected concrete outbound for selectable proxy sections","duration":1.2521000000000129,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]},{"ancestorTitles":["runSectionsCheck"],"fullName":"runSectionsCheck checks the selected URLTest outbound directly","status":"passed","title":"checks the selected URLTest outbound directly","duration":0.7852000000000032,"failureMessages":[],"meta":{},"tags":[],"benchmarks":[]}],"startTime":1789128886923,"endTime":1789128886930.7852,"status":"passed","message":"","name":"G:/tachyon/fe-app-tachyon/src/tachyon/tabs/diagnostic/checks/tests/runSectionsCheck.test.ts"}]} \ No newline at end of file diff --git a/fe-app-tachyon/locales/calls.json b/fe-app-tachyon/locales/calls.json index dcb952bb..52e7bbe0 100644 --- a/fe-app-tachyon/locales/calls.json +++ b/fe-app-tachyon/locales/calls.json @@ -13214,8 +13214,8 @@ ] }, { - "call": "Traffic from these IP addresses will never be routed through this section, even if other conditions match.", - "key": "Traffic from these IP addresses will never be routed through this section, even if other conditions match.", + "call": "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match.", + "key": "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match.", "places": [ "../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821" ] diff --git a/fe-app-tachyon/locales/tachyon.pot b/fe-app-tachyon/locales/tachyon.pot index d156bcce..ac062e79 100644 --- a/fe-app-tachyon/locales/tachyon.pot +++ b/fe-app-tachyon/locales/tachyon.pot @@ -1329,6 +1329,81 @@ msgstr "" msgid "ByeDPI strategy cannot be empty" msgstr "" +msgid "WDTT" +msgstr "" + +msgid "WDTT URI" +msgstr "" + +msgid "WDTT connection URI (wdtt://...)" +msgstr "" + +msgid "WDTT Mode" +msgstr "" + +msgid "WDTT Device ID" +msgstr "" + +msgid "WDTT Workers" +msgstr "" + +msgid "WDTT Max Hashes" +msgstr "" + +msgid "WDTT MTU" +msgstr "" + +msgid "WDTT Refresh" +msgstr "" + +msgid "WDTT Auto Update" +msgstr "" + +msgid "WDTT Block DoH" +msgstr "" + +msgid "WDTT Block IPv6" +msgstr "" + +msgid "WDTT QWDTT Mode" +msgstr "" + +msgid "WDTT Subscription Links" +msgstr "" + +msgid "WDTT Community Lists" +msgstr "" + +msgid "WDTT Remote Domain List" +msgstr "" + +msgid "OlcRTC" +msgstr "" + +msgid "OlcRTC Provider" +msgstr "" + +msgid "OlcRTC Transport" +msgstr "" + +msgid "OlcRTC Room ID" +msgstr "" + +msgid "OlcRTC Crypto Key" +msgstr "" + +msgid "OlcRTC SOCKS Host" +msgstr "" + +msgid "OlcRTC SOCKS Port" +msgstr "" + +msgid "OlcRTC DNS Server" +msgstr "" + +msgid "OlcRTC Subscription Links" +msgstr "" + #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js:14975 #: src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts:170 msgid "Bypasses proxy (SO_MARK 0x08000000)" @@ -8155,7 +8230,7 @@ msgid "Traffic for this level goes directly." msgstr "" #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821 -msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match." +msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match." msgstr "" #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10849 diff --git a/fe-app-tachyon/locales/tachyon.ru.po b/fe-app-tachyon/locales/tachyon.ru.po index ebe93976..5f938e23 100644 --- a/fe-app-tachyon/locales/tachyon.ru.po +++ b/fe-app-tachyon/locales/tachyon.ru.po @@ -839,6 +839,81 @@ msgstr "Стратегия ByeDPI" msgid "ByeDPI strategy cannot be empty" msgstr "Стратегия ByeDPI не может быть пустой" +msgid "WDTT" +msgstr "WDTT" + +msgid "WDTT URI" +msgstr "URI WDTT" + +msgid "WDTT connection URI (wdtt://...)" +msgstr "URI подключения WDTT (wdtt://...)" + +msgid "WDTT Mode" +msgstr "Режим WDTT" + +msgid "WDTT Device ID" +msgstr "ID устройства WDTT" + +msgid "WDTT Workers" +msgstr "Рабочие процессы WDTT" + +msgid "WDTT Max Hashes" +msgstr "Максимум хешей WDTT" + +msgid "WDTT MTU" +msgstr "MTU WDTT" + +msgid "WDTT Refresh" +msgstr "Обновление WDTT" + +msgid "WDTT Auto Update" +msgstr "Автообновление WDTT" + +msgid "WDTT Block DoH" +msgstr "Блокировка DoH в WDTT" + +msgid "WDTT Block IPv6" +msgstr "Блокировка IPv6 в WDTT" + +msgid "WDTT QWDTT Mode" +msgstr "Режим QWDTT" + +msgid "WDTT Subscription Links" +msgstr "Ссылки подписок WDTT" + +msgid "WDTT Community Lists" +msgstr "Списки сообществ WDTT" + +msgid "WDTT Remote Domain List" +msgstr "Удалённый список доменов WDTT" + +msgid "OlcRTC" +msgstr "OlcRTC" + +msgid "OlcRTC Provider" +msgstr "Провайдер OlcRTC" + +msgid "OlcRTC Transport" +msgstr "Транспорт OlcRTC" + +msgid "OlcRTC Room ID" +msgstr "ID комнаты OlcRTC" + +msgid "OlcRTC Crypto Key" +msgstr "Ключ шифрования OlcRTC" + +msgid "OlcRTC SOCKS Host" +msgstr "Хост SOCKS OlcRTC" + +msgid "OlcRTC SOCKS Port" +msgstr "Порт SOCKS OlcRTC" + +msgid "OlcRTC DNS Server" +msgstr "DNS-сервер OlcRTC" + +msgid "OlcRTC Subscription Links" +msgstr "Ссылки подписок OlcRTC" + msgid "Bypasses proxy (SO_MARK 0x08000000)" msgstr "В обход прокси (SO_MARK 0x08000000)" @@ -1445,6 +1520,24 @@ msgstr "DNS через прокси" msgid "DNS through section" msgstr "DNS через секцию" +msgid "Routed DNS" +msgstr "Маршрутизированный DNS" + +msgid "Resolve matched domains through an encrypted DNS server of this section instead of FakeIP. DNS queries traverse the same proxy as the section's outbound." +msgstr "Резолвить совпавшие домены через зашифрованный DNS-сервер этой секции вместо FakeIP. DNS-запросы проходят через тот же прокси, что и исходящий трафик секции." + +msgid "Routed DNS Protocol" +msgstr "Протокол маршрутизированного DNS" + +msgid "Protocol for the routed DNS resolver." +msgstr "Протокол для DNS-резолвера маршрутизации." + +msgid "Routed DNS Servers" +msgstr "Серверы маршрутизированного DNS" + +msgid "DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query)." +msgstr "Адреса DNS-серверов для резолвинга доменов в этой секции (напр. 1.1.1.1, https://cloudflare-dns.com/dns-query)." + msgid "DNS Turbo Cache" msgstr "Быстрый DNS-кэш" @@ -5096,8 +5189,8 @@ msgstr "Трафик" msgid "Traffic for this level goes directly." msgstr "Трафик этого уровня идёт напрямую." -msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match." -msgstr "Трафик от этих IP-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают." +msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match." +msgstr "Трафик от этих IP-адресов, CIDR или MAC-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают." msgid "Traffic of specified protocols (e.g. BitTorrent, QUIC, STUN) will bypass this section." msgstr "Трафик указанных протоколов (например BitTorrent, QUIC, STUN) будет обходить эту секцию." diff --git a/fe-app-tachyon/src/tachyon/helpers/getComponentActionKey.ts b/fe-app-tachyon/src/tachyon/helpers/getComponentActionKey.ts index aa264a6e..e7cd1f9f 100644 --- a/fe-app-tachyon/src/tachyon/helpers/getComponentActionKey.ts +++ b/fe-app-tachyon/src/tachyon/helpers/getComponentActionKey.ts @@ -33,6 +33,16 @@ const componentActionKeyMap: Record = { 'byedpi:install_version': 'byedpiInstall', 'byedpi:remove': 'byedpiRemove', 'byedpi:rollback': 'byedpiRollback', + 'wdtt:check_update': 'wdttCheck', + 'wdtt:install': 'wdttInstall', + 'wdtt:install_version': 'wdttInstall', + 'wdtt:remove': 'wdttRemove', + 'wdtt:rollback': 'wdttRollback', + 'olcrtc:check_update': 'olcrtcCheck', + 'olcrtc:install': 'olcrtcInstall', + 'olcrtc:install_version': 'olcrtcInstall', + 'olcrtc:remove': 'olcrtcRemove', + 'olcrtc:rollback': 'olcrtcRollback', 'tailscale:check_update': 'tailscaleCheck', 'tailscale:install': 'tailscaleInstall', 'tailscale:install_version': 'tailscaleInstall', diff --git a/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts b/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts index 4b5151f6..a63003ad 100644 --- a/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts +++ b/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts @@ -415,7 +415,10 @@ function isConnectionAction(action?: string) { } function isServiceAction(action?: string) { - return Boolean(action && ['zapret', 'zapret2', 'byedpi'].includes(action)); + return Boolean( + action && + ['zapret', 'zapret2', 'byedpi', 'wdtt', 'olcrtc'].includes(action), + ); } function hasSubscriptionSources(section: Tachyon.ConfigSection) { @@ -1517,7 +1520,9 @@ export async function getDashboardSections( Promise >(); - const getServiceStatus = (serviceType: 'zapret' | 'zapret2' | 'byedpi') => { + const getServiceStatus = ( + serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc', + ) => { if (!serviceStatusCache.has(serviceType)) { serviceStatusCache.set( serviceType, @@ -1571,6 +1576,38 @@ export async function getDashboardSections( statusMessage: s.status_message, }; } + } else if (serviceType === 'wdtt') { + const result = await TachyonShellMethods.getWdttStatus(); + if (result.success && result.data) { + const s = result.data; + return { + serviceType: 'wdtt', + configured: Boolean(s.configured), + ready: Boolean(s.ready), + conflict: Boolean(s.conflict), + runningProcesses: s.running_process_count, + expectedProcesses: s.expected_process_count, + restartCount: s.restart_count, + unstable: Boolean(s.runtime_unstable), + statusMessage: s.status_message, + }; + } + } else if (serviceType === 'olcrtc') { + const result = await TachyonShellMethods.getOlcrtcStatus(); + if (result.success && result.data) { + const s = result.data; + return { + serviceType: 'olcrtc', + configured: Boolean(s.configured), + ready: Boolean(s.ready), + conflict: Boolean(s.conflict), + runningProcesses: s.running_process_count, + expectedProcesses: s.expected_process_count, + restartCount: s.restart_count, + unstable: Boolean(s.runtime_unstable), + statusMessage: s.status_message, + }; + } } } catch (_error) { // Status fetch failed — show section without status diff --git a/fe-app-tachyon/src/tachyon/methods/shell/index.ts b/fe-app-tachyon/src/tachyon/methods/shell/index.ts index 21527b17..5d2faaf9 100644 --- a/fe-app-tachyon/src/tachyon/methods/shell/index.ts +++ b/fe-app-tachyon/src/tachyon/methods/shell/index.ts @@ -316,6 +316,20 @@ export const TachyonShellMethods = { '/usr/bin/tachyon', { allowNonZeroWithStdout: true }, ), + getWdttStatus: async () => + callBaseMethod( + Tachyon.AvailableMethods.GET_WDTT_STATUS, + [], + '/usr/bin/tachyon', + { allowNonZeroWithStdout: true }, + ), + getOlcrtcStatus: async () => + callBaseMethod( + Tachyon.AvailableMethods.GET_OLCRTC_STATUS, + [], + '/usr/bin/tachyon', + { allowNonZeroWithStdout: true }, + ), getClashApiProxies: async () => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [ Tachyon.AvailableClashAPIMethods.GET_PROXIES, diff --git a/fe-app-tachyon/src/tachyon/services/core.service.ts b/fe-app-tachyon/src/tachyon/services/core.service.ts index eac9f1ec..48daab7e 100644 --- a/fe-app-tachyon/src/tachyon/services/core.service.ts +++ b/fe-app-tachyon/src/tachyon/services/core.service.ts @@ -28,6 +28,8 @@ function componentDisplayName(component: string) { zapret: 'Zapret', zapret2: 'Zapret2', byedpi: 'ByeDPI', + wdtt: 'WDTT', + olcrtc: 'OlcRTC', }; return names[component] || component; diff --git a/fe-app-tachyon/src/tachyon/services/logNotificationDeduper.service.ts b/fe-app-tachyon/src/tachyon/services/logNotificationDeduper.service.ts index 9f284df8..f3a33880 100644 --- a/fe-app-tachyon/src/tachyon/services/logNotificationDeduper.service.ts +++ b/fe-app-tachyon/src/tachyon/services/logNotificationDeduper.service.ts @@ -86,7 +86,7 @@ export function getTachyonLogNotification( } const update = line.match( - /\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi)\s+(\S+)/i, + /\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi|wdtt|olcrtc)\s+(\S+)/i, ); if (!update) { diff --git a/fe-app-tachyon/src/tachyon/services/store.service.ts b/fe-app-tachyon/src/tachyon/services/store.service.ts index 228b2a13..c2c49fea 100644 --- a/fe-app-tachyon/src/tachyon/services/store.service.ts +++ b/fe-app-tachyon/src/tachyon/services/store.service.ts @@ -233,6 +233,14 @@ export interface StoreType { byedpi_installed: number; byedpi_backup_version?: string; byedpi_backup_time?: number; + wdtt_version: string; + wdtt_installed: number; + wdtt_backup_version?: string; + wdtt_backup_time?: number; + olcrtc_version: string; + olcrtc_installed: number; + olcrtc_backup_version?: string; + olcrtc_backup_time?: number; tailscale_version: string; tailscale_installed: number; tailscale_backup_version?: string; @@ -273,6 +281,14 @@ export interface StoreType { byedpiInstall: { loading: boolean }; byedpiRemove: { loading: boolean }; byedpiRollback: { loading: boolean }; + wdttCheck: { loading: boolean }; + wdttInstall: { loading: boolean }; + wdttRemove: { loading: boolean }; + wdttRollback: { loading: boolean }; + olcrtcCheck: { loading: boolean }; + olcrtcInstall: { loading: boolean }; + olcrtcRemove: { loading: boolean }; + olcrtcRollback: { loading: boolean }; tailscaleCheck: { loading: boolean }; tailscaleInstall: { loading: boolean }; tailscaleRemove: { loading: boolean }; diff --git a/fe-app-tachyon/src/tachyon/services/systemInfo.service.ts b/fe-app-tachyon/src/tachyon/services/systemInfo.service.ts index 6f17696c..a54b08d1 100644 --- a/fe-app-tachyon/src/tachyon/services/systemInfo.service.ts +++ b/fe-app-tachyon/src/tachyon/services/systemInfo.service.ts @@ -24,6 +24,10 @@ const UNKNOWN_SYSTEM_INFO: StoreType['diagnosticsSystemInfo'] = { zapret2_installed: 0, byedpi_version: _('unknown'), byedpi_installed: 0, + wdtt_version: _('unknown'), + wdtt_installed: 0, + olcrtc_version: _('unknown'), + olcrtc_installed: 0, tailscale_version: _('unknown'), tailscale_installed: 0, server_inbounds_enabled_count: -1, @@ -108,6 +112,8 @@ export async function ensureSystemInfo({ zapret_installed: latestSystemInfo.zapret_installed, zapret2_installed: latestSystemInfo.zapret2_installed, byedpi_installed: latestSystemInfo.byedpi_installed, + wdtt_installed: latestSystemInfo.wdtt_installed, + olcrtc_installed: latestSystemInfo.olcrtc_installed, server_inbounds_enabled_count: latestSystemInfo.server_inbounds_enabled_count, }; diff --git a/fe-app-tachyon/src/tachyon/services/tests/runtimeUiState.service.test.ts b/fe-app-tachyon/src/tachyon/services/tests/runtimeUiState.service.test.ts index 47b999a9..c27c9711 100644 --- a/fe-app-tachyon/src/tachyon/services/tests/runtimeUiState.service.test.ts +++ b/fe-app-tachyon/src/tachyon/services/tests/runtimeUiState.service.test.ts @@ -43,6 +43,8 @@ function createUiState( zapret_installed: 1, zapret2_installed: 1, byedpi_installed: 0, + wdtt_installed: 0, + olcrtc_installed: 0, server_inbounds_enabled_count: 0, }, actions: { diff --git a/fe-app-tachyon/src/tachyon/services/tests/uiState.service.test.ts b/fe-app-tachyon/src/tachyon/services/tests/uiState.service.test.ts index 7a0c6f13..a55541bf 100644 --- a/fe-app-tachyon/src/tachyon/services/tests/uiState.service.test.ts +++ b/fe-app-tachyon/src/tachyon/services/tests/uiState.service.test.ts @@ -37,6 +37,8 @@ function createUiState( zapret_installed: 1, zapret2_installed: 0, byedpi_installed: 1, + wdtt_installed: 0, + olcrtc_installed: 0, server_inbounds_enabled_count: 0, ...capabilities, }, diff --git a/fe-app-tachyon/src/tachyon/services/uiState.service.ts b/fe-app-tachyon/src/tachyon/services/uiState.service.ts index e0ee5ae6..ad42fe12 100644 --- a/fe-app-tachyon/src/tachyon/services/uiState.service.ts +++ b/fe-app-tachyon/src/tachyon/services/uiState.service.ts @@ -37,6 +37,14 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] { byedpiInstall: { loading: false }, byedpiRemove: { loading: false }, byedpiRollback: { loading: false }, + wdttCheck: { loading: false }, + wdttInstall: { loading: false }, + wdttRemove: { loading: false }, + wdttRollback: { loading: false }, + olcrtcCheck: { loading: false }, + olcrtcInstall: { loading: false }, + olcrtcRemove: { loading: false }, + olcrtcRollback: { loading: false }, tailscaleCheck: { loading: false }, tailscaleInstall: { loading: false }, tailscaleRemove: { loading: false }, @@ -88,6 +96,8 @@ function applyServiceState(uiState: Tachyon.UiState) { zapret_installed: uiState.capabilities.zapret_installed, zapret2_installed: uiState.capabilities.zapret2_installed, byedpi_installed: uiState.capabilities.byedpi_installed, + wdtt_installed: uiState.capabilities.wdtt_installed, + olcrtc_installed: uiState.capabilities.olcrtc_installed, server_inbounds_enabled_count: uiState.capabilities.server_inbounds_enabled_count, }; diff --git a/fe-app-tachyon/src/tachyon/tabs/diagnostic/diagnostic.store.ts b/fe-app-tachyon/src/tachyon/tabs/diagnostic/diagnostic.store.ts index ce91941c..ba6020cd 100644 --- a/fe-app-tachyon/src/tachyon/tabs/diagnostic/diagnostic.store.ts +++ b/fe-app-tachyon/src/tachyon/tabs/diagnostic/diagnostic.store.ts @@ -102,6 +102,14 @@ export const initialDiagnosticStore: Pick< byedpi_installed: 0, byedpi_backup_version: '', byedpi_backup_time: 0, + wdtt_version: 'loading', + wdtt_installed: 0, + wdtt_backup_version: '', + wdtt_backup_time: 0, + olcrtc_version: 'loading', + olcrtc_installed: 0, + olcrtc_backup_version: '', + olcrtc_backup_time: 0, tailscale_version: 'loading', tailscale_installed: 0, tailscale_backup_version: '', @@ -182,6 +190,14 @@ export const initialDiagnosticStore: Pick< byedpiInstall: { loading: false }, byedpiRemove: { loading: false }, byedpiRollback: { loading: false }, + wdttCheck: { loading: false }, + wdttInstall: { loading: false }, + wdttRemove: { loading: false }, + wdttRollback: { loading: false }, + olcrtcCheck: { loading: false }, + olcrtcInstall: { loading: false }, + olcrtcRemove: { loading: false }, + olcrtcRollback: { loading: false }, tailscaleCheck: { loading: false }, tailscaleInstall: { loading: false }, tailscaleRemove: { loading: false }, diff --git a/fe-app-tachyon/src/tachyon/types.ts b/fe-app-tachyon/src/tachyon/types.ts index d5d3a83f..fde3534f 100644 --- a/fe-app-tachyon/src/tachyon/types.ts +++ b/fe-app-tachyon/src/tachyon/types.ts @@ -69,6 +69,8 @@ export namespace Tachyon { GET_TAILSCALE_PEERS = 'get_tailscale_peers', GET_ZAPRET2_STATUS = 'get_zapret2_status', GET_BYEDPI_STATUS = 'get_byedpi_status', + GET_WDTT_STATUS = 'get_wdtt_status', + GET_OLCRTC_STATUS = 'get_olcrtc_status', CLASH_API = 'clash_api', ENABLE = 'enable', DISABLE = 'disable', @@ -200,7 +202,7 @@ export namespace Tachyon { } export interface ServiceStatus { - serviceType: 'zapret' | 'zapret2' | 'byedpi'; + serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc'; configured: boolean; ready: boolean; conflict: boolean; @@ -275,6 +277,8 @@ export namespace Tachyon { | 'zapret' | 'zapret2' | 'byedpi' + | 'wdtt' + | 'olcrtc' | 'awg' | 'warp' | 'anytls' @@ -321,6 +325,9 @@ export namespace Tachyon { ports?: string[]; protocol?: string[]; excluded_protocol?: string[]; + routed_dns_enabled?: '0' | '1'; + routed_dns_type?: 'udp' | 'dot' | 'doh' | 'doq'; + routed_dns_server?: string | string[]; update_interval?: string; proxy_string?: string; @@ -328,6 +335,31 @@ export namespace Tachyon { nfqws2_opt?: string; byedpi_cmd_opts?: string; cmd_opts?: string; + + warp_flow_uri?: string; + warp_flow_mode?: string; + warp_flow_device_id?: string; + warp_flow_workers?: string; + warp_flow_max_hashes?: string; + warp_flow_mtu?: string; + warp_flow_refresh?: string; + warp_flow_auto_update?: boolean; + warp_flow_block_doh?: boolean; + warp_flow_block_ipv6?: boolean; + warp_flow_subscription_links?: string[]; + warp_flow_community_lists?: string[]; + warp_flow_remote_domain_list?: string[]; + warp_flow_qwdtt_mode?: string; + + olcrtc_provider?: string; + olcrtc_transport?: string; + olcrtc_room_id?: string; + olcrtc_crypto_key?: string; + olcrtc_socks_host?: string; + olcrtc_socks_port?: string; + olcrtc_dns_server?: string; + olcrtc_subscription_links?: string[]; + selector_proxy_links?: string[]; subscription_urls?: string[]; interfaces?: string[]; @@ -610,6 +642,10 @@ export namespace Tachyon { zapret2_installed: 0 | 1; byedpi_version: string; byedpi_installed: 0 | 1; + wdtt_version: string; + wdtt_installed: 0 | 1; + olcrtc_version: string; + olcrtc_installed: 0 | 1; tailscale_version: string; tailscale_installed: 0 | 1; direct_bypass_enabled?: 0 | 1; @@ -639,6 +675,8 @@ export namespace Tachyon { zapret_installed: 0 | 1; zapret2_installed: 0 | 1; byedpi_installed: 0 | 1; + wdtt_installed: 0 | 1; + olcrtc_installed: 0 | 1; server_inbounds_enabled_count: number; } @@ -887,6 +925,54 @@ export namespace Tachyon { status_message: string; } + export interface GetWdttStatus { + installed: 0 | 1; + package_installed: 0 | 1; + provider_available: 0 | 1; + provider_path: string; + version: string; + configured: 0 | 1; + enabled_rule_count: number; + expected_process_count: number; + running_process_count: number; + supervisor_process_count: number; + restart_count: number; + runtime_unstable: 0 | 1; + standalone_service_enabled: 0 | 1; + standalone_service_running: 0 | 1; + listen_address: string; + port_base: number; + outbounds_configured: 0 | 1; + routes_configured: 0 | 1; + ready: 0 | 1; + conflict: 0 | 1; + status_message: string; + } + + export interface GetOlcrtcStatus { + installed: 0 | 1; + package_installed: 0 | 1; + provider_available: 0 | 1; + provider_path: string; + version: string; + configured: 0 | 1; + enabled_rule_count: number; + expected_process_count: number; + running_process_count: number; + supervisor_process_count: number; + restart_count: number; + runtime_unstable: 0 | 1; + standalone_service_enabled: 0 | 1; + standalone_service_running: 0 | 1; + listen_address: string; + port_base: number; + outbounds_configured: 0 | 1; + routes_configured: 0 | 1; + ready: 0 | 1; + conflict: 0 | 1; + status_message: string; + } + export interface ByedpiCheckResult { byedpi_installed: 0 | 1; byedpi_package_installed: 0 | 1; diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index 936149d5..6880b474 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -3407,6 +3407,8 @@ var Tachyon; AvailableMethods2["GET_TAILSCALE_PEERS"] = "get_tailscale_peers"; AvailableMethods2["GET_ZAPRET2_STATUS"] = "get_zapret2_status"; AvailableMethods2["GET_BYEDPI_STATUS"] = "get_byedpi_status"; + AvailableMethods2["GET_WDTT_STATUS"] = "get_wdtt_status"; + AvailableMethods2["GET_OLCRTC_STATUS"] = "get_olcrtc_status"; AvailableMethods2["CLASH_API"] = "clash_api"; AvailableMethods2["ENABLE"] = "enable"; AvailableMethods2["DISABLE"] = "disable"; @@ -3734,6 +3736,18 @@ var TachyonShellMethods = { "/usr/bin/tachyon", { allowNonZeroWithStdout: true } ), + getWdttStatus: async () => callBaseMethod( + Tachyon.AvailableMethods.GET_WDTT_STATUS, + [], + "/usr/bin/tachyon", + { allowNonZeroWithStdout: true } + ), + getOlcrtcStatus: async () => callBaseMethod( + Tachyon.AvailableMethods.GET_OLCRTC_STATUS, + [], + "/usr/bin/tachyon", + { allowNonZeroWithStdout: true } + ), getClashApiProxies: async () => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [ Tachyon.AvailableClashAPIMethods.GET_PROXIES ]), @@ -5045,7 +5059,9 @@ function isConnectionAction(action) { ); } function isServiceAction(action) { - return Boolean(action && ["zapret", "zapret2", "byedpi"].includes(action)); + return Boolean( + action && ["zapret", "zapret2", "byedpi", "wdtt", "olcrtc"].includes(action) + ); } function hasSubscriptionSources(section) { return getSubscriptionSourceCount(section) > 0; @@ -5826,6 +5842,38 @@ async function getDashboardSections(options = {}) { statusMessage: s.status_message }; } + } else if (serviceType === "wdtt") { + const result = await TachyonShellMethods.getWdttStatus(); + if (result.success && result.data) { + const s = result.data; + return { + serviceType: "wdtt", + configured: Boolean(s.configured), + ready: Boolean(s.ready), + conflict: Boolean(s.conflict), + runningProcesses: s.running_process_count, + expectedProcesses: s.expected_process_count, + restartCount: s.restart_count, + unstable: Boolean(s.runtime_unstable), + statusMessage: s.status_message + }; + } + } else if (serviceType === "olcrtc") { + const result = await TachyonShellMethods.getOlcrtcStatus(); + if (result.success && result.data) { + const s = result.data; + return { + serviceType: "olcrtc", + configured: Boolean(s.configured), + ready: Boolean(s.ready), + conflict: Boolean(s.conflict), + runningProcesses: s.running_process_count, + expectedProcesses: s.expected_process_count, + restartCount: s.restart_count, + unstable: Boolean(s.runtime_unstable), + statusMessage: s.status_message + }; + } } } catch (_error) { } @@ -6227,6 +6275,14 @@ var initialDiagnosticStore = { byedpi_installed: 0, byedpi_backup_version: "", byedpi_backup_time: 0, + wdtt_version: "loading", + wdtt_installed: 0, + wdtt_backup_version: "", + wdtt_backup_time: 0, + olcrtc_version: "loading", + olcrtc_installed: 0, + olcrtc_backup_version: "", + olcrtc_backup_time: 0, tailscale_version: "loading", tailscale_installed: 0, tailscale_backup_version: "", @@ -6307,6 +6363,14 @@ var initialDiagnosticStore = { byedpiInstall: { loading: false }, byedpiRemove: { loading: false }, byedpiRollback: { loading: false }, + wdttCheck: { loading: false }, + wdttInstall: { loading: false }, + wdttRemove: { loading: false }, + wdttRollback: { loading: false }, + olcrtcCheck: { loading: false }, + olcrtcInstall: { loading: false }, + olcrtcRemove: { loading: false }, + olcrtcRollback: { loading: false }, tailscaleCheck: { loading: false }, tailscaleInstall: { loading: false }, tailscaleRemove: { loading: false }, @@ -6632,7 +6696,7 @@ function getTachyonLogNotification(line) { return { kind: "error", line }; } const update = line.match( - /\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi)\s+(\S+)/i + /\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi|wdtt|olcrtc)\s+(\S+)/i ); if (!update) { return null; @@ -6697,6 +6761,16 @@ var componentActionKeyMap = { "byedpi:install_version": "byedpiInstall", "byedpi:remove": "byedpiRemove", "byedpi:rollback": "byedpiRollback", + "wdtt:check_update": "wdttCheck", + "wdtt:install": "wdttInstall", + "wdtt:install_version": "wdttInstall", + "wdtt:remove": "wdttRemove", + "wdtt:rollback": "wdttRollback", + "olcrtc:check_update": "olcrtcCheck", + "olcrtc:install": "olcrtcInstall", + "olcrtc:install_version": "olcrtcInstall", + "olcrtc:remove": "olcrtcRemove", + "olcrtc:rollback": "olcrtcRollback", "tailscale:check_update": "tailscaleCheck", "tailscale:install": "tailscaleInstall", "tailscale:install_version": "tailscaleInstall", @@ -6872,6 +6946,14 @@ function getEmptyUpdatesActions() { byedpiInstall: { loading: false }, byedpiRemove: { loading: false }, byedpiRollback: { loading: false }, + wdttCheck: { loading: false }, + wdttInstall: { loading: false }, + wdttRemove: { loading: false }, + wdttRollback: { loading: false }, + olcrtcCheck: { loading: false }, + olcrtcInstall: { loading: false }, + olcrtcRemove: { loading: false }, + olcrtcRollback: { loading: false }, tailscaleCheck: { loading: false }, tailscaleInstall: { loading: false }, tailscaleRemove: { loading: false }, @@ -6913,6 +6995,8 @@ function applyServiceState(uiState) { zapret_installed: uiState.capabilities.zapret_installed, zapret2_installed: uiState.capabilities.zapret2_installed, byedpi_installed: uiState.capabilities.byedpi_installed, + wdtt_installed: uiState.capabilities.wdtt_installed, + olcrtc_installed: uiState.capabilities.olcrtc_installed, server_inbounds_enabled_count: uiState.capabilities.server_inbounds_enabled_count }; nextSystemInfo.sing_box_extended = uiState.capabilities.sing_box_extended; @@ -7146,7 +7230,9 @@ function componentDisplayName(component) { sing_box: "sing-box", zapret: "Zapret", zapret2: "Zapret2", - byedpi: "ByeDPI" + byedpi: "ByeDPI", + wdtt: "WDTT", + olcrtc: "OlcRTC" }; return names[component] || component; } @@ -10767,6 +10853,7 @@ async function runZapret2Check() { const tachyonRuntimeReady = !hasZapret2Rules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses; const unexpectedRuntime = !hasZapret2Rules && (runningProcesses > 0 || supervisorProcesses > 0); const outboundsConfigured = Boolean(data.outbounds_configured); + const routesConfigured = Boolean(data.routes_configured); const standaloneServiceEnabled = Boolean(data.standalone_service_enabled); const standaloneServiceRunning = Boolean(data.standalone_service_running); const standaloneConflict = hasZapret2Rules && standaloneServiceRunning; @@ -10802,6 +10889,11 @@ async function runZapret2Check() { key: outboundsConfigured ? _("Zapret2 sing-box outbound is configured") : _("Zapret2 sing-box outbound is not configured"), value: "" }, + { + state: !hasZapret2Rules || routesConfigured ? "success" : "error", + key: routesConfigured ? _("Zapret2 sing-box route rules are configured") : _("Zapret2 sing-box route rules are not configured"), + value: "" + }, { state: standaloneConflict ? "error" : standaloneAutostartRisk ? "warning" : "success", key: standaloneServiceRunning ? hasZapret2Rules ? _( @@ -10925,6 +11017,10 @@ var UNKNOWN_SYSTEM_INFO = { zapret2_installed: 0, byedpi_version: _("unknown"), byedpi_installed: 0, + wdtt_version: _("unknown"), + wdtt_installed: 0, + olcrtc_version: _("unknown"), + olcrtc_installed: 0, tailscale_version: _("unknown"), tailscale_installed: 0, server_inbounds_enabled_count: -1, @@ -10990,6 +11086,8 @@ async function ensureSystemInfo({ zapret_installed: latestSystemInfo.zapret_installed, zapret2_installed: latestSystemInfo.zapret2_installed, byedpi_installed: latestSystemInfo.byedpi_installed, + wdtt_installed: latestSystemInfo.wdtt_installed, + olcrtc_installed: latestSystemInfo.olcrtc_installed, server_inbounds_enabled_count: latestSystemInfo.server_inbounds_enabled_count }; store.set({ diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js index bc1c971c..685cc876 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js @@ -23,6 +23,8 @@ const ROUTING_ACTIONS = [ "zapret", "zapret2", "byedpi", + "wdtt", + "olcrtc", "awg", "warp", "anytls", @@ -487,6 +489,8 @@ const actionProvidersAvailabilityState = { zapretInstalled: false, zapret2Installed: false, byedpiInstalled: false, + wdttInstalled: false, + olcrtcInstalled: false, torInstalled: false, singBoxExtended: false, }; @@ -669,6 +673,18 @@ function updateActionProvidersAvailabilityState(nextState) { ); } + if (typeof nextState.wdttInstalled !== "undefined") { + actionProvidersAvailabilityState.wdttInstalled = Boolean( + nextState.wdttInstalled, + ); + } + + if (typeof nextState.olcrtcInstalled !== "undefined") { + actionProvidersAvailabilityState.olcrtcInstalled = Boolean( + nextState.olcrtcInstalled, + ); + } + if (typeof nextState.singBoxExtended !== "undefined") { actionProvidersAvailabilityState.singBoxExtended = Boolean( nextState.singBoxExtended, @@ -693,6 +709,8 @@ function updateActionProvidersAvailabilityFromSystemInfo(systemInfo) { zapretInstalled: Boolean(systemInfo.zapret_installed), zapret2Installed: Boolean(systemInfo.zapret2_installed), byedpiInstalled: Boolean(systemInfo.byedpi_installed), + wdttInstalled: Boolean(systemInfo.wdtt_installed), + olcrtcInstalled: Boolean(systemInfo.olcrtc_installed), singBoxExtended: Boolean(systemInfo.sing_box_extended), torInstalled: Boolean(systemInfo.tor_installed), }); @@ -2102,6 +2120,14 @@ function isDownloadThroughTargetSection(section, currentSectionId) { return isByedpiInstalledForUi(); } + if (action === "wdtt") { + return isWdttInstalledForUi(); + } + + if (action === "olcrtc") { + return isOlcrtcInstalledForUi(); + } + return false; } @@ -4534,6 +4560,14 @@ function isByedpiInstalledForUi() { return actionProvidersAvailabilityState.byedpiInstalled; } +function isWdttInstalledForUi() { + return actionProvidersAvailabilityState.wdttInstalled; +} + +function isOlcrtcInstalledForUi() { + return actionProvidersAvailabilityState.olcrtcInstalled; +} + function isSingBoxExtendedForUi() { return actionProvidersAvailabilityState.singBoxExtended; } @@ -4565,6 +4599,10 @@ function getActionOptionLabel(action) { return "Zapret2"; case "byedpi": return "ByeDPI"; + case "wdtt": + return "WDTT"; + case "olcrtc": + return "OlcRTC"; case "awg": return "AmneziaWG"; @@ -4607,6 +4645,14 @@ function getRuleActionDisplayValue(section_id) { return "ByeDPI"; } + if (action === "wdtt") { + return "WDTT"; + } + + if (action === "olcrtc") { + return "OlcRTC"; + } + if (action === "awg") { return "AmneziaWG"; } @@ -4673,6 +4719,12 @@ function populateActionOptionValues(option) { if (isByedpiInstalledForUi()) { option.value("byedpi", getActionOptionLabel("byedpi")); } + if (isWdttInstalledForUi()) { + option.value("wdtt", getActionOptionLabel("wdtt")); + } + if (isOlcrtcInstalledForUi()) { + option.value("olcrtc", getActionOptionLabel("olcrtc")); + } option.value("hosts", getActionOptionLabel("hosts")); } @@ -7819,6 +7871,227 @@ function createSectionContent(section) { }; configureTextareaOption(o, analyzeByedpiStrategy); + // ─── WDTT Fields ────────────────────────────────────────────────────── + + o = section.taboption( + "settings", + form.Value, + "warp_flow_uri", + _("WDTT URI"), + _("WDTT connection URI (wdtt://...)"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.ListValue, + "warp_flow_mode", + _("WDTT Mode"), + ); + o.depends("action", "wdtt"); + o.value("", _("Default")); + o.value("freedom", _("Freedom")); + o.value("captive", _("Captive")); + o.value("camou", _("Camou")); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "warp_flow_device_id", + _("WDTT Device ID"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "warp_flow_workers", + _("WDTT Workers"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "warp_flow_max_hashes", + _("WDTT Max Hashes"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "warp_flow_mtu", + _("WDTT MTU"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "warp_flow_refresh", + _("WDTT Refresh"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Flag, + "warp_flow_auto_update", + _("WDTT Auto Update"), + ); + o.default = "1"; + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Flag, + "warp_flow_block_doh", + _("WDTT Block DoH"), + ); + o.default = "0"; + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Flag, + "warp_flow_block_ipv6", + _("WDTT Block IPv6"), + ); + o.default = "0"; + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.ListValue, + "warp_flow_qwdtt_mode", + _("WDTT QWDTT Mode"), + ); + o.depends("action", "wdtt"); + o.value("", _("Default")); + o.value("client", _("Client")); + o.value("server", _("Server")); + o.modalonly = true; + + o = section.taboption( + "settings", + form.DynamicList, + "warp_flow_subscription_links", + _("WDTT Subscription Links"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.DynamicList, + "warp_flow_community_lists", + _("WDTT Community Lists"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.DynamicList, + "warp_flow_remote_domain_list", + _("WDTT Remote Domain List"), + ); + o.depends("action", "wdtt"); + o.modalonly = true; + + // ─── OlcRTC Fields ──────────────────────────────────────────────────── + + o = section.taboption( + "settings", + form.ListValue, + "olcrtc_provider", + _("OlcRTC Provider"), + ); + o.depends("action", "olcrtc"); + o.value("jitsi", _("Jitsi")); + o.value("wrt", _("WRT")); + o.modalonly = true; + + o = section.taboption( + "settings", + form.ListValue, + "olcrtc_transport", + _("OlcRTC Transport"), + ); + o.depends("action", "olcrtc"); + o.value("whep", _("WHEP")); + o.value("whip", _("WHIP")); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "olcrtc_room_id", + _("OlcRTC Room ID"), + ); + o.depends("action", "olcrtc"); + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "olcrtc_crypto_key", + _("OlcRTC Crypto Key"), + ); + o.depends("action", "olcrtc"); + o.password = true; + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "olcrtc_socks_host", + _("OlcRTC SOCKS Host"), + ); + o.depends("action", "olcrtc"); + o.default = "127.0.0.1"; + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "olcrtc_socks_port", + _("OlcRTC SOCKS Port"), + ); + o.depends("action", "olcrtc"); + o.default = "1082"; + o.modalonly = true; + + o = section.taboption( + "settings", + form.Value, + "olcrtc_dns_server", + _("OlcRTC DNS Server"), + ); + o.depends("action", "olcrtc"); + o.default = "8.8.8.8:53"; + o.modalonly = true; + + o = section.taboption( + "settings", + form.DynamicList, + "olcrtc_subscription_links", + _("OlcRTC Subscription Links"), + ); + o.depends("action", "olcrtc"); + o.modalonly = true; + // ─── Hosts Import Helpers ────────────────────────────────────────────── function parseHostsFile(text, options) { @@ -10333,6 +10606,8 @@ function createSectionContent(section) { o.depends("action", "connection"); o.depends("action", "awg"); o.depends("action", "byedpi"); + o.depends("action", "wdtt"); + o.depends("action", "olcrtc"); o.depends("action", "zapret"); o.depends("action", "zapret2"); o.modalonly = true; @@ -10814,11 +11089,47 @@ function createSectionContent(section) { key: "excluded_ips", label: _("Excluded devices"), description: _( - "Traffic from these IP addresses will never be routed through this section, even if other conditions match.", + "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match.", ), }); dependsOnRoutingAction(excludedIpsOption); + const routedDnsEnabledOption = section.taboption( + "conditions", + form.Flag, + "routed_dns_enabled", + _("Routed DNS"), + _("Resolve matched domains through an encrypted DNS server of this section instead of FakeIP. DNS queries traverse the same proxy as the section's outbound."), + ); + dependsOnRoutingAction(routedDnsEnabledOption); + routedDnsEnabledOption.default = "0"; + routedDnsEnabledOption.rmempty = false; + routedDnsEnabledOption.modalonly = true; + + const routedDnsTypeOption = section.taboption( + "conditions", + form.ListValue, + "routed_dns_type", + _("Routed DNS Protocol"), + _("Protocol for the routed DNS resolver."), + ); + routedDnsTypeOption.depends({ routed_dns_enabled: "1" }); + routedDnsTypeOption.rmempty = false; + routedDnsTypeOption.modalonly = true; + routedDnsTypeOption.value("udp", "UDP"); + routedDnsTypeOption.value("dot", "DNS over TLS"); + routedDnsTypeOption.value("doh", "DNS over HTTPS"); + routedDnsTypeOption.value("doq", "DNS over QUIC"); + + const routedDnsServerOption = addDynamicConditionField(section, { + key: "routed_dns_server", + label: _("Routed DNS Servers"), + description: _( + "DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query).", + ), + }); + routedDnsServerOption.depends({ routed_dns_enabled: "1" }); + const portsOption = addDynamicConditionField(section, { key: "ports", label: _("Ports"), @@ -10914,6 +11225,8 @@ const ACTION_COLORS = { zapret: "#8e44ad", zapret2: "#9b59b6", byedpi: "#d35400", + wdtt: "#27ae60", + olcrtc: "#2980b9", awg: "#008080", warp: "#e67e22", anytls: "#16a085", diff --git a/luci-app-tachyon/po/ru/tachyon.po b/luci-app-tachyon/po/ru/tachyon.po index ebe93976..5f938e23 100644 --- a/luci-app-tachyon/po/ru/tachyon.po +++ b/luci-app-tachyon/po/ru/tachyon.po @@ -839,6 +839,81 @@ msgstr "Стратегия ByeDPI" msgid "ByeDPI strategy cannot be empty" msgstr "Стратегия ByeDPI не может быть пустой" +msgid "WDTT" +msgstr "WDTT" + +msgid "WDTT URI" +msgstr "URI WDTT" + +msgid "WDTT connection URI (wdtt://...)" +msgstr "URI подключения WDTT (wdtt://...)" + +msgid "WDTT Mode" +msgstr "Режим WDTT" + +msgid "WDTT Device ID" +msgstr "ID устройства WDTT" + +msgid "WDTT Workers" +msgstr "Рабочие процессы WDTT" + +msgid "WDTT Max Hashes" +msgstr "Максимум хешей WDTT" + +msgid "WDTT MTU" +msgstr "MTU WDTT" + +msgid "WDTT Refresh" +msgstr "Обновление WDTT" + +msgid "WDTT Auto Update" +msgstr "Автообновление WDTT" + +msgid "WDTT Block DoH" +msgstr "Блокировка DoH в WDTT" + +msgid "WDTT Block IPv6" +msgstr "Блокировка IPv6 в WDTT" + +msgid "WDTT QWDTT Mode" +msgstr "Режим QWDTT" + +msgid "WDTT Subscription Links" +msgstr "Ссылки подписок WDTT" + +msgid "WDTT Community Lists" +msgstr "Списки сообществ WDTT" + +msgid "WDTT Remote Domain List" +msgstr "Удалённый список доменов WDTT" + +msgid "OlcRTC" +msgstr "OlcRTC" + +msgid "OlcRTC Provider" +msgstr "Провайдер OlcRTC" + +msgid "OlcRTC Transport" +msgstr "Транспорт OlcRTC" + +msgid "OlcRTC Room ID" +msgstr "ID комнаты OlcRTC" + +msgid "OlcRTC Crypto Key" +msgstr "Ключ шифрования OlcRTC" + +msgid "OlcRTC SOCKS Host" +msgstr "Хост SOCKS OlcRTC" + +msgid "OlcRTC SOCKS Port" +msgstr "Порт SOCKS OlcRTC" + +msgid "OlcRTC DNS Server" +msgstr "DNS-сервер OlcRTC" + +msgid "OlcRTC Subscription Links" +msgstr "Ссылки подписок OlcRTC" + msgid "Bypasses proxy (SO_MARK 0x08000000)" msgstr "В обход прокси (SO_MARK 0x08000000)" @@ -1445,6 +1520,24 @@ msgstr "DNS через прокси" msgid "DNS through section" msgstr "DNS через секцию" +msgid "Routed DNS" +msgstr "Маршрутизированный DNS" + +msgid "Resolve matched domains through an encrypted DNS server of this section instead of FakeIP. DNS queries traverse the same proxy as the section's outbound." +msgstr "Резолвить совпавшие домены через зашифрованный DNS-сервер этой секции вместо FakeIP. DNS-запросы проходят через тот же прокси, что и исходящий трафик секции." + +msgid "Routed DNS Protocol" +msgstr "Протокол маршрутизированного DNS" + +msgid "Protocol for the routed DNS resolver." +msgstr "Протокол для DNS-резолвера маршрутизации." + +msgid "Routed DNS Servers" +msgstr "Серверы маршрутизированного DNS" + +msgid "DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query)." +msgstr "Адреса DNS-серверов для резолвинга доменов в этой секции (напр. 1.1.1.1, https://cloudflare-dns.com/dns-query)." + msgid "DNS Turbo Cache" msgstr "Быстрый DNS-кэш" @@ -5096,8 +5189,8 @@ msgstr "Трафик" msgid "Traffic for this level goes directly." msgstr "Трафик этого уровня идёт напрямую." -msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match." -msgstr "Трафик от этих IP-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают." +msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match." +msgstr "Трафик от этих IP-адресов, CIDR или MAC-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают." msgid "Traffic of specified protocols (e.g. BitTorrent, QUIC, STUN) will bypass this section." msgstr "Трафик указанных протоколов (например BitTorrent, QUIC, STUN) будет обходить эту секцию." diff --git a/luci-app-tachyon/po/templates/tachyon.pot b/luci-app-tachyon/po/templates/tachyon.pot index d156bcce..cac6d39f 100644 --- a/luci-app-tachyon/po/templates/tachyon.pot +++ b/luci-app-tachyon/po/templates/tachyon.pot @@ -1329,6 +1329,81 @@ msgstr "" msgid "ByeDPI strategy cannot be empty" msgstr "" +msgid "WDTT" +msgstr "" + +msgid "WDTT URI" +msgstr "" + +msgid "WDTT connection URI (wdtt://...)" +msgstr "" + +msgid "WDTT Mode" +msgstr "" + +msgid "WDTT Device ID" +msgstr "" + +msgid "WDTT Workers" +msgstr "" + +msgid "WDTT Max Hashes" +msgstr "" + +msgid "WDTT MTU" +msgstr "" + +msgid "WDTT Refresh" +msgstr "" + +msgid "WDTT Auto Update" +msgstr "" + +msgid "WDTT Block DoH" +msgstr "" + +msgid "WDTT Block IPv6" +msgstr "" + +msgid "WDTT QWDTT Mode" +msgstr "" + +msgid "WDTT Subscription Links" +msgstr "" + +msgid "WDTT Community Lists" +msgstr "" + +msgid "WDTT Remote Domain List" +msgstr "" + +msgid "OlcRTC" +msgstr "" + +msgid "OlcRTC Provider" +msgstr "" + +msgid "OlcRTC Transport" +msgstr "" + +msgid "OlcRTC Room ID" +msgstr "" + +msgid "OlcRTC Crypto Key" +msgstr "" + +msgid "OlcRTC SOCKS Host" +msgstr "" + +msgid "OlcRTC SOCKS Port" +msgstr "" + +msgid "OlcRTC DNS Server" +msgstr "" + +msgid "OlcRTC Subscription Links" +msgstr "" + #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js:14975 #: src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts:170 msgid "Bypasses proxy (SO_MARK 0x08000000)" @@ -2355,6 +2430,24 @@ msgstr "" msgid "DNS through section" msgstr "" +msgid "Routed DNS" +msgstr "" + +msgid "Resolve matched domains through an encrypted DNS server of this section instead of FakeIP. DNS queries traverse the same proxy as the section's outbound." +msgstr "" + +msgid "Routed DNS Protocol" +msgstr "" + +msgid "Protocol for the routed DNS resolver." +msgstr "" + +msgid "Routed DNS Servers" +msgstr "" + +msgid "DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query)." +msgstr "" + #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/settings.js:1615 msgid "DNS Turbo Cache" msgstr "" @@ -8155,7 +8248,7 @@ msgid "Traffic for this level goes directly." msgstr "" #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821 -msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match." +msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match." msgstr "" #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10849 diff --git a/tachyon/files/usr/bin/tachyon b/tachyon/files/usr/bin/tachyon index 460d057d..4abe991b 100755 --- a/tachyon/files/usr/bin/tachyon +++ b/tachyon/files/usr/bin/tachyon @@ -112,6 +112,8 @@ function show_help() { print(" get_zapret_status Get zapret provider and Tachyon-managed nfqws status\n"); print(" get_zapret2_status Get zapret2 provider and Tachyon-managed nfqws2 status\n"); print(" get_byedpi_status Get ByeDPI provider and Tachyon-managed ciadpi status\n"); + print(" get_wdtt_status Get WDTT provider and Tachyon-managed wdtt status\n"); + print(" get_olcrtc_status Get OlcRTC provider and Tachyon-managed olcrtc status\n"); print(" get_system_info Get system information in JSON format\n"); print(" get_server_capabilities Get server UI capabilities in JSON format\n"); print(" get_ui_capabilities Get lightweight UI capabilities in JSON format\n"); @@ -226,6 +228,8 @@ function command_spec(command) { get_zapret_status: [ "diagnostics/runtime.uc", "get-zapret-status", 0 ], get_zapret2_status: [ "diagnostics/runtime.uc", "get-zapret2-status", 0 ], get_byedpi_status: [ "diagnostics/runtime.uc", "get-byedpi-status", 0 ], + get_wdtt_status: [ "diagnostics/runtime.uc", "get-wdtt-status", 0 ], + get_olcrtc_status: [ "diagnostics/runtime.uc", "get-olcrtc-status", 0 ], get_tailscale_status: [ "diagnostics/runtime.uc", "get-tailscale-status", 0 ], get_tailscale_peers: [ "diagnostics/runtime.uc", "get-tailscale-peers", 0 ], tailscale_restart: [ "providers/tailscale/runtime.uc", "start-runtime", 0 ], diff --git a/tachyon/files/usr/lib/components/action.uc b/tachyon/files/usr/lib/components/action.uc index b5e21fc6..7ef022d5 100644 --- a/tachyon/files/usr/lib/components/action.uc +++ b/tachyon/files/usr/lib/components/action.uc @@ -1349,6 +1349,74 @@ function resolve_byedpi_release(arch, tag) { return null; } +function resolve_wdtt_release(arch, tag) { + let asset_ext = is_apk() ? "apk" : "ipk"; + let release_series = trim(helper_output("openwrt-release-series", [ "/etc/openwrt_release" ])); + let releases_json = (tag != null && tag != "") ? + fetch_github_release_by_tag_json("ret_dev", "wdtt", tag) : + fetch_github_releases_json("ret_dev", "wdtt", "30"); + if (releases_json != "") { + let resolved = trim(helper_output_input(releases_json, "wdtt-select-asset", [ release_series, asset_ext, arch.candidates ])); + let fields = split(resolved, "\t"); + if (length(fields) >= 4) { + return { + arch: fields[0], + package_name: fields[1], + package_url: fields[2], + release_url: fields[3], + version: extract_arch_package_version(fields[1], fields[0]) + }; + } + } + if (tag != null && tag != "") { + let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH"); + let tag_clean = replace(tag, /^v/, ""); + let pkg_name = "wdtt_" + tag_clean + "_openwrt_" + distrib_arch + "." + asset_ext; + return { + arch: distrib_arch, + package_name: pkg_name, + package_url: "https://github.com/ret_dev/wdtt/releases/download/" + tag + "/" + pkg_name, + release_url: "https://github.com/ret_dev/wdtt/releases/tag/" + tag, + version: tag + }; + } + return null; +} + +function resolve_olcrtc_release(arch, tag) { + let asset_ext = is_apk() ? "apk" : "ipk"; + let release_series = trim(helper_output("openwrt-release-series", [ "/etc/openwrt_release" ])); + let releases_json = (tag != null && tag != "") ? + fetch_github_release_by_tag_json("ret_dev", "olcrtc", tag) : + fetch_github_releases_json("ret_dev", "olcrtc", "30"); + if (releases_json != "") { + let resolved = trim(helper_output_input(releases_json, "olcrtc-select-asset", [ release_series, asset_ext, arch.candidates ])); + let fields = split(resolved, "\t"); + if (length(fields) >= 4) { + return { + arch: fields[0], + package_name: fields[1], + package_url: fields[2], + release_url: fields[3], + version: extract_arch_package_version(fields[1], fields[0]) + }; + } + } + if (tag != null && tag != "") { + let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH"); + let tag_clean = replace(tag, /^v/, ""); + let pkg_name = "olcrtc_" + tag_clean + "_openwrt_" + distrib_arch + "." + asset_ext; + return { + arch: distrib_arch, + package_name: pkg_name, + package_url: "https://github.com/ret_dev/olcrtc/releases/download/" + tag + "/" + pkg_name, + release_url: "https://github.com/ret_dev/olcrtc/releases/tag/" + tag, + version: tag + }; + } + return null; +} + function download_direct_package(release) { let package_file = tmp_dir + "/" + release.package_name; if (!download_with_retry(release.package_url, package_file, release.package_name) || !file_nonempty(package_file)) @@ -1540,6 +1608,86 @@ function install_byedpi(action, target_tag) { action_success("byedpi", action, "ByeDPI package has been installed", current_version, pkg.version, 1, "latest", release.release_url || ""); } +function install_wdtt(action, target_tag) { + init_tmp_dir() || action_fail("wdtt", action, "Failed to create temporary directory"); + let arch = resolve_arch_candidates(); + if (arch == null) + action_fail("wdtt", action, "Failed to detect package architecture"); + let release = null; + retry_resolve("Resolving WDTT package", function() { + release = resolve_wdtt_release(arch, target_tag); + return release != null; + }); + if (release == null) + action_fail("wdtt", action, "Failed to resolve WDTT package for this router architecture"); + + let runtime_module = LIB_DIR + "/providers/wdtt/runtime.uc"; + let installed = provider_installed(runtime_module); + let current_version = provider_package_version(runtime_module); + if (action == "check_update") { + if (!installed) + action_success("wdtt", action, "WDTT is not installed", current_version, release.version, 0, "", release.release_url || ""); + check_success("wdtt", current_version, release.version, release.release_url || ""); + } + + let pkg = download_direct_package(release); + if (pkg == null) + action_fail("wdtt", action, "Failed to download WDTT package"); + + run_logged("Updating package lists before WDTT package installation", pkg_list_update_command()); + + if (!run_logged("Installing WDTT package " + pkg.name, pkg_install_files_command([ pkg.file ]))) + action_fail("wdtt", action, "Failed to install WDTT package", current_version, pkg.version); + + disable_standalone_service("wdtt"); + restart_tachyon_after_successful_change(); + clear_version_caches(); + current_version = provider_package_version(runtime_module); + if (current_version == "") + current_version = pkg.version || "unknown"; + action_success("wdtt", action, "WDTT package has been installed", current_version, pkg.version, 1, "latest", release.release_url || ""); +} + +function install_olcrtc(action, target_tag) { + init_tmp_dir() || action_fail("olcrtc", action, "Failed to create temporary directory"); + let arch = resolve_arch_candidates(); + if (arch == null) + action_fail("olcrtc", action, "Failed to detect package architecture"); + let release = null; + retry_resolve("Resolving OlcRTC package", function() { + release = resolve_olcrtc_release(arch, target_tag); + return release != null; + }); + if (release == null) + action_fail("olcrtc", action, "Failed to resolve OlcRTC package for this router architecture"); + + let runtime_module = LIB_DIR + "/providers/olcrtc/runtime.uc"; + let installed = provider_installed(runtime_module); + let current_version = provider_package_version(runtime_module); + if (action == "check_update") { + if (!installed) + action_success("olcrtc", action, "OlcRTC is not installed", current_version, release.version, 0, "", release.release_url || ""); + check_success("olcrtc", current_version, release.version, release.release_url || ""); + } + + let pkg = download_direct_package(release); + if (pkg == null) + action_fail("olcrtc", action, "Failed to download OlcRTC package"); + + run_logged("Updating package lists before OlcRTC package installation", pkg_list_update_command()); + + if (!run_logged("Installing OlcRTC package " + pkg.name, pkg_install_files_command([ pkg.file ]))) + action_fail("olcrtc", action, "Failed to install OlcRTC package", current_version, pkg.version); + + disable_standalone_service("olcrtc"); + restart_tachyon_after_successful_change(); + clear_version_caches(); + current_version = provider_package_version(runtime_module); + if (current_version == "") + current_version = pkg.version || "unknown"; + action_success("olcrtc", action, "OlcRTC package has been installed", current_version, pkg.version, 1, "latest", release.release_url || ""); +} + function install_tailscale(action) { let component = "tailscale"; let label = "Tailscale"; @@ -3115,6 +3263,26 @@ function rollback_component(component) { restart_tachyon_after_successful_change(); action_success("byedpi", "rollback", "ByeDPI rolled back to " + backup_version, backup_version, "", 1); } + else if (component == "wdtt") { + let backup_bin = bdir + "/wdtt"; + if (!file_exists(backup_bin)) action_fail("wdtt", "rollback", "WDTT backup binary is missing"); + remove_file("/usr/bin/wdtt"); + command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/wdtt" ]); + command_success_from_args([ "chmod", "0755", "/usr/bin/wdtt" ]); + clear_version_caches(); + restart_tachyon_after_successful_change(); + action_success("wdtt", "rollback", "WDTT rolled back to " + backup_version, backup_version, "", 1); + } + else if (component == "olcrtc") { + let backup_bin = bdir + "/olcrtc"; + if (!file_exists(backup_bin)) action_fail("olcrtc", "rollback", "OlcRTC backup binary is missing"); + remove_file("/usr/bin/olcrtc"); + command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/olcrtc" ]); + command_success_from_args([ "chmod", "0755", "/usr/bin/olcrtc" ]); + clear_version_caches(); + restart_tachyon_after_successful_change(); + action_success("olcrtc", "rollback", "OlcRTC rolled back to " + backup_version, backup_version, "", 1); + } else if (component == "zapret") { let backup_bin = bdir + "/nfqws"; if (!file_exists(backup_bin)) action_fail("zapret", "rollback", "Zapret backup binary is missing"); @@ -3380,6 +3548,14 @@ function component_action(component, action, extra) { install_byedpi(action); else if (component == "byedpi" && action == "remove") remove_optional_component("byedpi", "byedpi", "ByeDPI", LIB_DIR + "/providers/byedpi/runtime.uc"); + else if (component == "wdtt" && (action == "check_update" || action == "install")) + install_wdtt(action); + else if (component == "wdtt" && action == "remove") + remove_optional_component("wdtt", "wdtt", "WDTT", LIB_DIR + "/providers/wdtt/runtime.uc"); + else if (component == "olcrtc" && (action == "check_update" || action == "install")) + install_olcrtc(action); + else if (component == "olcrtc" && action == "remove") + remove_optional_component("olcrtc", "olcrtc", "OlcRTC", LIB_DIR + "/providers/olcrtc/runtime.uc"); else if (component == "tailscale" && (action == "check_update" || action == "install")) install_tailscale(action); else if (component == "tailscale" && action == "remove") diff --git a/tachyon/files/usr/lib/components/updater.uc b/tachyon/files/usr/lib/components/updater.uc index 426edefc..b0f58ed9 100644 --- a/tachyon/files/usr/lib/components/updater.uc +++ b/tachyon/files/usr/lib/components/updater.uc @@ -414,7 +414,7 @@ function openwrt_release_series(path) { function updates_arch_package_version(package_name, package_arch) { let version = str_remove_suffix(str_remove_suffix(package_name, ".ipk"), ".apk"); - let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-"]; + let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-", "wdtt_", "wdtt-", "olcrtc_", "olcrtc-"]; for (let prefix in prefixes) { if (str_startswith(version, prefix)) { @@ -1037,6 +1037,78 @@ function byedpi_select_asset(series, asset_ext, arch_candidates) { } } +function wdtt_select_asset(series, asset_ext, arch_candidates) { + let releases = releases_array_or_wrapped(read_stdin_json()); + + for (let pass = 0; pass < 2; pass++) { + if (pass == 0 && as_string(series) == "") + continue; + + for (let release in releases) { + if (type(release) != "object") + continue; + if (release.draft === true || (length(releases) > 1 && release.prerelease === true)) + continue; + if (pass == 0 && length(releases) > 1) { + let tag = as_string(release.tag_name || ""); + let name = as_string(release.name || ""); + if (!str_contains(tag, series) && !str_contains(name, series)) + continue; + } + for (let arch in split(as_string(arch_candidates), " ")) { + if (arch == "") + continue; + for (let asset in array_or_empty(release.assets)) { + if (type(asset) != "object") + continue; + let name = as_string(asset.name || ""); + let url = as_string(asset.browser_download_url || ""); + if (url != "" && release_asset_matches_arch(name, "wdtt", arch, asset_ext)) { + print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n"); + return; + } + } + } + } + } +} + +function olcrtc_select_asset(series, asset_ext, arch_candidates) { + let releases = releases_array_or_wrapped(read_stdin_json()); + + for (let pass = 0; pass < 2; pass++) { + if (pass == 0 && as_string(series) == "") + continue; + + for (let release in releases) { + if (type(release) != "object") + continue; + if (release.draft === true || (length(releases) > 1 && release.prerelease === true)) + continue; + if (pass == 0 && length(releases) > 1) { + let tag = as_string(release.tag_name || ""); + let name = as_string(release.name || ""); + if (!str_contains(tag, series) && !str_contains(name, series)) + continue; + } + for (let arch in split(as_string(arch_candidates), " ")) { + if (arch == "") + continue; + for (let asset in array_or_empty(release.assets)) { + if (type(asset) != "object") + continue; + let name = as_string(asset.name || ""); + let url = as_string(asset.browser_download_url || ""); + if (url != "" && release_asset_matches_arch(name, "olcrtc", arch, asset_ext)) { + print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n"); + return; + } + } + } + } + } +} + function sing_box_extended_release_tag() { for (let release in releases_array_or_wrapped(read_stdin_json())) { if (type(release) != "object") @@ -1444,6 +1516,10 @@ else if (mode == "release-select-arch-suffix-asset") release_select_arch_suffix_asset(ARGV[1], ARGV[2]); else if (mode == "byedpi-select-asset") byedpi_select_asset(ARGV[1], ARGV[2], ARGV[3]); +else if (mode == "wdtt-select-asset") + wdtt_select_asset(ARGV[1], ARGV[2], ARGV[3]); +else if (mode == "olcrtc-select-asset") + olcrtc_select_asset(ARGV[1], ARGV[2], ARGV[3]); else if (mode == "sing-box-extended-release-tag") sing_box_extended_release_tag(); else if (mode == "sing-box-lx-release-tag") diff --git a/tachyon/files/usr/lib/components/updates.uc b/tachyon/files/usr/lib/components/updates.uc index 9f488715..b702685b 100644 --- a/tachyon/files/usr/lib/components/updates.uc +++ b/tachyon/files/usr/lib/components/updates.uc @@ -631,7 +631,7 @@ function normalize_component_name(component) { function valid_component_name(component) { component = normalize_component_name(component); return component == "tachyon" || component == "sing_box" || component == "zapret" || - component == "zapret2" || component == "byedpi" || component == "tailscale"; + component == "zapret2" || component == "byedpi" || component == "wdtt" || component == "olcrtc" || component == "tailscale"; } function settings_component_auto_update_enabled(settings) { @@ -1604,7 +1604,7 @@ function component_update_check_cache() { let results = []; if (enabled) { - for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi" ]) { + for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi", "wdtt", "olcrtc" ]) { let value = read_json_file(component_update_check_cache_path(component)); if (component_update_check_result_cacheable(value)) push(results, value); @@ -2030,6 +2030,10 @@ function automatic_component_check_names() { push(result, "zapret2"); if (module_success([ LIB_DIR + "/providers/byedpi/runtime.uc", "installed" ])) push(result, "byedpi"); + if (module_success([ LIB_DIR + "/providers/wdtt/runtime.uc", "installed" ])) + push(result, "wdtt"); + if (module_success([ LIB_DIR + "/providers/olcrtc/runtime.uc", "installed" ])) + push(result, "olcrtc"); return result; } diff --git a/tachyon/files/usr/lib/config/connections.uc b/tachyon/files/usr/lib/config/connections.uc index 522ab986..e5c0f3c9 100644 --- a/tachyon/files/usr/lib/config/connections.uc +++ b/tachyon/files/usr/lib/config/connections.uc @@ -306,6 +306,34 @@ function is_connections_action(action) { return action == "connection" || is_legacy_connection_action(action); } +function is_wdtt_action(action) { + return as_string(action) == "wdtt"; +} + +function is_olcrtc_action(action) { + return as_string(action) == "olcrtc"; +} + +function wdtt_sections() { + let result = []; + let sections = uci_core.section_objects(CONFIG_NAME, "section"); + for (let section in sections) { + if (bool_option(section, "enabled", true) && option(section, "action", "") == "wdtt") + push(result, section); + } + return result; +} + +function olcrtc_sections() { + let result = []; + let sections = uci_core.section_objects(CONFIG_NAME, "section"); + for (let section in sections) { + if (bool_option(section, "enabled", true) && option(section, "action", "") == "olcrtc") + push(result, section); + } + return result; +} + function normalize_action(action) { action = as_string(action); return is_connections_action(action) ? "connection" : action; @@ -617,6 +645,26 @@ function dscp_value(section) { return list_option_value_from_array(dscp_list(section)); } +function routed_dns_enabled(section) { + return bool_option(section, "routed_dns_enabled", false); +} + +function routed_dns_type(section) { + let t = option(section, "routed_dns_type", "udp"); + if (!contains(["udp", "dot", "doh", "doq"], t)) + t = "udp"; + return t; +} + +function routed_dns_servers(section) { + let servers = list_option(section, "routed_dns_server"); + if (length(servers) == 0) { + let single = option(section, "routed_dns_server", ""); + servers = [single]; + } + return servers; +} + function contains(arr, val) { if (type(arr) != "array") return false; for (let item in arr) { @@ -1552,6 +1600,13 @@ return { subscription_download_targets, subscription_download_target_port, has_dns_matchers, + routed_dns_enabled, + routed_dns_type, + routed_dns_servers, item_index_from_cursor, - cli_delete_section + cli_delete_section, + wdtt_sections, + olcrtc_sections, + is_wdtt_action, + is_olcrtc_action }; diff --git a/tachyon/files/usr/lib/config/validator.uc b/tachyon/files/usr/lib/config/validator.uc index 9a10c0a8..19453c67 100644 --- a/tachyon/files/usr/lib/config/validator.uc +++ b/tachyon/files/usr/lib/config/validator.uc @@ -8,6 +8,8 @@ let subscription_parser_module = null; let zapret_validator_module = null; let zapret2_validator_module = null; let byedpi_validator_module = null; +let wdtt_validator_module = null; +let olcrtc_validator_module = null; let constants_module = null; let core_url = require("core.url"); let core_ip = require("core.ip"); @@ -695,6 +697,18 @@ function byedpi_validator() { return byedpi_validator_module; } +function wdtt_validator() { + if (wdtt_validator_module == null) + wdtt_validator_module = require("providers.wdtt.validator"); + return wdtt_validator_module; +} + +function olcrtc_validator() { + if (olcrtc_validator_module == null) + olcrtc_validator_module = require("providers.olcrtc.validator"); + return olcrtc_validator_module; +} + function runtime_constants() { if (constants_module == null) constants_module = require("core.constants"); @@ -769,13 +783,14 @@ function rule_action(section) { function rule_action_supported(action) { return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp", "anytls", "snell", "mieru", "sudoku", "masque", "openvpn", - "bypass", "block", "dns", "zapret", "zapret2", "byedpi", "hosts" ], as_string(action)); + "bypass", "block", "dns", "zapret", "zapret2", "byedpi", "hosts", + "wdtt", "olcrtc" ], as_string(action)); } function server_routing_section_action_supported(action) { return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp", "anytls", "snell", "mieru", "sudoku", "masque", "openvpn", - "zapret", "zapret2", "byedpi" ], as_string(action)); + "zapret", "zapret2", "byedpi", "wdtt", "olcrtc" ], as_string(action)); } function duration_to_seconds_value(value) { @@ -1367,6 +1382,53 @@ function validate_provider_strategy(kind, section, context) { fail_validation("Invalid ByeDPI strategy for rule '" + name + "': " + result.message); } +function validate_wdtt_section(section, context) { + let name = section_name(section); + let uri = option(section, "warp_flow_uri", ""); + + if (uri == "") { + fail_validation("WDTT rule '" + name + "' has empty URI. Aborted."); + return; + } + + let result = wdtt_validator().parse_wdtt_uri(uri); + if (!result.valid) + fail_validation("Invalid WDTT URI for rule '" + name + "': " + result.message + ". Aborted."); + + let mode = option(section, "warp_flow_mode", ""); + if (mode != "" && !contains(["freedom", "captive", "camou"], mode)) + fail_validation("Invalid WDTT mode '" + mode + "' in rule '" + name + "'. Aborted."); + + let device_id = option(section, "warp_flow_device_id", ""); + if (device_id != "" && !wdtt_validator().validate_device_id(device_id)) + fail_validation("Invalid WDTT device ID '" + device_id + "' in rule '" + name + "'. Aborted."); +} + +function validate_olcrtc_section(section, context) { + let name = section_name(section); + let provider = option(section, "olcrtc_provider", ""); + + if (provider == "") { + fail_validation("OlcRTC rule '" + name + "' has empty provider. Aborted."); + return; + } + + if (!contains(["livekit", "wrt"], provider)) + fail_validation("Invalid OlcRTC provider '" + provider + "' in rule '" + name + "'. Aborted."); + + let transport = option(section, "olcrtc_transport", "whep"); + if (!contains(["whep", "whip"], transport)) + fail_validation("Invalid OlcRTC transport '" + transport + "' in rule '" + name + "'. Aborted."); + + let room_id = option(section, "olcrtc_room_id", ""); + if (room_id == "") + fail_validation("OlcRTC rule '" + name + "' has empty room ID. Aborted."); + + let crypto_key = option(section, "olcrtc_crypto_key", ""); + if (crypto_key == "") + fail_validation("OlcRTC rule '" + name + "' has empty crypto key. Aborted."); +} + function dns_action_has_domain_matchers(section) { for (let key in [ "domain", "domain_suffix", "domain_keyword", "domain_regex" ]) if (option(section, key, "") != "" || option(section, key + "_text", "") != "" || length(list_option(section, key)) > 0) @@ -1467,6 +1529,22 @@ function validate_rule(section, sections, context) { validate_provider_strategy("byedpi", section, context); } + if (action == "wdtt") { + if (!context.wdtt_installed) { + validate_common_rule_references(section, context); + return; + } + validate_wdtt_section(section, context); + } + + if (action == "olcrtc") { + if (!context.olcrtc_installed) { + validate_common_rule_references(section, context); + return; + } + validate_olcrtc_section(section, context); + } + if (connections.is_connections_action(action)) { validate_dashboard_filter(section); @@ -1536,6 +1614,23 @@ function validate_rule(section, sections, context) { for (let value in list_option(section, "domain_suffix")) validate_combined_domain_value(value, name); validate_combined_domain_text_value(option(section, "domain_suffix_text", ""), name); + + if (connections.routed_dns_enabled(section)) { + if (action == "dns" || action == "hosts" || action == "bypass" || action == "block") + fail_validation("Rule '" + name + "' has routed DNS enabled but action '" + action + "' does not support routed DNS. Use a proxy action (connection, zapret, etc.). Aborted."); + let routed_type = connections.routed_dns_type(section); + if (!contains(["udp", "dot", "doh", "doq"], routed_type)) + fail_validation("Rule '" + name + "' has invalid routed DNS type '" + routed_type + "'. Use udp, dot, doh, or doq. Aborted."); + let routed_servers = connections.routed_dns_servers(section); + if (length(routed_servers) == 0) + fail_validation("Rule '" + name + "' has routed DNS enabled but no DNS server specified. Aborted."); + for (let srv in routed_servers) + if (!dns_server_value_valid(srv)) + fail_validation("Rule '" + name + "' has an invalid routed DNS server '" + srv + "'. Aborted."); + if (!connections.has_dns_matchers(section) && length(list_option(section, "fully_routed_ips")) == 0) + fail_validation("Rule '" + name + "' has routed DNS enabled but no domain conditions or forced devices. Routed DNS requires at least one domain matcher. Aborted."); + } + validate_common_rule_references(section, context); } @@ -1992,6 +2087,8 @@ function context_from_runtime() { byedpi_installed: file_executable(constant_value(constants, "BYEDPI_BIN")), zapret_installed: file_executable(constant_value(constants, "ZAPRET_PROVIDER_NFQWS_BIN")), zapret2_installed: file_executable(constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN")), + wdtt_installed: file_executable(constant_value(constants, "WDTT_BIN")), + olcrtc_installed: file_executable(constant_value(constants, "OLCRTC_BIN")), zapret_provider_nfqws_bin: constant_value(constants, "ZAPRET_PROVIDER_NFQWS_BIN"), zapret2_provider_nfqws2_bin: constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN"), zapret_route_mark_base: constant_value(constants, "ZAPRET_ROUTE_MARK_BASE"), diff --git a/tachyon/files/usr/lib/core/constants.uc b/tachyon/files/usr/lib/core/constants.uc index dccf6508..0e354ba2 100644 --- a/tachyon/files/usr/lib/core/constants.uc +++ b/tachyon/files/usr/lib/core/constants.uc @@ -225,6 +225,25 @@ function constants_map() { c.BYEDPI_OPEN_FILES_LIMIT = env("BYEDPI_OPEN_FILES_LIMIT", "4096"); c.BYEDPI_DEFAULT_CMD_OPTS = env("BYEDPI_DEFAULT_CMD_OPTS", "-o 2 --auto=t,r,a,s -d 2"); + c.WDTT_BIN = env("WDTT_BIN", "/usr/bin/wdtt"); + c.WDTT_SERVICE_INIT = env("WDTT_SERVICE_INIT", "/etc/init.d/wdtt"); + c.WDTT_STATE_DIR = env("WDTT_STATE_DIR", "/var/run/tachyon/wdtt"); + c.WDTT_PID_DIR = env("WDTT_PID_DIR", c.WDTT_STATE_DIR + "/pid"); + c.WDTT_LOG_DIR = env("WDTT_LOG_DIR", c.WDTT_STATE_DIR + "/log"); + c.WDTT_LISTEN_ADDRESS = env("WDTT_LISTEN_ADDRESS", "127.0.0.1"); + c.WDTT_PORT_BASE = env("WDTT_PORT_BASE", "1081"); + c.WDTT_RESPAWN_DELAY = env("WDTT_RESPAWN_DELAY", "5"); + c.WDTT_DEFAULT_MODE = env("WDTT_DEFAULT_MODE", "freedom"); + + c.OLCRTC_BIN = env("OLCRTC_BIN", "/usr/bin/olcrtc"); + c.OLCRTC_SERVICE_INIT = env("OLCRTC_SERVICE_INIT", "/etc/init.d/olcrtc"); + c.OLCRTC_STATE_DIR = env("OLCRTC_STATE_DIR", "/var/run/tachyon/olcrtc"); + c.OLCRTC_PID_DIR = env("OLCRTC_PID_DIR", c.OLCRTC_STATE_DIR + "/pid"); + c.OLCRTC_LOG_DIR = env("OLCRTC_LOG_DIR", c.OLCRTC_STATE_DIR + "/log"); + c.OLCRTC_LISTEN_ADDRESS = env("OLCRTC_LISTEN_ADDRESS", "127.0.0.1"); + c.OLCRTC_PORT_BASE = env("OLCRTC_PORT_BASE", "1082"); + c.OLCRTC_RESPAWN_DELAY = env("OLCRTC_RESPAWN_DELAY", "5"); + return c; } diff --git a/tachyon/files/usr/lib/diagnostics/runtime.uc b/tachyon/files/usr/lib/diagnostics/runtime.uc index 0eb8409d..bb5cc247 100644 --- a/tachyon/files/usr/lib/diagnostics/runtime.uc +++ b/tachyon/files/usr/lib/diagnostics/runtime.uc @@ -57,6 +57,8 @@ const SINGBOX_RUNTIME_UC = LIB_DIR + "/singbox/runtime.uc"; const ZAPRET_RUNTIME_UC = LIB_DIR + "/providers/zapret/runtime.uc"; const ZAPRET2_RUNTIME_UC = LIB_DIR + "/providers/zapret2/runtime.uc"; const BYEDPI_RUNTIME_UC = LIB_DIR + "/providers/byedpi/runtime.uc"; +const WDTT_RUNTIME_UC = LIB_DIR + "/providers/wdtt/runtime.uc"; +const OLCRTC_RUNTIME_UC = LIB_DIR + "/providers/olcrtc/runtime.uc"; const TAILSCALE_RUNTIME_UC = LIB_DIR + "/providers/tailscale/runtime.uc"; const ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/validator.uc"; const ZAPRET2_VALIDATOR_UC = LIB_DIR + "/providers/zapret2/validator.uc"; @@ -2658,6 +2660,37 @@ function run_doctor_checks_impl(repair) { } } + // 4b2. Dnsmasq dns_redirect — when dhcp.@dnsmasq[0].dns_redirect='1', + // firewall4 creates DNAT rules that redirect external DNS queries to the + // router's :53. If Tachyon TProxy has already marked those packets, the + // DNAT rewrites destination to 192.168.1.1:53 and sing-box hijack-dns + // creates transparent sockets that collide with dnsmasq. + if (!agh_primary) { + let dns_redirect = uci_core.get("dhcp.@dnsmasq[0].dns_redirect"); + if (dns_redirect != "1") { + doc_check("✅", "dnsmasq dns_redirect", "disabled (OK)", ""); + } else { + issues++; + if (!DOCTOR_REPAIR_MODE) { + doc_plan("uci set dhcp.@dnsmasq[0].dns_redirect='0' + dnsmasq restart"); + doc_check("⚠️", "dnsmasq dns_redirect", "enabled (causes DNAT → port 53 collision with sing-box)", + "→ WILL FIX (doctor --fix): отключение dns_redirect"); + } else { + doc_set("dhcp.@dnsmasq[0].dns_redirect", "0"); + doc_commit("dhcp"); + command_status("/etc/init.d/dnsmasq restart >/dev/null 2>&1"); + command_status("sleep 1"); + let dr2 = uci_core.get("dhcp.@dnsmasq[0].dns_redirect"); + if (dr2 != "1") { + doc_check("❌", "dnsmasq dns_redirect", "enabled", "→ FIXED: dns_redirect=0"); + fixed++; + } else { + doc_check("❌", "dnsmasq dns_redirect", "enabled", "→ не удалось отключить dns_redirect"); + } + } + } + } + // 4c. Resolv.conf symlink let resolv_link = ""; // Throws when /etc/resolv.conf is a regular file rather than a symlink, @@ -3354,6 +3387,61 @@ function run_doctor_checks_impl(repair) { } } + // 20b. Port 53 Socket Collision — sing-box hijack-dns on tproxy-in creates + // transparent write-only sockets that collide with dnsmasq on :53 via + // SO_REUSEADDR. Diagnostic: find sing-box UNCONN sockets on :53 with + // non-zero Recv-Q (parasitic write-back sockets that steal UDP packets + // from dnsmasq). + { + let ss_output = command_capture("ss -u -a -e -n 'sport = :53' 2>/dev/null").output; + let parasitic_count = 0; + let collision_detail = ""; + for (let line in split(ss_output, "\n")) { + if (index(line, "UNCONN") < 0) + continue; + if (index(line, "sing-box") < 0 && index(line, "sing_box") < 0) + continue; + let fields = split(trim(line), /[ \t]+/); + if (length(fields) < 5) + continue; + let recv_q = int(fields[1]); + if (recv_q > 0) { + parasitic_count++; + if (collision_detail == "") + collision_detail = fields[3] + " Recv-Q=" + as_string(recv_q); + } + } + if (parasitic_count == 0) { + doc_check("✅", "Port 53 socket collision", "none", ""); + } else { + issues++; + if (!DOCTOR_REPAIR_MODE) { + doc_plan("restart sing-box to clear parasitic :53 sockets"); + doc_check("⚠️", "Port 53 socket collision", as_string(parasitic_count) + " sing-box sockets on :53 (" + collision_detail + ")", + "→ sing-box hijack-dns создаёт прозрачные write-only сокеты, перехватывающие UDP у dnsmasq; WILL FIX: restart sing-box"); + } else { + command_status("/etc/init.d/sing-box restart >/dev/null 2>&1"); + command_status("sleep 2"); + let ss_check = command_capture("ss -u -a -e -n 'sport = :53' 2>/dev/null").output; + let still_parasitic = 0; + for (let line2 in split(ss_check, "\n")) { + if (index(line2, "UNCONN") < 0) continue; + if (index(line2, "sing-box") < 0 && index(line2, "sing_box") < 0) continue; + let f2 = split(trim(line2), /[ \t]+/); + if (length(f2) >= 5 && int(f2[1]) > 0) still_parasitic++; + } + if (still_parasitic == 0) { + doc_check("❌", "Port 53 socket collision", as_string(parasitic_count) + " sing-box sockets on :53", + "→ FIXED: sing-box перезапущен, паразитные сокеты очищены"); + fixed++; + } else { + doc_check("❌", "Port 53 socket collision", "persists after restart", + "→ не удалось устранить — проверьте конфигурацию hijack-dns inbound фильтра"); + } + } + } + } + // 21. Subscription Cache Age — sing-box can route through days-old node // lists while the subscription URL itself is reachable. if (has_sections && cfg.subscription_url) { @@ -5020,6 +5108,10 @@ else if (mode == "get-zapret2-status") exit(module_passthrough(ZAPRET2_RUNTIME_UC, [ "status" ])); else if (mode == "get-byedpi-status") exit(module_passthrough(BYEDPI_RUNTIME_UC, [ "status" ])); +else if (mode == "get-wdtt-status") + exit(module_passthrough(WDTT_RUNTIME_UC, [ "status" ])); +else if (mode == "get-olcrtc-status") + exit(module_passthrough(OLCRTC_RUNTIME_UC, [ "status" ])); else if (mode == "get-tailscale-status") exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "status" ])); else if (mode == "get-tailscale-peers") diff --git a/tachyon/files/usr/lib/nft/apply.uc b/tachyon/files/usr/lib/nft/apply.uc index 7f43a4dc..22c6c505 100644 --- a/tachyon/files/usr/lib/nft/apply.uc +++ b/tachyon/files/usr/lib/nft/apply.uc @@ -584,7 +584,8 @@ function section_action(section) { function action_captures_traffic(action) { return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" || - action == "awg" || action == "warp" || action == "block" || action == "zapret" || action == "zapret2" || action == "byedpi"; + action == "awg" || action == "warp" || action == "block" || action == "zapret" || action == "zapret2" || + action == "byedpi" || action == "wdtt" || action == "olcrtc"; } function section_priority_action(section) { @@ -698,9 +699,18 @@ function nft_excluded_source_match_args(section, family) { return []; let ip_key = family == 6 ? "ip6" : "ip"; let addrs = []; - for (let ip in excluded) - if (core_ip.ip_family(as_string(ip)) == family) - push(addrs, as_string(ip)); + for (let item in excluded) { + let val = trim(as_string(item)); + if (val == "") continue; + let is_mac = match(val, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null; + if (is_mac) { + for (let res_ip in core_ip.resolve_mac_to_ips(val)) + if (core_ip.ip_family(res_ip) == family) + push(addrs, res_ip); + } else if (core_ip.ip_family(val) == family) { + push(addrs, val); + } + } if (length(addrs) == 0) return []; return [ ip_key, "saddr", "!=", "{ " + join(", ", addrs) + " }" ]; @@ -2369,6 +2379,9 @@ function nft_rule_signature_body(body, section) { body = signature_add_value(body, "rule." + section_name + ".fully_routed_ips", option(section, "fully_routed_ips", "")); body = signature_add_value(body, "rule." + section_name + ".excluded_ips", option(section, "excluded_ips", "")); body = signature_add_value(body, "rule." + section_name + ".excluded_protocol", option(section, "excluded_protocol", "")); + body = signature_add_value(body, "rule." + section_name + ".routed_dns_enabled", bool_option(section, "routed_dns_enabled", false) ? "1" : "0"); + body = signature_add_value(body, "rule." + section_name + ".routed_dns_type", option(section, "routed_dns_type", "")); + body = signature_add_value(body, "rule." + section_name + ".routed_dns_server", option(section, "routed_dns_server", "")); body = signature_add_value(body, "rule." + section_name + ".protocol", option(section, "protocol", "")); let comm_subnets = bool_option(section, "community_subnets", true) ? filter_community_subnet_lists_value(connections.community_lists_value(section)) : ""; body = signature_add_value(body, "rule." + section_name + ".community_subnet_lists", comm_subnets); @@ -2771,6 +2784,15 @@ function source_aware_dns_values(sections, deferred_sections) { } } } + + if (connections.routed_dns_enabled(section)) { + for (let value in nft_csv_values(section_source_ip_values(section))) { + if (!seen[value]) { + seen[value] = true; + push(values, value); + } + } + } } let settings = uci_settings(); diff --git a/tachyon/files/usr/lib/providers/olcrtc/common.uc b/tachyon/files/usr/lib/providers/olcrtc/common.uc new file mode 100644 index 00000000..5c22269c --- /dev/null +++ b/tachyon/files/usr/lib/providers/olcrtc/common.uc @@ -0,0 +1,32 @@ +let constants = require("core.constants"); + +function config(ctx) { + let env_get = function(key, fallback) { + let val = getenv(key); + return val != null ? val : fallback; + }; + + return { + kind: "olcrtc", + action: "olcrtc", + config_path: env_get("TACHYON_OLCRTC_CONFIG", "/etc/olcrtc/client.yaml"), + service_init: env_get("TACHYON_OLCRTC_SERVICE_INIT", "/etc/init.d/olcrtc"), + binary: env_get("TACHYON_OLCRTC_BIN", "/usr/bin/olcrtc"), + default_socks_host: "127.0.0.1", + default_socks_port: "1080", + default_dns: "8.8.8.8:53", + default_provider: "jitsi", + default_transport: "datachannel", + package_name: "olcrtc", + runtime_path: ctx.lib_dir + "/providers/olcrtc/runtime.uc", + config_name: "olcrtc", + status_label: "olcrtc", + check_prefix: "olcrtc" + }; +} + +function validator() { + return require("providers.olcrtc.validator"); +} + +return { config, validator }; diff --git a/tachyon/files/usr/lib/providers/olcrtc/runtime.uc b/tachyon/files/usr/lib/providers/olcrtc/runtime.uc new file mode 100644 index 00000000..0bd67e84 --- /dev/null +++ b/tachyon/files/usr/lib/providers/olcrtc/runtime.uc @@ -0,0 +1,425 @@ +#!/usr/bin/env ucode + +let fs = require("fs"); +let common = require("core.common"); +let uci_core = require("core.uci"); +let connections = require("config.connections"); + +let as_string = common.as_string; +let bool_value = common.bool_value; +let object_or_empty = common.object_or_empty; + +const MODE = as_string(getenv("TACHYON_CONFIG_NAME")) || "tachyon"; +const LIB_DIR = as_string(getenv("TACHYON_LIB_DIR")) || "/usr/lib/tachyon"; +const CONFIG_NAME = MODE; + +let cfg = require("providers.olcrtc.common").config({ lib_dir: LIB_DIR }); +let olcrtc_validator = require("providers.olcrtc.validator"); + +function log_message(msg, level) { + level = level || "info"; + printf("%s\n", msg); + system("logger -t " + MODE + " -p " + (level == "fatal" ? "user.err" : level == "warn" ? "user.warning" : "user.info") + " '" + msg + "'"); +} + +function command_from_args(args) { + let parts = []; + for (let arg in args) + push(parts, common.shell_quote(as_string(arg))); + return join(" ", parts); +} + +function command_output(cmd) { + let output = trim(popen(cmd, "r") || ""); + return output; +} + +function command_output_from_args(args) { + return command_output(command_from_args(args)); +} + +function command_success_from_args(args) { + return system(command_from_args(args)) == 0; +} + +function option(section, key, fallback) { + let val = section[key]; + if (val == null) + return fallback || ""; + if (type(val) == "array") + return join(" ", val); + return as_string(val); +} + +function bool_option(section, key, fallback) { + let val = section[key]; + if (val == null) + return fallback || false; + return bool_value(val); +} + +function list_option(section, key) { + let val = section[key]; + if (val == null) + return []; + if (type(val) == "array") + return val; + return split(trim(as_string(val)), " \t\n\r"); +} + +function section_name(section) { + return as_string(section[".name"] || ""); +} + +function read_json_file(path) { + let data = fs.readfile(path); + if (data == null || data == "") + return null; + return json(data); +} + +function yaml_quote(value) { + value = as_string(value); + if (value == "") + return "''"; + if (match(value, /^[a-zA-Z0-9_./:-]+$/) != null) + return value; + return "'" + replace(value, "'", "''") + "'"; +} + +function enabled_sections() { + return connections.olcrtc_sections(); +} + +function enabled_rule_count() { + let sections = enabled_sections(); + return length(sections); +} + +function provider_available() { + return fs.stat(cfg.binary) != null; +} + +function service_init_exists() { + return fs.stat(cfg.service_init) != null; +} + +function service_running() { + return command_success_from_args([cfg.service_init, "status"]); +} + +function service_enabled() { + return command_success_from_args([cfg.service_init, "enabled"]); +} + +function package_version() { + let ver = trim(fs.readfile("/etc/olcrtc/.version") || ""); + if (ver != "") + return ver; + return ""; +} + +function fetch_url(url) { + let tmp = "/tmp/tachyon-olcrtc-sub"; + if (command_success_from_args(["curl", "-fsSL", "-o", tmp, "--connect-timeout", "20", "--max-time", "20", url])) + return trim(fs.readfile(tmp) || ""); + if (command_success_from_args(["wget", "-q", "-O", tmp, "--timeout=20", url])) + return trim(fs.readfile(tmp) || ""); + if (command_success_from_args(["uclient-fetch", "-q", "-O", tmp, "--timeout=20", url])) + return trim(fs.readfile(tmp) || ""); + return ""; +} + +function first_uri_from_subscription(data) { + let lines = split(data, "\n\r"); + for (let line in lines) { + line = trim(line); + if (substr(line, 0, 9) == "olcrtc://") + return line; + } + return ""; +} + +function resolve_connection(section) { + let links = list_option(section, "subscription_links"); + let resolved = null; + let used_link = ""; + let subscription_name = ""; + let subscription_refresh = ""; + + for (let link in links) { + link = trim(as_string(link)); + if (link == "") + continue; + + if (substr(link, 0, 9) == "olcrtc://") { + let parsed = olcrtc_validator.parse_uri(link); + if (parsed.valid) { + resolved = parsed; + used_link = link; + break; + } + log_message("Invalid olcrtc:// URI: " + parsed.reason, "warn"); + continue; + } + + if (substr(link, 0, 8) == "https://" || substr(link, 0, 7) == "http://") { + let data = fetch_url(link); + if (data == "") { + log_message("Failed to fetch subscription: " + link, "warn"); + continue; + } + let uri = first_uri_from_subscription(data); + if (uri == "") { + log_message("No olcrtc:// URI found in subscription: " + link, "warn"); + continue; + } + let parsed = olcrtc_validator.parse_uri(uri); + if (parsed.valid) { + resolved = parsed; + used_link = link; + let lines = split(data, "\n\r"); + for (let line in lines) { + line = trim(line); + if (substr(line, 0, 6) == "#name:") + subscription_name = trim(substr(line, 6)); + else if (substr(line, 0, 8) == "#refresh:") + subscription_refresh = trim(substr(line, 8)); + } + break; + } + log_message("Invalid olcrtc URI in subscription: " + parsed.reason, "warn"); + } + } + + if (resolved == null) + return { valid: false, reason: "No valid connection found in subscription links" }; + + let provider = option(section, "provider", ""); + if (provider == "") + provider = resolved.provider; + let transport = option(section, "transport", ""); + if (transport == "") + transport = resolved.transport; + let room_id = option(section, "room_id", ""); + if (room_id == "") + room_id = resolved.room_id; + let crypto_key = option(section, "crypto_key", ""); + if (crypto_key == "") + crypto_key = resolved.crypto_key; + + return { + valid: true, + provider: provider, + transport: transport, + room_id: room_id, + crypto_key: crypto_key, + mimo: resolved.mimo, + payload: resolved.payload, + used_link: used_link, + subscription_name: subscription_name, + subscription_refresh: subscription_refresh + }; +} + +function payload_option_mapping() { + return { + "vp8-fps": "vp8_fps", + "vp8-batch": "vp8_batch", + "fps": "sei_fps", + "batch": "sei_batch", + "video-w": "video_w", + "video-h": "video_h", + "video-fps": "video_fps", + "video-codec": "video_codec", + "video-qr-size": "video_qr_size", + "video-qr-recovery": "video_qr_recovery", + "video-tile-module": "video_tile_module", + "video-tile-rs": "video_tile_rs" + }; +} + +function payload_uci_commands(payload) { + let cmds = []; + if (payload == "" || payload == null) + return cmds; + let mapping = payload_option_mapping(); + let pairs = split(payload, "&"); + for (let pair in pairs) { + let kv = split(pair, "=", 2); + if (length(kv) == 2) { + let key = kv[0]; + let val = kv[1]; + if (mapping[key] != null) + push(cmds, "uci set olcrtc.config." + mapping[key] + "=" + yaml_quote(val)); + } + } + return cmds; +} + +function write_olcrtc_config(section, connection) { + let uci_cmds = [ + "uci set olcrtc.config.carrier=" + yaml_quote(connection.provider), + "uci set olcrtc.config.transport=" + yaml_quote(connection.transport), + "uci set olcrtc.config.room_id=" + yaml_quote(connection.room_id), + "uci set olcrtc.config.key=" + yaml_quote(connection.crypto_key), + "uci set olcrtc.config.socks_host=" + yaml_quote(option(section, "socks_host", cfg.default_socks_host)), + "uci set olcrtc.config.socks_port=" + yaml_quote(option(section, "socks_port", cfg.default_socks_port)), + "uci set olcrtc.config.dns=" + yaml_quote(option(section, "dns_server", cfg.default_dns)) + ]; + + let socks_user = option(section, "socks_user", ""); + if (socks_user != "") + push(uci_cmds, "uci set olcrtc.config.socks_user=" + yaml_quote(socks_user)); + let socks_pass = option(section, "socks_pass", ""); + if (socks_pass != "") + push(uci_cmds, "uci set olcrtc.config.socks_pass=" + yaml_quote(socks_pass)); + + let payload_cmds = payload_uci_commands(connection.payload); + for (let cmd in payload_cmds) + push(uci_cmds, cmd); + + for (let cmd in uci_cmds) + system(cmd); + system("uci commit olcrtc"); + + return true; +} + +function write_olcrtc_yaml_config(section, connection) { + let yaml = "mode: cnc\n"; + yaml += "auth:\n"; + yaml += " provider: " + yaml_quote(connection.provider) + "\n"; + yaml += "room:\n"; + yaml += " id: " + yaml_quote(connection.room_id) + "\n"; + yaml += "crypto:\n"; + yaml += " key: " + yaml_quote(connection.crypto_key) + "\n"; + yaml += "net:\n"; + yaml += " transport: " + yaml_quote(connection.transport) + "\n"; + yaml += " dns: " + yaml_quote(option(section, "dns_server", cfg.default_dns)) + "\n"; + yaml += "socks:\n"; + yaml += " host: " + yaml_quote(option(section, "socks_host", cfg.default_socks_host)) + "\n"; + yaml += " port: " + yaml_quote(option(section, "socks_port", cfg.default_socks_port)) + "\n"; + + let socks_user = option(section, "socks_user", ""); + if (socks_user != "") { + yaml += " username: " + yaml_quote(socks_user) + "\n"; + yaml += " password: " + yaml_quote(option(section, "socks_pass", "")) + "\n"; + } + + let tmp = cfg.config_path + ".tmp"; + let f = fs.open(tmp, "w"); + if (!f) + return false; + f.write(yaml); + f.close(); + return rename(tmp, cfg.config_path) == 0; +} + +function start_runtime() { + let sections = enabled_sections(); + if (length(sections) == 0) { + log_message("No enabled OlcRTC sections found, stopping service"); + command_success_from_args([cfg.service_init, "stop"]); + return true; + } + + if (!provider_available()) { + log_message("OlcRTC provider not available (binary not found: " + cfg.binary + ")", "fatal"); + return false; + } + + if (!service_init_exists()) { + log_message("OlcRTC service init script not found: " + cfg.service_init, "fatal"); + return false; + } + + if (length(sections) > 1) + log_message("Multiple OlcRTC sections found, using first: " + section_name(sections[0]), "warn"); + + let section = sections[0]; + let connection = resolve_connection(section); + if (!connection.valid) { + log_message("OlcRTC rule '" + section_name(section) + "': " + connection.reason, "fatal"); + return false; + } + + let config_type = fs.stat("/etc/olcrtc/client.yaml") != null ? "yaml" : "uci"; + + if (config_type == "yaml") { + if (!write_olcrtc_yaml_config(section, connection)) { + log_message("Failed to write OlcRTC YAML config", "fatal"); + return false; + } + } + else { + if (!write_olcrtc_config(section, connection)) { + log_message("Failed to write OlcRTC UCI config", "fatal"); + return false; + } + } + + if (!command_success_from_args([cfg.service_init, "restart"])) { + log_message("Failed to restart OlcRTC service", "fatal"); + return false; + } + + log_message("OlcRTC provider started successfully (provider=" + connection.provider + ", transport=" + connection.transport + ")"); + return true; +} + +function stop_runtime() { + command_success_from_args([cfg.service_init, "stop"]); + log_message("OlcRTC provider stopped"); + return true; +} + +function status_json() { + let installed = provider_available(); + let running = installed ? service_running() : false; + let enabled = installed ? service_enabled() : false; + let rule_count = enabled_rule_count(); + let version = package_version(); + + write_json({ + installed: installed, + configured: rule_count > 0, + enabled_rule_count: rule_count, + service_enabled: enabled, + service_running: running, + config_path: cfg.config_path, + binary: cfg.binary, + service_init: cfg.service_init, + ready: installed && running && rule_count > 0, + status_message: running ? "olcrtc provider status is normal" : (installed ? "olcrtc service is not running" : "olcrtc is not installed") + }); + return true; +} + +function check_json() { + write_json({ + olcrtc_installed: provider_available(), + olcrtc_config_path: cfg.config_path + }); + return true; +} + +let mode = as_string(ARGV[0]); +if (mode == "start-runtime") + start_runtime(); +else if (mode == "stop-runtime") + stop_runtime(); +else if (mode == "status") + status_json(); +else if (mode == "check") + check_json(); +else if (mode == "installed" || mode == "provider-available") + exit(provider_available() ? 0 : 1); +else if (mode == "package-version") + printf("%s\n", package_version()); +else if (mode == "enabled-rule-count") + printf("%d\n", enabled_rule_count()); +else { + warn("Usage: olcrtc/runtime.uc \n"); + exit(1); +} diff --git a/tachyon/files/usr/lib/providers/olcrtc/validator.uc b/tachyon/files/usr/lib/providers/olcrtc/validator.uc new file mode 100644 index 00000000..0928851a --- /dev/null +++ b/tachyon/files/usr/lib/providers/olcrtc/validator.uc @@ -0,0 +1,203 @@ +let common = require("core.common"); +let fs = require("fs"); + +let as_string = common.as_string; + +const OLCRTC_PROVIDERS = [ "jitsi", "telemost", "wbstream" ]; +const OLCRTC_TRANSPORTS = [ "datachannel", "vp8channel", "seichannel", "videochannel" ]; + +function yaml_quote(value) { + value = as_string(value); + if (value == "") + return "''"; + if (match(value, /^[a-zA-Z0-9_./:-]+$/) != null) + return value; + return "'" + replace(value, "'", "''") + "'"; +} + +function parse_uri(value) { + value = trim(as_string(value)); + if (substr(value, 0, 9) != "olcrtc://") + return { valid: false, reason: "URI must start with olcrtc://" }; + + let rest = substr(value, 9); + + let dollar_idx = rindex(rest, "$"); + let mimo = ""; + if (dollar_idx >= 0) { + mimo = substr(rest, dollar_idx + 1); + rest = substr(rest, 0, dollar_idx); + } + + let hash_idx = rindex(rest, "#"); + let crypto_key = ""; + if (hash_idx >= 0) { + crypto_key = substr(rest, hash_idx + 1); + rest = substr(rest, 0, hash_idx); + } + + let at_idx = index(rest, "@"); + let room_id = ""; + let provider_transport = rest; + if (at_idx >= 0) { + provider_transport = substr(rest, 0, at_idx); + room_id = substr(rest, at_idx + 1); + } + + let q_idx = index(provider_transport, "?"); + let provider = ""; + let transport_with_payload = provider_transport; + if (q_idx >= 0) { + provider = substr(provider_transport, 0, q_idx); + transport_with_payload = substr(provider_transport, q_idx + 1); + } + + let payload = ""; + let transport = transport_with_payload; + let lt_idx = index(transport_with_payload, "<"); + let gt_idx = index(transport_with_payload, ">"); + if (lt_idx >= 0 && gt_idx > lt_idx) { + transport = substr(transport_with_payload, 0, lt_idx); + payload = substr(transport_with_payload, lt_idx + 1, gt_idx - lt_idx - 1); + } + + if (!array_contains(OLCRTC_PROVIDERS, provider)) + return { valid: false, reason: "provider must be one of: " + join(", ", OLCRTC_PROVIDERS) }; + if (!array_contains(OLCRTC_TRANSPORTS, transport)) + return { valid: false, reason: "transport must be one of: " + join(", ", OLCRTC_TRANSPORTS) }; + if (room_id == "") + return { valid: false, reason: "room_id is required" }; + if (crypto_key != "" && length(crypto_key) != 64) + return { valid: false, reason: "crypto_key must be 64 hex characters" }; + + return { + valid: true, + provider: provider, + transport: transport, + room_id: room_id, + crypto_key: crypto_key, + mimo: mimo, + payload: payload + }; +} + +function valid_hex_key(value) { + value = trim(as_string(value)); + if (value == "" || value == null) + return true; + return length(value) == 64 && match(value, /^[0-9a-fA-F]+$/) != null; +} + +function valid_number(value, min, max) { + value = as_string(value); + if (value == "" || value == null) + return true; + let num = int(value); + if (num == null || isNaN(num)) + return false; + return num >= min && num <= max; +} + +function valid_url(value) { + value = trim(as_string(value)); + if (value == "" || value == null) + return true; + return match(value, /^https?:\/\/[^ \t\r\n]+$/) != null; +} + +function valid_host_port(value) { + value = trim(as_string(value)); + if (value == "" || value == null) + return true; + return match(value, /^[^ \t\r\n:]+:[0-9]{1,5}$/) != null; +} + +function list_values(value) { + if (type(value) == "array") + return value; + value = trim(as_string(value)); + if (value == "" || value == null) + return []; + return split(value, " \t\n\r"); +} + +function validation_result(valid, message, options) { + let result = { valid: valid, message: message || "" }; + if (options != null) + for (let key in options) + result[key] = options[key]; + return result; +} + +function validate_section(section) { + let errors = []; + + let provider = as_string(section.provider || ""); + if (provider != "" && !array_contains(OLCRTC_PROVIDERS, provider)) + errors.push("provider must be one of: " + join(", ", OLCRTC_PROVIDERS)); + + let transport = as_string(section.transport || ""); + if (transport != "" && !array_contains(OLCRTC_TRANSPORTS, transport)) + errors.push("transport must be one of: " + join(", ", OLCRTC_TRANSPORTS)); + + let crypto_key = as_string(section.crypto_key || ""); + if (crypto_key != "" && !valid_hex_key(crypto_key)) + errors.push("crypto_key must be 64 hex characters"); + + let socks_port = as_string(section.socks_port || ""); + if (socks_port != "" && !valid_number(socks_port, 1, 65535)) + errors.push("socks_port must be 1-65535"); + + let dns_server = as_string(section.dns_server || ""); + if (dns_server != "" && !valid_host_port(dns_server)) + errors.push("dns_server must be in HOST:PORT format"); + + let subscription_links = list_values(section.subscription_links); + for (let link in subscription_links) { + link = trim(as_string(link)); + if (link == "") + continue; + if (substr(link, 0, 9) == "olcrtc://") { + let parsed = parse_uri(link); + if (!parsed.valid) + errors.push("invalid olcrtc:// URI: " + parsed.reason); + } + else if (!valid_url(link)) + errors.push("subscription link must be olcrtc:// URI or HTTP URL: " + link); + } + + return errors; +} + +function validate_section_json() { + let input = read_stdin(); + let data = object_or_empty(json(input)); + let errors = validate_section(data); + if (length(errors) > 0) + write_json({ valid: false, message: join("; ", errors) }); + else + write_json({ valid: true, message: "" }); + return length(errors) == 0; +} + +function read_stdin() { + let input = fs.open("/dev/stdin", "r"); + if (!input) + return ""; + let data = input.read("all"); + input.close(); + return data == null ? "" : data; +} + +function write_json(value) { + printf("%s\n", sprintf("%J", value)); +} + +return { + validate_section, + validate_section_json, + parse_uri, + valid_hex_key, + providers: OLCRTC_PROVIDERS, + transports: OLCRTC_TRANSPORTS +}; diff --git a/tachyon/files/usr/lib/providers/wdtt/common.uc b/tachyon/files/usr/lib/providers/wdtt/common.uc new file mode 100644 index 00000000..2a024404 --- /dev/null +++ b/tachyon/files/usr/lib/providers/wdtt/common.uc @@ -0,0 +1,41 @@ +let constants = require("core.constants"); + +function config(ctx) { + let env_get = function(key, fallback) { + let val = getenv(key); + return val != null ? val : fallback; + }; + + return { + kind: "wdtt", + action: "wdtt", + config_path: env_get("TACHYON_WDTT_CONFIG", "/etc/config/wdtt"), + qwdtt_config_path: env_get("TACHYON_QWDTT_CONFIG", "/etc/qwdtt/config.json"), + service_init: env_get("TACHYON_WDTT_SERVICE_INIT", "/etc/init.d/wdtt-client"), + qwdtt_service_init: env_get("TACHYON_QWDTT_SERVICE_INIT", "/etc/init.d/qwdtt"), + binary: env_get("TACHYON_QWDTT_BIN", "/usr/bin/qwdtt-client"), + genlists_bin: env_get("TACHYON_WDTT_GENLISTS_BIN", "/usr/sbin/wdtt-genlists"), + resolve_bin: env_get("TACHYON_WDTT_RESOLVE_BIN", "/usr/sbin/wdtt-resolve"), + hashes_dir: env_get("TACHYON_WDTT_HASHES_DIR", "/etc/wdtt"), + captcha_token_default: "/var/run/qwdtt/captcha.token", + default_peer: "YOUR_SERVER:56000", + default_workers_qwdtt: "9", + default_workers_wdtt: "36", + default_max_hashes: "4", + default_mode: "selective", + default_mtu: "1280", + default_refresh: "15m", + default_socks_port_base: 1080, + package_name: "wdtt", + runtime_path: ctx.lib_dir + "/providers/wdtt/runtime.uc", + config_name: "tachyon", + status_label: "wdtt", + check_prefix: "wdtt" + }; +} + +function validator() { + return require("providers.wdtt.validator"); +} + +return { config, validator }; diff --git a/tachyon/files/usr/lib/providers/wdtt/runtime.uc b/tachyon/files/usr/lib/providers/wdtt/runtime.uc new file mode 100644 index 00000000..c3acc364 --- /dev/null +++ b/tachyon/files/usr/lib/providers/wdtt/runtime.uc @@ -0,0 +1,607 @@ +#!/usr/bin/env ucode + +let fs = require("fs"); +let common = require("core.common"); +let uci_core = require("core.uci"); +let connections = require("config.connections"); + +let as_string = common.as_string; +let bool_value = common.bool_value; +let object_or_empty = common.object_or_empty; + +const MODE = as_string(getenv("TACHYON_CONFIG_NAME")) || "tachyon"; +const LIB_DIR = as_string(getenv("TACHYON_LIB_DIR")) || "/usr/lib/tachyon"; +const CONFIG_NAME = MODE; + +let cfg = require("providers.wdtt.common").config({ lib_dir: LIB_DIR }); +let wdtt_validator = require("providers.wdtt.validator"); + +function log_message(msg, level) { + level = level || "info"; + printf("%s\n", msg); + system("logger -t " + MODE + " -p " + (level == "fatal" ? "user.err" : level == "warn" ? "user.warning" : "user.info") + " '" + msg + "'"); +} + +function command_from_args(args) { + let parts = []; + for (let arg in args) + push(parts, common.shell_quote(as_string(arg))); + return join(" ", parts); +} + +function command_output(cmd) { + let output = trim(popen(cmd, "r") || ""); + return output; +} + +function command_output_from_args(args) { + return command_output(command_from_args(args)); +} + +function command_success_from_args(args) { + return system(command_from_args(args)) == 0; +} + +function option(section, key, fallback) { + let val = section[key]; + if (val == null) + return fallback || ""; + if (type(val) == "array") + return join(" ", val); + return as_string(val); +} + +function bool_option(section, key, fallback) { + let val = section[key]; + if (val == null) + return fallback || false; + return bool_value(val); +} + +function list_option(section, key) { + let val = section[key]; + if (val == null) + return []; + if (type(val) == "array") + return val; + return split(trim(as_string(val)), " \t\n\r"); +} + +function section_name(section) { + return as_string(section[".name"] || ""); +} + +function read_json_file(path) { + let data = fs.readfile(path); + if (data == null || data == "") + return null; + return json(data); +} + +function write_json_file(path, data) { + let tmp = path + ".tmp"; + let f = fs.open(tmp, "w"); + if (!f) + return false; + f.write(sprintf("%J", data)); + f.close(); + return rename(tmp, path) == 0; +} + +function lan_mac_address() { + for (let iface in ["br-lan", "lan", "eth0", "eth1"]) { + let mac_path = "/sys/class/net/" + iface + "/address"; + let mac = trim(fs.readfile(mac_path) || ""); + if (mac != "" && mac != "00:00:00:00:00:00") + return mac; + } + return ""; +} + +function enabled_sections() { + return connections.wdtt_sections(); +} + +function enabled_rule_count() { + let sections = enabled_sections(); + return length(sections); +} + +function provider_available() { + return fs.stat(cfg.binary) != null || fs.stat(cfg.config_path) != null; +} + +function qwdtt_installed() { + return fs.stat(cfg.binary) != null; +} + +function active_service_init() { + if (qwdtt_installed()) + return cfg.qwdtt_service_init; + return cfg.service_init; +} + +function service_init_exists() { + return fs.stat(active_service_init()) != null; +} + +function service_running() { + return command_success_from_args([active_service_init(), "status"]); +} + +function service_enabled() { + return command_success_from_args([active_service_init(), "enabled"]); +} + +function package_version() { + if (qwdtt_installed()) { + let ver = trim(fs.readfile("/etc/qwdtt/.version") || ""); + if (ver != "") + return ver; + } + return ""; +} + +function first_http_url(values) { + for (let v in values) { + v = trim(as_string(v)); + if (substr(v, 0, 7) == "http://") + return v; + if (substr(v, 0, 8) == "https://") + return v; + } + return ""; +} + +function first_local_file(values) { + for (let v in values) { + v = trim(as_string(v)); + if (substr(v, 0, 1) == "/") + return v; + } + return ""; +} + +function first_wdtt_link(values) { + for (let v in values) { + v = trim(as_string(v)); + if (substr(v, 0, 7) == "wdtt://") + return v; + } + return ""; +} + +function write_wdtt_hashes_file(hashes_value) { + let dir = cfg.hashes_dir; + system("mkdir -p " + common.shell_quote(dir)); + let path = dir + "/vk-calls.txt"; + let tmp = path + ".tmp"; + let f = fs.open(tmp, "w"); + if (!f) + return false; + if (type(hashes_value) == "array") { + for (let h in hashes_value) + f.write(trim(as_string(h)) + "\n"); + } + else { + let hashes_str = trim(as_string(hashes_value)); + if (hashes_str != "") { + let parts = split(hashes_str, " ,\n\r\t"); + for (let h in parts) { + h = trim(h); + if (h != "") + f.write(h + "\n"); + } + } + } + f.close(); + rename(tmp, path); + return true; +} + +function qwdtt_hashes_list(section) { + let hashes = []; + let links = list_option(section, "subscription_links"); + let wdtt_link = first_wdtt_link(links); + if (wdtt_link != "") { + let parsed = wdtt_validator.parse_wdtt_uri(wdtt_link); + if (parsed.valid && parsed.hashes != "") { + let parts = split(parsed.hashes, ","); + for (let h in parts) { + h = trim(h); + if (h != "") + push(hashes, h); + } + } + } + if (length(hashes) == 0) { + let hashes_file = first_local_file(links); + if (hashes_file != "" && fs.stat(hashes_file) != null) { + let data = trim(fs.readfile(hashes_file) || ""); + if (data != "") { + let lines = split(data, "\n\r"); + for (let line in lines) { + line = trim(line); + if (line != "") + push(hashes, line); + } + } + } + } + return hashes; +} + +function write_qwdtt_json_config(section, name) { + let links = list_option(section, "subscription_links"); + let wdtt_link = first_wdtt_link(links); + let parsed = null; + if (wdtt_link != "") + parsed = wdtt_validator.parse_wdtt_uri(wdtt_link); + + let peer = option(section, "peer", ""); + if (peer == "" && parsed != null && parsed.valid) + peer = parsed.peer; + if (peer == "") + peer = cfg.default_peer; + + let password = option(section, "password", ""); + if (password == "" && parsed != null && parsed.valid) + password = parsed.pass; + + let device_id = option(section, "device_id", ""); + if (device_id == "") + device_id = lan_mac_address(); + + let workers = option(section, "workers", ""); + if (workers == "" && parsed != null && parsed.valid && parsed.workers != "") + workers = parsed.workers; + if (workers == "") + workers = cfg.default_workers_qwdtt; + + let hashes = qwdtt_hashes_list(section); + let hashes_file = first_local_file(links); + if (hashes_file != "" && length(hashes) == 0) + write_wdtt_hashes_file([]); + + let qwdtt_mode = option(section, "qwdtt_mode", "rawtun"); + + let config = { + peer: peer, + password: password, + device_id: device_id, + workers: int(workers) || 9, + dns: option(section, "dns", "yandex"), + obfs: option(section, "obfs", "audio"), + captcha_mode: option(section, "captcha_mode", "auto"), + vk_auth: option(section, "vk_auth", "anonymous"), + vk_anon_path: option(section, "vk_anon_path", "vkcalls"), + no_dtls: bool_option(section, "no_dtls", false), + turn_tcp: bool_option(section, "turn_tcp", false), + tun_name: option(section, "tun_name", "qwdtt0"), + lan_interface: option(section, "lan_iface", "br-lan"), + mode: qwdtt_mode + }; + + if (password != "") + config.vk_creds_file = option(section, "vk_creds_file", ""); + config.captcha_token_file = option(section, "captcha_token_file", cfg.captcha_token_default); + + if (length(hashes) > 0) + config.hashes = hashes; + else if (hashes_file != "") + config.hashes_file = hashes_file; + + if (qwdtt_mode == "socks") { + let socks_port = option(section, "socks_port", ""); + if (socks_port == "" && parsed != null && parsed.valid && parsed.port != "") + socks_port = parsed.port; + if (socks_port == "") + socks_port = as_string(cfg.default_socks_port_base); + config.socks = option(section, "socks_addr", "127.0.0.1:" + socks_port); + } + + let result = write_json_file(cfg.qwdtt_config_path, config); + if (result) + system("chmod 0600 " + common.shell_quote(cfg.qwdtt_config_path)); + + system("uci set " + MODE + ".wdtt_state.enabled=" + (result ? "1" : "0")); + system("uci commit " + MODE); + + return result; +} + +function write_wdtt_uci_config(section) { + let links = list_option(section, "subscription_links"); + let wdtt_link = first_wdtt_link(links); + let parsed = null; + if (wdtt_link != "") + parsed = wdtt_validator.parse_wdtt_uri(wdtt_link); + + let peer = option(section, "peer", ""); + if (peer == "" && parsed != null && parsed.valid) + peer = parsed.peer; + if (peer == "") + peer = cfg.default_peer; + + let password = option(section, "password", ""); + if (password == "" && parsed != null && parsed.valid) + password = parsed.pass; + + let device_id = option(section, "device_id", ""); + if (device_id == "") + device_id = lan_mac_address(); + + let workers = option(section, "workers", ""); + if (workers == "" && parsed != null && parsed.valid && parsed.workers != "") + workers = parsed.workers; + if (workers == "") + workers = cfg.default_workers_wdtt; + + let hashes_url = first_http_url(links); + let hashes_file = first_local_file(links); + + let uci_cmds = [ + "uci set wdtt.settings.enabled='1'", + "uci set wdtt.settings.peer='" + peer + "'", + "uci set wdtt.settings.password='" + password + "'", + "uci set wdtt.settings.device_id='" + device_id + "'", + "uci set wdtt.settings.workers='" + workers + "'", + "uci set wdtt.settings.max_hashes='" + option(section, "max_hashes", cfg.default_max_hashes) + "'", + "uci set wdtt.settings.mode='" + option(section, "mode", cfg.default_mode) + "'", + "uci set wdtt.settings.mtu='" + option(section, "mtu", cfg.default_mtu) + "'", + "uci set wdtt.settings.refresh='" + option(section, "refresh", cfg.default_refresh) + "'", + "uci set wdtt.settings.auto_update='" + (bool_option(section, "auto_update", true) ? "1" : "0") + "'", + "uci set wdtt.settings.block_doh='" + (bool_option(section, "block_doh", false) ? "1" : "0") + "'", + "uci set wdtt.settings.block_ipv6='" + (bool_option(section, "block_ipv6", false) ? "1" : "0") + "'" + ]; + + if (hashes_url != "") + push(uci_cmds, "uci set wdtt.settings.hashes_url='" + hashes_url + "'"); + if (hashes_file != "") + push(uci_cmds, "uci set wdtt.settings.hashes_file='" + hashes_file + "'"); + + let port = ""; + if (parsed != null && parsed.valid && parsed.port != "") + port = parsed.port; + if (port != "") + push(uci_cmds, "uci set wdtt.settings.listen='127.0.0.1:" + port + "'"); + + for (let cmd in uci_cmds) + system(cmd); + system("uci commit wdtt"); + + return true; +} + +function udp_port_in_use(port) { + let hex_port = sprintf("%04X", port); + for (let proc_file in ["/proc/net/udp", "/proc/net/udp6"]) { + let data = fs.readfile(proc_file); + if (data == null) + continue; + let lines = split(data, "\n"); + for (let i = 1; i < length(lines); i++) { + let parts = split(trim(lines[i]), " \t"); + if (length(parts) >= 2) { + let local = parts[1]; + let colon_idx = index(local, ":"); + if (colon_idx >= 0) { + let port_hex = substr(local, colon_idx + 1); + if (port_hex == hex_port) + return true; + } + } + } + } + return false; +} + +function restart_qwdtt_service() { + command_success_from_args([cfg.qwdtt_service_init, "stop"]); + + let wait_ms = 0; + while (wait_ms < 5000) { + if (command_output_from_args(["pidof", "qwdtt-client"]) == "") + break; + system("sleep 0.1"); + wait_ms += 100; + } + if (wait_ms >= 5000) + system("killall -9 qwdtt-client 2>/dev/null || true"); + + let port = cfg.default_socks_port_base; + wait_ms = 0; + while (wait_ms < 10000) { + if (!udp_port_in_use(port)) + break; + system("sleep 0.5"); + wait_ms += 500; + } + + system("sleep 0.5"); + return command_success_from_args([cfg.qwdtt_service_init, "start"]); +} + +function start_runtime() { + let sections = enabled_sections(); + if (length(sections) == 0) { + log_message("No enabled WDTT sections found, disabling provider"); + if (qwdtt_installed()) { + system("uci set " + MODE + ".wdtt_state.enabled='0'"); + system("uci commit " + MODE); + command_success_from_args([cfg.qwdtt_service_init, "stop"]); + } + else { + system("uci set wdtt.settings.enabled='0'"); + system("uci commit wdtt"); + command_success_from_args([cfg.service_init, "stop"]); + } + return true; + } + + if (!provider_available()) { + log_message("WDTT provider not available (no binary or config found)", "fatal"); + return false; + } + + if (!service_init_exists()) { + log_message("WDTT service init script not found: " + active_service_init(), "fatal"); + return false; + } + + if (length(sections) > 1) + log_message("Multiple WDTT sections found, using first: " + section_name(sections[0]), "warn"); + + let section = sections[0]; + + if (qwdtt_installed()) { + if (!write_qwdtt_json_config(section, section_name(section))) { + log_message("Failed to write qwdtt config", "fatal"); + return false; + } + if (!restart_qwdtt_service()) { + log_message("Failed to start qwdtt service", "fatal"); + return false; + } + } + else { + if (!write_wdtt_uci_config(section)) { + log_message("Failed to write wdtt UCI config", "fatal"); + return false; + } + if (!command_success_from_args([cfg.service_init, "restart"])) { + log_message("Failed to restart wdtt-client service", "fatal"); + return false; + } + } + + if (fs.stat(cfg.genlists_bin) != null) + command_output_from_args([cfg.genlists_bin]); + if (fs.stat(cfg.resolve_bin) != null) + command_output_from_args([cfg.resolve_bin]); + + log_message("WDTT provider started successfully"); + return true; +} + +function stop_runtime() { + if (qwdtt_installed()) { + system("uci set " + MODE + ".wdtt_state.enabled='0'"); + system("uci commit " + MODE); + command_success_from_args([cfg.qwdtt_service_init, "stop"]); + } + else { + system("uci set wdtt.settings.enabled='0'"); + system("uci commit wdtt"); + command_success_from_args([cfg.service_init, "stop"]); + } + log_message("WDTT provider stopped"); + return true; +} + +function status_json() { + let installed = provider_available(); + let running = installed ? service_running() : false; + let enabled = installed ? service_enabled() : false; + let rule_count = enabled_rule_count(); + let client = qwdtt_installed() ? "qwdtt" : "wdtt-openwrt"; + let config_path = qwdtt_installed() ? cfg.qwdtt_config_path : cfg.config_path; + let version = package_version(); + + write_json({ + installed: installed, + configured: rule_count > 0, + enabled_rule_count: rule_count, + service_enabled: enabled, + service_running: running, + config_path: config_path, + service_init: active_service_init(), + client: client, + package_version: version, + ready: installed && running && rule_count > 0, + status_message: running ? "wdtt provider status is normal" : (installed ? "wdtt service is not running" : "wdtt is not installed") + }); + return true; +} + +function check_json() { + write_json({ + wdtt_installed: provider_available(), + wdtt_config_path: cfg.config_path, + qwdtt_installed: qwdtt_installed(), + qwdtt_version: qwdtt_installed() ? package_version() : "" + }); + return true; +} + +function captcha_token_file() { + if (qwdtt_installed()) { + let config = read_json_file(cfg.qwdtt_config_path); + if (config != null && config.captcha_token_file != null) + return config.captcha_token_file; + } + return cfg.captcha_token_default; +} + +function wdtt_captcha_status() { + let running = service_running(); + let token_file = captcha_token_file(); + let has_token = fs.stat(token_file) != null; + let token = ""; + if (has_token) + token = trim(fs.readfile(token_file) || ""); + + write_json({ + running: running, + token_file: token_file, + has_token: has_token, + token: token + }); + return true; +} + +function wdtt_captcha_submit(token) { + let token_file = captcha_token_file(); + let dir = ""; + let last_slash = rindex(token_file, "/"); + if (last_slash >= 0) + dir = substr(token_file, 0, last_slash); + if (dir != "") + system("mkdir -p " + common.shell_quote(dir)); + + let f = fs.open(token_file, "w"); + if (!f) { + write_json({ success: false, error: "Failed to write token file" }); + return false; + } + f.write(trim(as_string(token))); + f.close(); + system("chmod 0600 " + common.shell_quote(token_file)); + + write_json({ success: true, path: token_file }); + return true; +} + +let mode = as_string(ARGV[0]); +if (mode == "start-runtime") + start_runtime(); +else if (mode == "stop-runtime") + stop_runtime(); +else if (mode == "status") + status_json(); +else if (mode == "check") + check_json(); +else if (mode == "installed" || mode == "provider-available") + exit(provider_available() ? 0 : 1); +else if (mode == "package-version") + printf("%s\n", package_version()); +else if (mode == "enabled-rule-count") + printf("%d\n", enabled_rule_count()); +else if (mode == "wdtt-captcha-status") + wdtt_captcha_status(); +else if (mode == "wdtt-captcha-submit") + wdtt_captcha_submit(ARGV[1]); +else { + warn("Usage: wdtt/runtime.uc \n"); + exit(1); +} diff --git a/tachyon/files/usr/lib/providers/wdtt/validator.uc b/tachyon/files/usr/lib/providers/wdtt/validator.uc new file mode 100644 index 00000000..89a83f40 --- /dev/null +++ b/tachyon/files/usr/lib/providers/wdtt/validator.uc @@ -0,0 +1,239 @@ +let common = require("core.common"); +let fs = require("fs"); + +let as_string = common.as_string; + +const WDTT_MODES = [ "selective", "lan-all", "full" ]; +const QWDTT_MODES = [ "rawtun", "socks", "vpn" ]; +const WDTT_COMMUNITY_LISTS = [ + "russia-inside", "russia-outside", "ukraine", "telegram", "meta", + "youtube", "discord", "tiktok", "twitter", "hdrezka", "roblox", + "cloudflare", "cloudfront", "google_ai", "google_meet", "google_play", + "hetzner", "ovh", "digitalocean", "anime", "news", "geoblock", + "block", "porn", "hodca" +]; + +function url_decode(value) { + let result = ""; + let len = length(value); + let i = 0; + while (i < len) { + if (value[i] == '%' && i + 2 < len) { + let hex = substr(value, i + 1, 2); + let code = parseInt(hex, 16); + if (code != null && !isNaN(code)) + result += chr(code); + else + result += '%'; + i += 3; + } + else if (value[i] == '+') { + result += ' '; + i += 1; + } + else { + result += value[i]; + i += 1; + } + } + return result; +} + +function parse_query_string(query) { + let params = {}; + if (query == "" || query == null) + return params; + let pairs = split(query, "&"); + for (let pair in pairs) { + let kv = split(pair, "=", 2); + if (length(kv) == 2) + params[kv[0]] = url_decode(kv[1]); + else if (length(kv) == 1) + params[kv[0]] = ""; + } + return params; +} + +function parse_wdtt_uri(value) { + value = trim(as_string(value)); + if (substr(value, 0, 7) != "wdtt://") + return { valid: false, reason: "URI must start with wdtt://" }; + + let rest = substr(value, 7); + let hash_idx = index(rest, "#"); + let label = ""; + if (hash_idx >= 0) { + label = substr(rest, hash_idx + 1); + rest = substr(rest, 0, hash_idx); + } + + let params = parse_query_string(rest); + let peer = params.peer || ""; + let hashes = params.hashes || ""; + let pass = params.pass || ""; + let workers = params.workers || ""; + let port = params.port || ""; + + if (peer == "") + return { valid: false, reason: "wdtt:// URI must contain peer parameter" }; + + return { + valid: true, + peer: peer, + hashes: hashes, + pass: pass, + workers: workers, + port: port, + label: label + }; +} + +function valid_peer(value) { + value = trim(as_string(value)); + if (value == "" || value == null) + return true; + return match(value, /^[^ \t\r\n:]+:[0-9]{1,5}$/) != null; +} + +function valid_url(value) { + value = trim(as_string(value)); + if (value == "" || value == null) + return true; + return match(value, /^https?:\/\/[^ \t\r\n]+$/) != null; +} + +function valid_number(value, min, max) { + value = as_string(value); + if (value == "" || value == null) + return true; + let num = int(value); + if (num == null || isNaN(num)) + return false; + return num >= min && num <= max; +} + +function valid_mode(value) { + if (value == "" || value == null) + return true; + return array_contains(WDTT_MODES, value); +} + +function valid_qwdtt_mode(value) { + if (value == "" || value == null) + return true; + return array_contains(QWDTT_MODES, value); +} + +function valid_community_list(value) { + return array_contains(WDTT_COMMUNITY_LISTS, value); +} + +function list_values(value) { + if (type(value) == "array") + return value; + value = trim(as_string(value)); + if (value == "" || value == null) + return []; + return split(value, " \t\n\r"); +} + +function validation_result(valid, message, options) { + let result = { valid: valid, message: message || "" }; + if (options != null) + for (let key in options) + result[key] = options[key]; + return result; +} + +function validate_section(section) { + let errors = []; + + let peer = as_string(section.peer || ""); + if (peer != "" && !valid_peer(peer)) + errors.push("peer must be in HOST:PORT format"); + + let mode = as_string(section.mode || ""); + if (!valid_mode(mode)) + errors.push("mode must be one of: " + join(", ", WDTT_MODES)); + + let qwdtt_mode = as_string(section.qwdtt_mode || ""); + if (!valid_qwdtt_mode(qwdtt_mode)) + errors.push("qwdtt_mode must be one of: " + join(", ", QWDTT_MODES)); + + let workers = as_string(section.workers || ""); + if (workers != "" && !valid_number(workers, 1, 1024)) + errors.push("workers must be 1-1024"); + + let max_hashes = as_string(section.max_hashes || ""); + if (max_hashes != "" && !valid_number(max_hashes, 1, 64)) + errors.push("max_hashes must be 1-64"); + + let mtu = as_string(section.mtu || ""); + if (mtu != "" && !valid_number(mtu, 576, 1500)) + errors.push("mtu must be 576-1500"); + + let subscription_links = list_values(section.subscription_links); + for (let link in subscription_links) { + link = trim(as_string(link)); + if (link == "") + continue; + if (substr(link, 0, 7) == "wdtt://") { + let parsed = parse_wdtt_uri(link); + if (!parsed.valid) + errors.push("invalid wdtt:// URI: " + parsed.reason); + } + else if (substr(link, 0, 1) == "/") { + /* local file — ok */ + } + else if (!valid_url(link)) + errors.push("subscription link must be HTTP URL, wdtt:// URI, or local file path: " + link); + } + + let community_lists = list_values(section.community_lists); + for (let cl in community_lists) { + if (!valid_community_list(cl)) + errors.push("unknown community list: " + cl); + } + + return errors; +} + +function validate_section_json() { + let input = read_stdin(); + let data = object_or_empty(json(input)); + let errors = validate_section(data); + if (length(errors) > 0) + write_json({ valid: false, message: join("; ", errors) }); + else + write_json({ valid: true, message: "" }); + return length(errors) == 0; +} + +function read_stdin() { + let input = fs.open("/dev/stdin", "r"); + if (!input) + return ""; + let data = input.read("all"); + input.close(); + return data == null ? "" : data; +} + +function write_json(value) { + printf("%s\n", sprintf("%J", value)); +} + +return { + validate_section, + validate_section_json, + parse_wdtt_uri, + url_decode, + valid_peer, + valid_mode, + valid_qwdtt_mode, + valid_url, + valid_community_list, + list_values, + modes: WDTT_MODES, + qwdtt_modes: QWDTT_MODES, + community_lists: WDTT_COMMUNITY_LISTS +}; diff --git a/tachyon/files/usr/lib/service/failover.uc b/tachyon/files/usr/lib/service/failover.uc index 70d0a6a1..36d757e6 100644 --- a/tachyon/files/usr/lib/service/failover.uc +++ b/tachyon/files/usr/lib/service/failover.uc @@ -63,6 +63,7 @@ function candidates() { let action = option(section, "action", ""); if (action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" || action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" || + action == "wdtt" || action == "olcrtc" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "masque" || action == "openvpn") push(result, as_string(section[".name"])); diff --git a/tachyon/files/usr/lib/service/i18n.uc b/tachyon/files/usr/lib/service/i18n.uc index 07aee338..5b38b79f 100644 --- a/tachyon/files/usr/lib/service/i18n.uc +++ b/tachyon/files/usr/lib/service/i18n.uc @@ -11,6 +11,9 @@ let strings = { bootstrap_dns_server: "Bootstrap DNS", dns_strategy: "DNS Strategy", dns_detour_enabled: "DNS Detour", + routed_dns_enabled: "Routed DNS", + routed_dns_type: "Routed DNS Protocol", + routed_dns_server: "Routed DNS Servers", source_network_interfaces: "Inbound Interfaces", enable_output_network_interface: "Bind to WAN", enable_badwan_interface_monitoring: "WAN Monitoring", @@ -561,6 +564,9 @@ let strings = { bootstrap_dns_server: "Bootstrap DNS", dns_strategy: "Стратегия DNS", dns_detour_enabled: "DNS Detour", + routed_dns_enabled: "Маршрутизированный DNS", + routed_dns_type: "Протокол маршрутизированного DNS", + routed_dns_server: "Серверы маршрутизированного DNS", source_network_interfaces: "Входящие интерфейсы", enable_output_network_interface: "Привязка к WAN", enable_badwan_interface_monitoring: "Мониторинг WAN", diff --git a/tachyon/files/usr/lib/service/lifecycle.uc b/tachyon/files/usr/lib/service/lifecycle.uc index b11f83c0..c49b180e 100644 --- a/tachyon/files/usr/lib/service/lifecycle.uc +++ b/tachyon/files/usr/lib/service/lifecycle.uc @@ -120,6 +120,8 @@ const ZAPRET_UC = LIB_DIR + "/providers/zapret/runtime.uc"; const ZAPRET2_UC = LIB_DIR + "/providers/zapret2/runtime.uc"; const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc"; const TAILSCALE_UC = LIB_DIR + "/providers/tailscale/runtime.uc"; +const WDTT_UC = LIB_DIR + "/providers/wdtt/runtime.uc"; +const OLCRTC_UC = LIB_DIR + "/providers/olcrtc/runtime.uc"; const PARENTAL_QUOTA_UC = LIB_DIR + "/service/parental_quota.uc"; const PACKAGES_UC = LIB_DIR + "/core/packages.uc"; const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc"; @@ -935,7 +937,10 @@ function start_main() { release_start_subscription_update_lock(); module_success(ZAPRET_UC, [ "start-runtime" ]); module_success(ZAPRET2_UC, [ "start-runtime" ]); + module_success(WDTT_UC, [ "start-runtime" ]); + module_success(OLCRTC_UC, [ "start-runtime" ]); + module_background(UI_UC, [ "latency-boot-sweep" ]); module_background(UPDATES_UC, [ "list-update" ]); module_background(LIB_DIR + "/service/lifecycle.uc", [ "discover-awg-mtu" ]); return 0; @@ -1021,6 +1026,10 @@ function stop_main() { log_message("Zapret2 stop failed (non-fatal)", "warn"); if (!module_success(BYEDPI_UC, [ "stop-runtime" ])) log_message("ByeDPI stop failed (non-fatal)", "warn"); + if (!module_success(WDTT_UC, [ "stop-runtime" ])) + log_message("WDTT stop failed (non-fatal)", "warn"); + if (!module_success(OLCRTC_UC, [ "stop-runtime" ])) + log_message("OlcRTC stop failed (non-fatal)", "warn"); if (!module_success(TAILSCALE_UC, [ "stop-runtime" ])) log_message("Tailscale stop failed (non-fatal)", "warn"); module_success(PARENTAL_QUOTA_UC, [ "remove-cron" ]); @@ -1209,6 +1218,8 @@ function parse_reload_plan(output) { needs_zapret_restart: 0, needs_zapret2_restart: 0, needs_byedpi_restart: 0, + needs_wdtt_restart: 0, + needs_olcrtc_restart: 0, needs_dnsmasq_configure: 0, needs_dnsmasq_restore: 0, needs_cron_refresh: 0, @@ -1245,6 +1256,8 @@ function reload_actions_summary(plan) { actions = append_reload_action(actions, plan.needs_zapret_restart, "Zapret"); actions = append_reload_action(actions, plan.needs_zapret2_restart, "Zapret2"); actions = append_reload_action(actions, plan.needs_byedpi_restart, "ByeDPI"); + actions = append_reload_action(actions, plan.needs_wdtt_restart, "WDTT"); + actions = append_reload_action(actions, plan.needs_olcrtc_restart, "OlcRTC"); actions = append_reload_action(actions, plan.needs_dnsmasq_configure || plan.needs_dnsmasq_restore, "dnsmasq"); actions = append_reload_action(actions, plan.needs_cron_refresh, "scheduled jobs"); actions = append_reload_action(actions, plan.needs_list_update, "remote lists"); @@ -1435,6 +1448,10 @@ function reload(reason) { module_success(ZAPRET2_UC, [ "stop-runtime" ]); if (plan.needs_byedpi_restart == 1) module_success(BYEDPI_UC, [ "stop-runtime" ]); + if (plan.needs_wdtt_restart == 1) + module_success(WDTT_UC, [ "stop-runtime" ]); + if (plan.needs_olcrtc_restart == 1) + module_success(OLCRTC_UC, [ "stop-runtime" ]); if (plan.needs_hosts_update == 1 && module_success(STATE_UC, [ "has-hosts-list-update-sources" ])) module_success(HOSTS_UC, [ "list-update" ]); @@ -1503,6 +1520,10 @@ function reload(reason) { module_success(ZAPRET_UC, [ "start-runtime" ]); if (plan.needs_zapret2_restart == 1) module_success(ZAPRET2_UC, [ "start-runtime" ]); + if (plan.needs_wdtt_restart == 1) + module_success(WDTT_UC, [ "start-runtime" ]); + if (plan.needs_olcrtc_restart == 1) + module_success(OLCRTC_UC, [ "start-runtime" ]); if (plan.needs_byedpi_restart == 1) module_success(BYEDPI_UC, [ "start-runtime" ]); // Native Tailscale must be up before sing-box so tailnet routes win over diff --git a/tachyon/files/usr/lib/service/reload.uc b/tachyon/files/usr/lib/service/reload.uc index 29148337..b792ed35 100644 --- a/tachyon/files/usr/lib/service/reload.uc +++ b/tachyon/files/usr/lib/service/reload.uc @@ -63,6 +63,8 @@ function emit_reload_plan(previous, current, context) { zapret2_queue: current.zapret2_queue != previous.zapret2_queue, zapret2_runtime: current.zapret2_runtime != previous.zapret2_runtime, byedpi_runtime: current.byedpi_runtime != previous.byedpi_runtime, + wdtt_runtime: current.wdtt_runtime != previous.wdtt_runtime, + olcrtc_runtime: current.olcrtc_runtime != previous.olcrtc_runtime, list: current.list != previous.list, hosts_list: current.hosts_list != previous.hosts_list, cron: current.cron != previous.cron @@ -74,6 +76,8 @@ function emit_reload_plan(previous, current, context) { zapret_restart: false, zapret2_restart: false, byedpi_restart: false, + wdtt_restart: false, + olcrtc_restart: false, dnsmasq_configure: false, dnsmasq_restore: false, cron_refresh: false, @@ -112,6 +116,12 @@ function emit_reload_plan(previous, current, context) { if (changed.byedpi_runtime) needs.byedpi_restart = true; + if (changed.wdtt_runtime) + needs.wdtt_restart = true; + + if (changed.olcrtc_runtime) + needs.olcrtc_restart = true; + if (changed.dnsmasq) { if (!current.dont_touch_dhcp) needs.dnsmasq_configure = true; @@ -175,6 +185,8 @@ function emit_reload_plan(previous, current, context) { emit_bool("changed_zapret2_queue", changed.zapret2_queue); emit_bool("changed_zapret2_runtime", changed.zapret2_runtime); emit_bool("changed_byedpi_runtime", changed.byedpi_runtime); + emit_bool("changed_wdtt_runtime", changed.wdtt_runtime); + emit_bool("changed_olcrtc_runtime", changed.olcrtc_runtime); emit_bool("changed_cron", changed.cron); emit_bool("changed_list", changed.list); emit_bool("changed_hosts_list", changed.hosts_list); @@ -184,6 +196,8 @@ function emit_reload_plan(previous, current, context) { emit_bool("needs_zapret_restart", needs.zapret_restart); emit_bool("needs_zapret2_restart", needs.zapret2_restart); emit_bool("needs_byedpi_restart", needs.byedpi_restart); + emit_bool("needs_wdtt_restart", needs.wdtt_restart); + emit_bool("needs_olcrtc_restart", needs.olcrtc_restart); emit_bool("needs_dnsmasq_configure", needs.dnsmasq_configure); emit_bool("needs_dnsmasq_restore", needs.dnsmasq_restore); emit_bool("needs_cron_refresh", needs.cron_refresh); diff --git a/tachyon/files/usr/lib/service/state.uc b/tachyon/files/usr/lib/service/state.uc index a2da00ef..e365455c 100644 --- a/tachyon/files/usr/lib/service/state.uc +++ b/tachyon/files/usr/lib/service/state.uc @@ -138,6 +138,8 @@ const RELOAD_STATE_FIELDS = [ "zapret2_queue_signature", "zapret2_runtime_signature", "byedpi_runtime_signature", + "wdtt_runtime_signature", + "olcrtc_runtime_signature", "list_signature", "hosts_list_signature", "cron_signature", @@ -1008,6 +1010,9 @@ function nft_runtime_signature_body(settings, sections, schedules, profiles, gue body = signature_add_value(body, "rule." + name + ".fully_routed_ips", option(section, "fully_routed_ips", "")); body = signature_add_value(body, "rule." + name + ".excluded_ips", option(section, "excluded_ips", "")); body = signature_add_value(body, "rule." + name + ".excluded_protocol", option(section, "excluded_protocol", "")); + body = signature_add_value(body, "rule." + name + ".routed_dns_enabled", bool_option(section, "routed_dns_enabled", false) ? "1" : "0"); + body = signature_add_value(body, "rule." + name + ".routed_dns_type", option(section, "routed_dns_type", "")); + body = signature_add_value(body, "rule." + name + ".routed_dns_server", option(section, "routed_dns_server", "")); body = signature_add_value(body, "rule." + name + ".protocol", option(section, "protocol", "")); let comm_subnets = bool_option(section, "community_subnets", true) ? rule_config.filter_community_subnet_lists_value(connections.community_lists_value(section)) : ""; body = signature_add_value(body, "rule." + name + ".community_subnet_lists", comm_subnets); @@ -1781,6 +1786,57 @@ function byedpi_runtime_signature_body(sections) { return body; } +function wdtt_runtime_signature_body(sections) { + let body = ""; + + for (let section in sections) { + section = object_or_empty(section); + if (!section_action_is_enabled(section, "wdtt")) + continue; + + let name = section_name(section); + body = signature_add_value(body, "wdtt." + name + ".peer", option(section, "peer", "")); + body = signature_add_value(body, "wdtt." + name + ".password", option(section, "password", "")); + body = signature_add_value(body, "wdtt." + name + ".device_id", option(section, "device_id", "")); + body = signature_add_value(body, "wdtt." + name + ".mode", option(section, "mode", "")); + body = signature_add_value(body, "wdtt." + name + ".workers", option(section, "workers", "")); + body = signature_add_value(body, "wdtt." + name + ".max_hashes", option(section, "max_hashes", "")); + body = signature_add_value(body, "wdtt." + name + ".mtu", option(section, "mtu", "")); + body = signature_add_value(body, "wdtt." + name + ".refresh", option(section, "refresh", "")); + body = signature_add_value(body, "wdtt." + name + ".auto_update", bool_option(section, "auto_update", true) ? "1" : "0"); + body = signature_add_value(body, "wdtt." + name + ".block_doh", bool_option(section, "block_doh", false) ? "1" : "0"); + body = signature_add_value(body, "wdtt." + name + ".block_ipv6", bool_option(section, "block_ipv6", false) ? "1" : "0"); + body = signature_add_value(body, "wdtt." + name + ".subscription_links", join(" ", list_option(section, "subscription_links"))); + body = signature_add_value(body, "wdtt." + name + ".community_lists", join(" ", list_option(section, "community_lists"))); + body = signature_add_value(body, "wdtt." + name + ".remote_domain_list", join(" ", list_option(section, "remote_domain_list"))); + body = signature_add_value(body, "wdtt." + name + ".qwdtt_mode", option(section, "qwdtt_mode", "")); + } + + return body; +} + +function olcrtc_runtime_signature_body(sections) { + let body = ""; + + for (let section in sections) { + section = object_or_empty(section); + if (!section_action_is_enabled(section, "olcrtc")) + continue; + + let name = section_name(section); + body = signature_add_value(body, "olcrtc." + name + ".provider", option(section, "provider", "")); + body = signature_add_value(body, "olcrtc." + name + ".transport", option(section, "transport", "")); + body = signature_add_value(body, "olcrtc." + name + ".room_id", option(section, "room_id", "")); + body = signature_add_value(body, "olcrtc." + name + ".crypto_key", option(section, "crypto_key", "")); + body = signature_add_value(body, "olcrtc." + name + ".socks_host", option(section, "socks_host", "")); + body = signature_add_value(body, "olcrtc." + name + ".socks_port", option(section, "socks_port", "")); + body = signature_add_value(body, "olcrtc." + name + ".dns_server", option(section, "dns_server", "")); + body = signature_add_value(body, "olcrtc." + name + ".subscription_links", join(" ", list_option(section, "subscription_links"))); + } + + return body; +} + function reload_state_values_from_sources(format, settings, sections, servers, dnsmasq, legacy_dnsmasq_present, mwan3_active_value, schedules, profiles, guest_modes) { return { format: as_string(format), @@ -1793,6 +1849,8 @@ function reload_state_values_from_sources(format, settings, sections, servers, d zapret2_queue_signature: signature_hash(action_queue_signature_body(sections, "zapret2", "zapret2_queue.section")), zapret2_runtime_signature: signature_hash(zapret2_runtime_signature_body(sections)), byedpi_runtime_signature: signature_hash(byedpi_runtime_signature_body(sections)), + wdtt_runtime_signature: signature_hash(wdtt_runtime_signature_body(sections)), + olcrtc_runtime_signature: signature_hash(olcrtc_runtime_signature_body(sections)), list_signature: signature_hash(list_update_signature_body(sections)), hosts_list_signature: signature_hash(hosts_list_signature_body(sections)), cron_signature: signature_hash(cron_signature_body(settings, sections)), @@ -2189,6 +2247,18 @@ else if (mode == "byedpi-runtime-signature-fixture") { let data = fixture_data(ARGV[1]); exit(print_signature_hash(byedpi_runtime_signature_body(fixture_section_list(data))) ? 0 : 1); } +else if (mode == "wdtt-runtime-signature") + exit(print_signature_hash(wdtt_runtime_signature_body(uci_sections("section"))) ? 0 : 1); +else if (mode == "wdtt-runtime-signature-fixture") { + let data = fixture_data(ARGV[1]); + exit(print_signature_hash(wdtt_runtime_signature_body(fixture_section_list(data))) ? 0 : 1); +} +else if (mode == "olcrtc-runtime-signature") + exit(print_signature_hash(olcrtc_runtime_signature_body(uci_sections("section"))) ? 0 : 1); +else if (mode == "olcrtc-runtime-signature-fixture") { + let data = fixture_data(ARGV[1]); + exit(print_signature_hash(olcrtc_runtime_signature_body(fixture_section_list(data))) ? 0 : 1); +} else if (mode == "dont-touch-dhcp") print(dont_touch_dhcp_value(uci_settings()), "\n"); else if (mode == "dont-touch-dhcp-fixture") { diff --git a/tachyon/files/usr/lib/service/telegram.uc b/tachyon/files/usr/lib/service/telegram.uc index a493b4db..686841ef 100644 --- a/tachyon/files/usr/lib/service/telegram.uc +++ b/tachyon/files/usr/lib/service/telegram.uc @@ -1801,6 +1801,8 @@ function view_system_info(token, chat_id, msg_id) { if (info.zapret_installed == "1") text += "🛡 Zapret: " + as_string(info.zapret_version || "?") + "\n"; if (info.zapret2_installed == "1") text += "🛡 Zapret2: " + as_string(info.zapret2_version || "?") + "\n"; if (info.byedpi_installed == "1") text += "🛡 ByeDPI: " + as_string(info.byedpi_version || "?") + "\n"; + if (info.wdtt_installed == "1") text += "🛡 WDTT: " + as_string(info.wdtt_version || "?") + "\n"; + if (info.olcrtc_installed == "1") text += "🛡 OlcRTC: " + as_string(info.olcrtc_version || "?") + "\n"; } catch(e) { text += t("info_read_error") + " " + e; } diff --git a/tachyon/files/usr/lib/service/ui.uc b/tachyon/files/usr/lib/service/ui.uc index 8ed15c00..f6826c55 100644 --- a/tachyon/files/usr/lib/service/ui.uc +++ b/tachyon/files/usr/lib/service/ui.uc @@ -3,6 +3,7 @@ let fs = require("fs"); let common = require("core.common"); let uci_core = require("core.uci"); +let connections = require("config.connections"); let as_string = common.as_string; @@ -1485,6 +1486,65 @@ function action_dir(kind) { return ""; } +function latency_boot_sweep() { + let guard_file = "/var/run/tachyon/boot-sweep-done"; + if (fs.stat(guard_file) != null) + return; + + ensure_dirs(); + + let sections = uci.sections(CONFIG_NAME, "section"); + let has_groups = false; + for (let section in sections) { + if (!bool_option(section, "enabled", true)) + continue; + if (!bool_option(section, "urltest_enabled", false)) + continue; + has_groups = true; + break; + } + if (!has_groups) { + fs.writefile(guard_file, as_string(time())); + return; + } + + let clash_api_check = command_status("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:9090/ 2>/dev/null | grep -q '200\\|404'") == 0; + if (!clash_api_check) { + fs.writefile(guard_file, as_string(time())); + return; + } + + for (let section in sections) { + if (!bool_option(section, "enabled", true)) + continue; + if (!bool_option(section, "urltest_enabled", false)) + continue; + + let section_name = section[".name"]; + let urltest_id = ""; + for (let ut in connections.urltests(section)) { + urltest_id = ut; + break; + } + if (urltest_id == "") + continue; + + let group_tag = section_name + "-urltest-" + urltest_id; + let id = "boot-sweep-" + section_name; + let path = job_state_path_value(LATENCY_ACTION_DIR, id); + if (path == "") + continue; + + write_state_file(path, running_latency_action_value("group", section_name, group_tag, now_seconds())); + let method_plan = latency_clash_method("group"); + command_status(command_from_args([ BIN_PATH, "clash_api", "get_group_latency", group_tag, method_plan.timeout, path ]) + " >/dev/null 2>&1"); + if (fs.stat(path) != null) + write_finished_action_state(path, true, "Boot latency sweep completed", 0); + } + + fs.writefile(guard_file, as_string(time())); +} + function action_ack(kind, job_id_value) { let dir = action_dir(kind); if (dir == "") { @@ -1594,6 +1654,8 @@ else if (mode == "latency-test-status") latency_test_status(ARGV[1]); else if (mode == "action-ack") action_ack(ARGV[1], ARGV[2]); +else if (mode == "latency-boot-sweep") + latency_boot_sweep(); else if (mode == "cleanup-action-dir-fixture") cleanup_dir(ARGV[1]); else { diff --git a/tachyon/files/usr/lib/singbox/generator.uc b/tachyon/files/usr/lib/singbox/generator.uc index ab89d4e7..c90efd9f 100644 --- a/tachyon/files/usr/lib/singbox/generator.uc +++ b/tachyon/files/usr/lib/singbox/generator.uc @@ -619,7 +619,8 @@ function base_config(settings, service_address, runtime_context) { function mixed_proxy_enabled_action(action) { return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" || - action == "awg" || action == "byedpi" || action == "zapret" || action == "zapret2"; + action == "awg" || action == "byedpi" || action == "zapret" || action == "zapret2" || + action == "wdtt" || action == "olcrtc"; } function add_mixed_proxy_for_section(config, section, service_address) { diff --git a/tachyon/files/usr/lib/singbox/generator_outbounds.uc b/tachyon/files/usr/lib/singbox/generator_outbounds.uc index 2a784fe5..1e8123b6 100644 --- a/tachyon/files/usr/lib/singbox/generator_outbounds.uc +++ b/tachyon/files/usr/lib/singbox/generator_outbounds.uc @@ -1802,6 +1802,32 @@ function add_byedpi_outbound(config, section, sections) { }); } +function add_wdtt_outbound(config, section, sections) { + let index = enabled_action_index(sections, section, "wdtt"); + if (index <= 0) + ctx.runtime_generate_unsupported("unable to resolve WDTT index for " + section[".name"]); + push(config.outbounds, { + type: "socks", + tag: outbound_tag(section[".name"]), + server: runtime_constants.WDTT_LISTEN_ADDRESS, + server_port: runtime_constants.WDTT_PORT_BASE + index - 1, + version: "5" + }); +} + +function add_olcrtc_outbound(config, section, sections) { + let index = enabled_action_index(sections, section, "olcrtc"); + if (index <= 0) + ctx.runtime_generate_unsupported("unable to resolve OlcRTC index for " + section[".name"]); + push(config.outbounds, { + type: "socks", + tag: outbound_tag(section[".name"]), + server: runtime_constants.OLCRTC_LISTEN_ADDRESS, + server_port: runtime_constants.OLCRTC_PORT_BASE + index - 1, + version: "5" + }); +} + return { init, reserved_runtime_tag_set, @@ -1836,5 +1862,7 @@ return { add_openvpn_endpoint, add_zapret_outbound, add_zapret2_outbound, - add_byedpi_outbound + add_byedpi_outbound, + add_wdtt_outbound, + add_olcrtc_outbound }; diff --git a/tachyon/files/usr/lib/singbox/generator_routes.uc b/tachyon/files/usr/lib/singbox/generator_routes.uc index f5d69ffc..2f3b9ca2 100644 --- a/tachyon/files/usr/lib/singbox/generator_routes.uc +++ b/tachyon/files/usr/lib/singbox/generator_routes.uc @@ -936,7 +936,14 @@ function domain_ip_list_ruleset_tag(section_name) { function domain_ip_list_ruleset_path(section_name) { let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder; - return folder + "/" + domain_ip_list_ruleset_tag(section_name) + ".json"; + let tag = domain_ip_list_ruleset_tag(section_name); + let tmp_path = folder + "/" + tag + ".json"; + let etc_path = "/etc/tachyon/rulesets/" + tag + ".json"; + if (source_rulesets.has_rules(tmp_path)) + return tmp_path; + if (source_rulesets.has_rules(etc_path)) + return etc_path; + return tmp_path; } function reference_is_local(reference) { @@ -948,7 +955,8 @@ function source_file_exists(path) { } function rebuild_local_domain_ip_list_ruleset(section_name, references, domains_only) { - let ruleset_path = domain_ip_list_ruleset_path(section_name); + let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder; + let ruleset_path = folder + "/" + domain_ip_list_ruleset_tag(section_name) + ".json"; let has_local = false; for (let reference in references) { @@ -1086,9 +1094,15 @@ function push_dns_matcher_rule(config, rule) { } function section_dns_server(section) { - return option(section, "action", "") == "bypass" - ? runtime_constants.DNS_SERVER_TAG - : runtime_constants.FAKEIP_DNS_SERVER_TAG; + if (option(section, "action", "") == "bypass") + return runtime_constants.DNS_SERVER_TAG; + if (connections.routed_dns_enabled(section)) + return runtime_constants.tag(section[".name"], "routed-dns-server"); + return runtime_constants.FAKEIP_DNS_SERVER_TAG; +} + +function routed_dns_server_tag(section_name) { + return runtime_constants.tag(section_name, "routed-dns-server"); } function single_or_array(values) { @@ -1143,6 +1157,32 @@ function add_dns_server_for_section(config, section) { return server_tags; } +function add_routed_dns_server_for_section(config, section) { + if (!connections.routed_dns_enabled(section)) + return; + let section_name = section[".name"]; + let servers = connections.routed_dns_servers(section); + let dns_type = connections.routed_dns_type(section); + let tag_name = routed_dns_server_tag(section_name); + let detour = outbound_tag(section_name); + + for (let i = 0; i < length(servers); i++) { + let s_val = servers[i]; + let server_tag = length(servers) <= 1 + ? tag_name + : runtime_constants.tag(section_name, "routed-dns-server-" + (i + 1)); + let server = runtime_dns.server_from_options( + server_tag, + dns_type, + s_val, + detour + ); + if (server.unsupported) + ctx.runtime_generate_unsupported(server.unsupported); + push(config.dns.servers, server); + } +} + function source_dns_inbound_matcher() { return [ runtime_constants.SOURCE_DNS_INBOUND_TAG ]; } @@ -1369,11 +1409,26 @@ function add_excluded_ips_rule(config, section) { if (length(excluded) == 0) return; + let resolved = []; + for (let item in excluded) { + let val = trim(as_string(item)); + if (val == "") continue; + let is_mac = match(val, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null; + if (is_mac) { + for (let res_ip in core_ip.resolve_mac_to_ips(val)) + push(resolved, res_ip + (index(res_ip, ":") != -1 ? "/128" : "/32")); + } else { + push(resolved, val); + } + } + if (length(resolved) == 0) + return; + let route_rule = { action: "route", inbound: tproxy_inbound_matcher(), outbound: runtime_constants.DIRECT_OUTBOUND_TAG, - source_ip_cidr: single_or_array(excluded) + source_ip_cidr: single_or_array(resolved) }; push(config.route.rules, route_rule); } @@ -1642,6 +1697,10 @@ function add_outbound_for_section(config, section, taken, sections) { ctx.outbounds.add_zapret2_outbound(config, section, sections); else if (action == "byedpi") ctx.outbounds.add_byedpi_outbound(config, section, sections); + else if (action == "wdtt") + ctx.outbounds.add_wdtt_outbound(config, section, sections); + else if (action == "olcrtc") + ctx.outbounds.add_olcrtc_outbound(config, section, sections); else if (action == "bypass") { /* route-only action */ } @@ -1657,6 +1716,9 @@ function add_outbound_for_section(config, section, taken, sections) { else { ctx.runtime_generate_unsupported("unsupported action " + action); } + + if (action != "dns" && action != "hosts" && action != "bypass" && action != "block") + add_routed_dns_server_for_section(config, section); } function reserve_section_outbound_tags(sections, taken) { @@ -1664,6 +1726,7 @@ function reserve_section_outbound_tags(sections, taken) { let action = option(section, "action", ""); if (connections.is_connections_action(action) || action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" || + action == "wdtt" || action == "olcrtc" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "masque" || action == "openvpn") taken[outbound_tag(section[".name"])] = true; @@ -1695,6 +1758,7 @@ function failover_candidate(sections) { let action = option(section, "action", ""); if (connections.is_connections_action(action) || action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" || + action == "wdtt" || action == "olcrtc" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "masque" || action == "openvpn") { push(result, section); diff --git a/tachyon/files/usr/lib/singbox/route.uc b/tachyon/files/usr/lib/singbox/route.uc index b774c2f9..c6386ce9 100644 --- a/tachyon/files/usr/lib/singbox/route.uc +++ b/tachyon/files/usr/lib/singbox/route.uc @@ -72,13 +72,16 @@ function config(settings, runtime) { if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array") for (let inbound in runtime.dns_health_inbounds) push(sniff_inbounds, inbound); + let dns_hijack_inbounds = [ runtime_constants.DNS_INBOUND_TAG ]; + if (type(runtime) == "object" && bool_value(runtime.source_aware_dns)) + push(dns_hijack_inbounds, runtime_constants.SOURCE_DNS_INBOUND_TAG); + if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array") + for (let inbound in runtime.dns_health_inbounds) + push(dns_hijack_inbounds, inbound); let rules = [ { action: "sniff", inbound: sniff_inbounds }, - { action: "hijack-dns", port: 53 }, - { action: "hijack-dns", protocol: "dns" } + { action: "hijack-dns", inbound: dns_hijack_inbounds } ]; - if (type(runtime) == "object" && bool_value(runtime.source_aware_dns)) - push(rules, { action: "hijack-dns", inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ] }); let result = { rules, rule_set: [], @@ -120,7 +123,8 @@ function target(section, outbound_tag_name) { action == "awg" || action == "warp" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "masque" || action == "openvpn" || - action == "byedpi" || action == "zapret" || action == "zapret2") + action == "byedpi" || action == "zapret" || action == "zapret2" || + action == "wdtt" || action == "olcrtc") return { action: "route", outbound: outbound_tag_name }; if (action == "bypass") return { action: "route", outbound: runtime_constants.BYPASS_OUTBOUND_TAG }; @@ -139,6 +143,7 @@ function has_resolve_matchers(rule) { function resolve_rule_for_section(section, route_rule) { let action = option(section, "action", ""); let should_resolve = action == "byedpi" || action == "zapret" || action == "zapret2" || + action == "wdtt" || action == "olcrtc" || (connections.is_connections_action(action) && bool_option(section, "resolve_real_ip_for_routing", false)); diff --git a/tests/nft_apply.sh b/tests/nft_apply.sh index eb38ef57..83ee6876 100644 --- a/tests/nft_apply.sh +++ b/tests/nft_apply.sh @@ -703,6 +703,12 @@ bypass [rule.text_rule.excluded_protocol] +[rule.text_rule.routed_dns_enabled] +0 +[rule.text_rule.routed_dns_type] + +[rule.text_rule.routed_dns_server] + [rule.text_rule.protocol] [rule.text_rule.community_subnet_lists] @@ -731,6 +737,12 @@ proxy [rule.default_enabled.excluded_protocol] +[rule.default_enabled.routed_dns_enabled] +0 +[rule.default_enabled.routed_dns_type] + +[rule.default_enabled.routed_dns_server] + [rule.default_enabled.protocol] [rule.default_enabled.community_subnet_lists] @@ -799,6 +811,12 @@ bypass [rule.enabled.excluded_protocol] +[rule.enabled.routed_dns_enabled] +0 +[rule.enabled.routed_dns_type] + +[rule.enabled.routed_dns_server] + [rule.enabled.protocol] [rule.enabled.community_subnet_lists] diff --git a/tests/runtime_state_predicates.sh b/tests/runtime_state_predicates.sh index 2fce7655..79ecacc3 100644 --- a/tests/runtime_state_predicates.sh +++ b/tests/runtime_state_predicates.sh @@ -1451,6 +1451,8 @@ zapret_runtime_signature=$(state_ucode zapret-runtime-signature-fixture "$WORK_D zapret2_queue_signature=$(state_ucode zapret2-queue-signature-fixture "$WORK_DIR/reload-state-signatures.json") zapret2_runtime_signature=$(state_ucode zapret2-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json") byedpi_runtime_signature=$(state_ucode byedpi-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json") +wdtt_runtime_signature=$(state_ucode wdtt-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json") +olcrtc_runtime_signature=$(state_ucode olcrtc-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json") list_signature=$(state_ucode list-update-signature-fixture "$WORK_DIR/reload-state-signatures.json") hosts_list_signature=$(state_ucode hosts-list-signature-fixture "$WORK_DIR/reload-state-signatures.json") cron_signature=$(state_ucode cron-signature-fixture "$WORK_DIR/reload-state-signatures.json")